Skip to content
This repository was archived by the owner on May 13, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions src/pages/Dashboards/CreateTileForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,36 +150,36 @@ const DataPreview = (props: { form: TileFormType }) => {
} = props;
const containerRef: MutableRefObject<HTMLDivElement | null> = useRef(null);
const [containerSize, setContainerSize] = useState({ height: 0, width: 0 });
const errorMsg = getErrorMsg(props.form, 'data');
useEffect(() => {
if (containerRef.current) {
setContainerSize({
height: containerRef.current?.offsetHeight || 0,
width: containerRef.current?.offsetWidth || 0,
});
}
}, []);
const errorMsg = getErrorMsg(props.form, 'data');
}, [errorMsg]);

return (
<Stack className={classes.sectionContainer} gap={0}>
<SectionHeader title="Data Preview" />
<ScrollArea style={{flex: 1}}>
<Stack ref={containerRef} style={{ flex: 1 }}>
<Stack style={{ width: containerSize.width, height: containerSize.height }}>
{errorMsg ? (
<WarningView msg={errorMsg} />
) : (
{errorMsg ? (
<WarningView msg={errorMsg} />
) : (
<ScrollArea style={{ flex: 1 }}>
<Stack ref={containerRef} style={{ flex: 1 }}>
<Stack style={{ width: containerSize.width, height: containerSize.height }}>
<CodeHighlight
code={JSON.stringify(data?.records || [], null, 2)}
style={{ background: 'white' }}
language="json"
styles={{ copy: { marginLeft: '550px' } }}
copyLabel="Copy Records"
/>
)}
</Stack>
</Stack>
</Stack>
</ScrollArea>
</ScrollArea>
)}
</Stack>
);
};
Expand Down
98 changes: 93 additions & 5 deletions src/pages/Dashboards/SideBar.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<File | null>(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<FileReader>) => {
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 (
<Modal
opened={importDashboardModalOpen}
onClose={closeModal}
size="auto"
centered
styles={{
body: { padding: '0 1rem 1rem 1rem', width: 400 },
header: { padding: '1rem', paddingBottom: '0.4rem' },
}}
title={<Text style={{ fontSize: '0.9rem', fontWeight: 600 }}>Import Dashboard</Text>}>
<Stack gap={24}>
<FileInput
style={{ marginTop: '0.25rem' }}
label=""
placeholder="Import Parseable dashboard config json"
fileInputProps={{ accept: '.json' }}
value={file}
onChange={setFile}
/>
<Stack style={{ flexDirection: 'row', justifyContent: 'flex-end' }}>
<Box>
<Button onClick={closeModal} variant="outline">
Cancel
</Button>
</Box>
<Box>
<Button disabled={file === null} onClick={onImport} loading={isCreatingDashboard}>
Import
</Button>
</Box>
</Stack>
</Stack>
</Modal>
);
};

const renderShareIcon = () => <IconFileDownload size={px('1rem')} stroke={1.5} />;

const ImportDashboardButton = () => {
const [_store, setDashbaordsStore] = useDashboardsStore((_store) => null);
const onClick = useCallback(() => {
setDashbaordsStore((store) => toggleImportDashboardModal(store, true));
}, []);
return <IconButton renderIcon={renderShareIcon} size={36} onClick={onClick} tooltipLabel="Import Dashboard" />;
};

