From 0d7ad11294c1756f66d773c45b87f4764d4a310d Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Thu, 19 Sep 2024 23:30:59 +0530 Subject: [PATCH 1/4] copy tile, dashboard config and png export cleanup --- src/pages/Dashboards/Tile.tsx | 153 ++++++++++++++------ src/pages/Dashboards/Toolbar.tsx | 21 ++- src/pages/Dashboards/styles/tile.module.css | 11 +- src/utils/exportImage.ts | 52 ++++++- src/utils/index.ts | 8 + 5 files changed, 196 insertions(+), 49 deletions(-) diff --git a/src/pages/Dashboards/Tile.tsx b/src/pages/Dashboards/Tile.tsx index a8f64a91..bb0ad633 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'; @@ -28,6 +29,49 @@ import Table from './Table'; import { downloadDataAsCSV, downloadDataAsJson } from '@/utils/exportHelpers'; import { makeExportData, useLogsStore } from '../Stream/providers/LogsProvider'; import { getRandomUnitTypeForChart, getUnitTypeByKey } from './utils'; +import { copyTextToClipboard } from '@/utils'; +import { notifySuccess } from '@/utils/notification'; + +const ParseableLogo = () => ( +
+ + + + + + + + + + + + +
+); const { toggleCreateTileModal, toggleDeleteTileModal } = dashboardsStoreReducers; @@ -130,50 +174,64 @@ function TileControls(props: { tile: TileType; data: TileQueryResponse }) { setDashboardsStore((store) => toggleDeleteTileModal(store, true, tile_id)); }, []); + const copyTileConfig = useCallback(async () => { + const santizedConfig = _.omit(props.tile, 'tile_id'); + await copyTextToClipboard(santizedConfig); + notifySuccess({ message: 'Tile config copied to clipboard' }); + }, []); + if (allowDrag) return ; return ( - - - - - - Actions - }> - Edit - - }> - Delete - - - Exports - }> - PNG - - }> - CSV - - }> - JSON - - - +
+ + + + + + Actions + }> + Edit + + }> + Share + + }> + Delete + + + Exports + }> + PNG + + }> + CSV + + }> + JSON + + + +
); } @@ -205,22 +263,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..17a1952d 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 { IconCheck, 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,6 +8,9 @@ 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 { copyTextToClipboard } from '@/utils'; +import { notifySuccess } from '@/utils/notification'; const { toggleEditDashboardModal, toggleAllowDrag, toggleCreateTileModal, toggleDeleteDashboardModal } = dashboardsStoreReducers; @@ -145,6 +148,7 @@ const DeleteDashboardModal = () => { }; const renderDeleteIcon = () => ; +const renderShareIcon = () => ; const DeleteDashboardButton = () => { const [_store, setDashbaordsStore] = useDashboardsStore((_store) => null); @@ -152,6 +156,20 @@ const DeleteDashboardButton = () => { return ; }; +const ShareDashbboardButton = (props: { dashboard: Dashboard }) => { + const { dashboard } = props; + const onClick = useCallback(async () => { + const sanitizedConfig = _.omit(dashboard, ['dashboard_id', 'user_id', 'version']); + const { tiles } = dashboard; + const sanitizedTiles = _.map(tiles, (tile) => { + return _.omit(tile, 'tile_id'); + }); + await copyTextToClipboard({ ...sanitizedConfig, tiles: sanitizedTiles }); + notifySuccess({ message: 'Dashboard config copied to clipboard' }); + }, [dashboard]); + return ; +}; + const Toolbar = (props: { layoutRef: React.MutableRefObject }) => { const [activeDashboard, setDashbaordsStore] = useDashboardsStore((store) => store.activeDashboard); const openEditDashboardModal = useCallback(() => { @@ -185,6 +203,7 @@ const Toolbar = (props: { layoutRef: React.MutableRefObject + 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/exportImage.ts b/src/utils/exportImage.ts index e3d512ff..1048724e 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 = (document: Document, 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 = document.querySelector('.png-export-tile-timerange') as HTMLElement; + const logoImage = document.querySelector('.png-export-parseable-logo') as HTMLElement; + if (headerDiv) { + headerDiv.style.height = 'auto'; + headerDiv.style.alignItems = 'flex-start'; + } + // rm border + 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(document, 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 From 3360b4ddcc3e39f95e78940baebd35d55c921879 Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Fri, 20 Sep 2024 16:24:20 +0530 Subject: [PATCH 2/4] minor bugs --- src/pages/Dashboards/CreateTileForm.tsx | 22 +++++++++++----------- src/pages/Dashboards/Toolbar.tsx | 2 +- src/utils/exportImage.ts | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) 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/Toolbar.tsx b/src/pages/Dashboards/Toolbar.tsx index 17a1952d..01857606 100644 --- a/src/pages/Dashboards/Toolbar.tsx +++ b/src/pages/Dashboards/Toolbar.tsx @@ -131,7 +131,7 @@ const DeleteDashboardModal = () => { - + + + + + + + + + ); +}; + +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 bb0ad633..0089ced3 100644 --- a/src/pages/Dashboards/Tile.tsx +++ b/src/pages/Dashboards/Tile.tsx @@ -26,11 +26,9 @@ 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'; -import { copyTextToClipboard } from '@/utils'; -import { notifySuccess } from '@/utils/notification'; const ParseableLogo = () => (
@@ -162,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]); @@ -174,11 +172,10 @@ function TileControls(props: { tile: TileType; data: TileQueryResponse }) { setDashboardsStore((store) => toggleDeleteTileModal(store, true, tile_id)); }, []); - const copyTileConfig = useCallback(async () => { + const exportTileConfig = useCallback(async () => { const santizedConfig = _.omit(props.tile, 'tile_id'); - await copyTextToClipboard(santizedConfig); - notifySuccess({ message: 'Tile config copied to clipboard' }); - }, []); + return exportJson(JSON.stringify(santizedConfig, null, 2), name) + }, [name]); if (allowDrag) return ; @@ -199,7 +196,7 @@ function TileControls(props: { tile: TileType; data: TileQueryResponse }) { }> Share @@ -224,7 +221,7 @@ function TileControls(props: { tile: TileType; data: TileQueryResponse }) { CSV }> JSON diff --git a/src/pages/Dashboards/Toolbar.tsx b/src/pages/Dashboards/Toolbar.tsx index 01857606..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, IconShare, 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'; @@ -9,11 +9,15 @@ import { useDashboardsQuery } from '@/hooks/useDashboards'; import _ from 'lodash'; import ReactGridLayout, { Layout } from 'react-grid-layout'; import { Dashboard } from '@/@types/parseable/api/dashboards'; -import { copyTextToClipboard } from '@/utils'; -import { notifySuccess } from '@/utils/notification'; +import { exportJson } from '@/utils/exportHelpers'; -const { toggleEditDashboardModal, toggleAllowDrag, toggleCreateTileModal, toggleDeleteDashboardModal } = - dashboardsStoreReducers; +const { + toggleEditDashboardModal, + toggleAllowDrag, + toggleCreateTileModal, + toggleDeleteDashboardModal, + toggleImportTileModal, +} = dashboardsStoreReducers; const tileIdsbyOrder = (layout: Layout[]) => { return layout @@ -80,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); @@ -164,12 +188,85 @@ const ShareDashbboardButton = (props: { dashboard: Dashboard }) => { const sanitizedTiles = _.map(tiles, (tile) => { return _.omit(tile, 'tile_id'); }); - await copyTextToClipboard({ ...sanitizedConfig, tiles: sanitizedTiles }); - notifySuccess({ message: 'Dashboard config copied to clipboard' }); + return exportJson(JSON.stringify({ ...sanitizedConfig, tiles: sanitizedTiles }, null, 2), dashboard.name); }, [dashboard]); return ; }; +const ImportTileModal = () => { + const [importTileModalOpen, setDashboardStore] = useDashboardsStore((store) => store.importTileModalOpen); + const [activeDashboard] = useDashboardsStore((store) => store.activeDashboard); + const [file, setFile] = useState(null); + const closeModal = useCallback(() => { + setDashboardStore((store) => toggleImportTileModal(store, false)); + }, []); + const { updateDashboard, isUpdatingDashboard } = useDashboardsQuery({}); + const onImport = useCallback(() => { + if (activeDashboard === null || file === null) return; + + const existingTiles = activeDashboard.tiles; + if (file) { + const reader = new FileReader(); + reader.onload = (e: ProgressEvent) => { + try { + const target = e.target; + if (target === null || typeof target.result !== 'string') return; + + const newTile = JSON.parse(target.result); + if (_.isEmpty(newTile)) return; + + return updateDashboard({ + dashboard: { ...activeDashboard, tiles: [...existingTiles, newTile] }, + onSuccess: () => { + closeModal(); + setFile(null); + }, + }); + } catch (error) {} + }; + reader.readAsText(file); + } else { + console.error('No file selected.'); + } + }, [activeDashboard, file]); + + return ( + Import Tile}> + + + + + + + + + + + + + ); +}; + const Toolbar = (props: { layoutRef: React.MutableRefObject }) => { const [activeDashboard, setDashbaordsStore] = useDashboardsStore((store) => store.activeDashboard); const openEditDashboardModal = useCallback(() => { @@ -186,6 +283,7 @@ const Toolbar = (props: { layoutRef: React.MutableRefObject + @@ -202,6 +300,7 @@ 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/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; From 862a1158c0f38cb592fb78a27786407f29b7befa Mon Sep 17 00:00:00 2001 From: balaji-jr Date: Sat, 21 Sep 2024 00:55:43 +0530 Subject: [PATCH 4/4] modify cloned element on exporting png --- src/utils/exportImage.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/utils/exportImage.ts b/src/utils/exportImage.ts index fe4eabf0..a9a908ae 100644 --- a/src/utils/exportImage.ts +++ b/src/utils/exportImage.ts @@ -6,14 +6,14 @@ export const makeExportClassName = (name: string) => { return `png-capture-${sanitizedName}`; }; -const onCloneCallback = (document: Document, element: HTMLElement) => { +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 = document.querySelector('.png-export-tile-timerange') as HTMLElement; - const logoImage = document.querySelector('.png-export-parseable-logo') 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'; @@ -58,8 +58,8 @@ const handleCapture = (opts: { className: string; fileName: string }) => { html2canvas(element, { scale: 3, backgroundColor: null, - onclone(document, element) { - onCloneCallback(document, element); + onclone(_document, element) { + onCloneCallback(element); }, }).then((canvas) => { const imgData = canvas.toDataURL('image/png');