const SideBar = (props: { updateTimeRange: (dashboard: Dashboard) => void }) => {
const [dashboards, setDashbaordsStore] = useDashboardsStore((store) => store.dashboards);

Expand All @@ -75,14 +161,16 @@ const SideBar = (props: { updateTimeRange: (dashboard: Dashboard) => void }) =>

return (
<Stack style={{ width: DASHBOARDS_SIDEBAR_WIDTH }} className={classes.container}>
<Stack style={{ padding: '0.75rem', paddingBottom: 0, justifyContent: 'center' }}>
<ImportDashboardModal />
<Stack style={{ padding: '0.75rem', paddingBottom: 0, justifyContent: 'center', flexDirection: 'row' }}>
<Button
variant="outline"
className={classes.createDashboardBtn}
onClick={openCreateStreamModal}
leftSection={<IconPlus stroke={2} size={'1rem'} />}>
New Dashboard
</Button>
<ImportDashboardButton />
</Stack>
<DashboardList updateTimeRange={props.updateTimeRange} />
</Stack>
Expand Down
154 changes: 107 additions & 47 deletions src/pages/Dashboards/Tile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
IconGripVertical,
IconPencil,
IconPhoto,
IconShare,
IconTable,
IconTrash,
} from '@tabler/icons-react';
Expand All @@ -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 = () => (
<div className="png-export-parseable-logo" style={{ display: 'none', height: '100%' }}>
<svg height={20} id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<style>
{`
.cls-1 {
fill: #fc466b;
}
.cls-2 {
fill: #545beb;
}
`}
</style>
<g>
<path
className="cls-2"
d="M13.92,7.76l-5.93,5.93c-.26,.26-.06,.71,.31,.68,1.57-.12,3.11-.78,4.31-1.99s1.86-2.74,1.99-4.31c.03-.37-.42-.57-.68-.31Z"
/>
<path
className="cls-2"
d="M13.97,4.61v-.02c-.36-.74-1.33-.92-1.91-.34l-7.58,7.58c-.58,.58-.4,1.55,.34,1.9h.02c.44,.22,.97,.12,1.32-.23l7.57-7.57c.35-.35,.45-.87,.24-1.32Z"
/>
<path
className="cls-2"
d="M7.54,1.38c.26-.26,.06-.71-.31-.68-1.57,.12-3.11,.78-4.31,1.99S1.05,5.43,.93,7c-.03,.37,.42,.57,.68,.31L7.54,1.38Z"
/>
</g>
<g>
<path
className="cls-2"
d="M2.67,8.27l-.87,.87c-.35,.35-.44,.88-.23,1.33v.02c.36,.73,1.33,.9,1.9,.33l.88-.88c.46-.46,.46-1.21,0-1.68h0c-.46-.46-1.21-.46-1.68,0Z"
/>
<path
className="cls-1"
d="M7.09,7.2l3.96-3.96c.57-.57,.41-1.54-.33-1.89h-.02c-.45-.22-.98-.13-1.33,.22l-3.96,3.96c-.46,.46-.46,1.21,0,1.68h0c.46,.46,1.21,.46,1.68,0Z"
/>
</g>
</svg>
</div>
);

const { toggleCreateTileModal, toggleDeleteTileModal } = dashboardsStoreReducers;

const NoDataView = () => {
Expand Down Expand Up @@ -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]);

Expand All @@ -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 <IconGripVertical className={classes.tileControlIcon + ' ' + classes.dragIcon} stroke={2} size="1rem" />;

return (
<Menu shadow="md" width={240} position="bottom-start">
<Menu.Target>
<IconDotsVertical className={classes.tileControlIcon} stroke={1} size="1rem" />
</Menu.Target>
<Menu.Dropdown style={{ padding: '0.25rem 0.25rem' }}>
<Text className={classes.tileCtrlLabel}>Actions</Text>
<Menu.Item
className={classes.tileCtrlItem}
onClick={openEditTile}
leftSection={<IconPencil className={classes.tileCtrlItemIcon} size="1rem" stroke={1.2} />}>
<Text className={classes.tileCtrlItemText}>Edit</Text>
</Menu.Item>
<Menu.Item
className={classes.tileCtrlItem}
onClick={openDeleteModal}
leftSection={<IconTrash className={classes.tileCtrlItemIcon} size="1rem" stroke={1.2} />}>
<Text className={classes.tileCtrlItemText}>Delete</Text>
</Menu.Item>
<Menu.Divider />
<Text className={classes.tileCtrlLabel}>Exports</Text>
<Menu.Item
onClick={exportPng}
className={classes.tileCtrlItem}
leftSection={<IconPhoto className={classes.tileCtrlItemIcon} size="1rem" stroke={1.2} />}>
<Text className={classes.tileCtrlItemText}>PNG</Text>
</Menu.Item>
<Menu.Item
onClick={exportCSV}
className={classes.tileCtrlItem}
leftSection={<IconTable className={classes.tileCtrlItemIcon} size="1rem" stroke={1.2} />}>
<Text className={classes.tileCtrlItemText}>CSV</Text>
</Menu.Item>
<Menu.Item
onClick={exportJson}
className={classes.tileCtrlItem}
leftSection={<IconBraces className={classes.tileCtrlItemIcon} size="1rem" stroke={1.2} />}>
<Text className={classes.tileCtrlItemText}>JSON</Text>
</Menu.Item>
</Menu.Dropdown>
</Menu>
<div className="png-export-menu-icon">
<Menu shadow="md" width={240} position="bottom-start">
<Menu.Target>
<IconDotsVertical className={classes.tileControlIcon} stroke={1} size="1rem" />
</Menu.Target>
<Menu.Dropdown style={{ padding: '0.25rem 0.25rem' }}>
<Text className={classes.tileCtrlLabel}>Actions</Text>
<Menu.Item
className={classes.tileCtrlItem}
onClick={openEditTile}
leftSection={<IconPencil className={classes.tileCtrlItemIcon} size="1rem" stroke={1.2} />}>
<Text className={classes.tileCtrlItemText}>Edit</Text>
</Menu.Item>
<Menu.Item
className={classes.tileCtrlItem}
onClick={exportTileConfig}
leftSection={<IconShare className={classes.tileCtrlItemIcon} size="1rem" stroke={1.2} />}>
<Text className={classes.tileCtrlItemText}>Share</Text>
</Menu.Item>
<Menu.Item
className={classes.tileCtrlItem}
onClick={openDeleteModal}
leftSection={<IconTrash className={classes.tileCtrlItemIcon} size="1rem" stroke={1.2} />}>
<Text className={classes.tileCtrlItemText}>Delete</Text>
</Menu.Item>
<Menu.Divider />
<Text className={classes.tileCtrlLabel}>Exports</Text>
<Menu.Item
onClick={exportPng}
className={classes.tileCtrlItem}
leftSection={<IconPhoto className={classes.tileCtrlItemIcon} size="1rem" stroke={1.2} />}>
<Text className={classes.tileCtrlItemText}>PNG</Text>
</Menu.Item>
<Menu.Item
onClick={exportCSV}
className={classes.tileCtrlItem}
leftSection={<IconTable className={classes.tileCtrlItemIcon} size="1rem" stroke={1.2} />}>
<Text className={classes.tileCtrlItemText}>CSV</Text>
</Menu.Item>
<Menu.Item
onClick={exportDataAsJson}
className={classes.tileCtrlItem}
leftSection={<IconBraces className={classes.tileCtrlItemIcon} size="1rem" stroke={1.2} />}>
<Text className={classes.tileCtrlItemText}>JSON</Text>
</Menu.Item>
</Menu.Dropdown>
</Menu>
</div>
);
}

Expand Down Expand Up @@ -205,22 +260,27 @@ const Tile = (props: { id: string }) => {
const Viz = getViz(vizType);

return (
<Stack h="100%" gap={0} className={classes.container}>
<Stack className={classes.tileHeader} gap={0}>
<Stack h="100%" gap={0} className={`png-export-tile-container ${classes.container}`}>
<Stack className={`png-export-tile-header ${classes.tileHeader}`} gap={0}>
<Stack gap={0}>
<Text title={tile.name} lineClamp={1} className={classes.tileTitle}>
<Text title={tile.name} lineClamp={1} className={`png-export-tile-title ${classes.tileTitle}`}>
{tile.name}
</Text>
<Text title={tile.description} className={classes.tileDescription} lineClamp={1}>
<Text
title={tile.description}
className={`png-export-tile-description ${classes.tileDescription}`}
lineClamp={1}>
{tile.description}
</Text>
<Text className={`png-export-tile-timerange ${classes.tileTimeRangeText}`}>{timeRange.label}</Text>
</Stack>
<TileControls tile={tile} data={tileData} />
<ParseableLogo />
</Stack>
{isLoading && <LoadingView />}
{!hasData && !isLoading && <NoDataView />}
{!isLoading && hasData && (
<Stack className={classes.tileContainer} style={{ flex: 1 }}>
<Stack className={`png-export-viz-container ${classes.tileContainer}`} style={{ flex: 1 }}>
{Viz && <Viz tile={tile} data={tileData} tick_config={tick_config} />}
</Stack>
)}
Expand Down
Loading
Loading