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
4 changes: 4 additions & 0 deletions src/@types/parseable/api/dashboards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ export type UpdateDashboardType = Omit<Dashboard, 'tiles'> & {
tiles: EditTileType[];
};

export type ImportDashboardType = Omit<Dashboard, 'tiles' | 'dashboard_id'> & {
tiles: EditTileType[];
};

export type TileQuery = { query: string; startTime: Date; endTime: Date };

export type TileData = Log[];
Expand Down
3 changes: 2 additions & 1 deletion src/api/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import _ from 'lodash';
import {
CreateDashboardType,
Dashboard,
ImportDashboardType,
TileQuery,
TileQueryResponse,
UpdateDashboardType,
Expand All @@ -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});
};

Expand Down
26 changes: 26 additions & 0 deletions src/hooks/useDashboards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useCallback, useState } from 'react';
import {
CreateDashboardType,
Dashboard,
ImportDashboardType,
TileQuery,
TileQueryResponse,
UpdateDashboardType,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -113,6 +137,8 @@ export const useDashboardsQuery = (opts: { updateTimeRange?: (dashboard: Dashboa
isCreatingDashboard,
updateDashboard,
isUpdatingDashboard,
importDashboard,
isImportingDashboard,

deleteDashboard,
isDeleting,
Expand Down
182 changes: 172 additions & 10 deletions src/pages/Dashboards/Dashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,72 @@
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';
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<boolean>(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 <NoTilesView />;

return (
<Stack className={classes.tilesViewConatiner} style={{ overflowY: 'scroll' }}>
<Stack ref={scrollRef} className={classes.tilesViewConatiner} style={{ overflowY: 'scroll' }}>
<GridLayout
className="layout"
layout={layout}
Expand Down Expand Up @@ -111,25 +151,146 @@ const DeleteTileModal = () => {
);
};

const DashboardTemplates = (props: {onImport: (template: ImportDashboardType) => void; isImportingDashboard: boolean}) => {
return (
<Stack gap={0} mt={6}>
{_.map(templates, (template) => {
return (
<Stack style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<Text style={{ fontSize: '0.76rem' }} c="gray.7">
{template.name}
</Text>
<Box>
<Button
disabled={props.isImportingDashboard}
loading={props.isImportingDashboard}
onClick={() => props.onImport(template)}
variant="outline">
Select
</Button>
</Box>
</Stack>
);
})}
</Stack>
);
}

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<File | null>(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<FileReader>) => {
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 (
<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}>
{!isStandAloneMode && (
<>
<DashboardTemplates onImport={makePostCall} isImportingDashboard={isImportingDashboard} />
<Divider label="OR" />
</>
)}
<FileInput
style={{ marginTop: '0.25rem' }}
label=""
placeholder="Import dashboard config downloaded from Parseable"
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 || isImportingDashboard} onClick={onImport} loading={isImportingDashboard}>
Import
</Button>
</Box>
</Stack>
</Stack>
</Modal>
);
};

const NoDashboardsView = () => {
const [, setDashboardsStore] = useDashboardsStore((_store) => null);

const openCreateDashboardModal = useCallback(() => {
setDashboardsStore((store) => toggleCreateDashboardModal(store, true));
}, []);

const openImportDashboardModal = useCallback(() => {
setDashboardsStore((store) => toggleImportDashboardModal(store, true));
}, []);

return (
<Stack className={classes.noDashboardsContainer} gap={4}>
<ImportDashboardModal />
<Stack className={classes.dashboardIconContainer}>
<IconLayoutDashboard className={classes.dashboardIcon} stroke={1.2} />
</Stack>
<Text className={classes.noDashboardsViewTitle}>Create dashboard</Text>
<Text className={classes.noDashboardsViewDescription}>
Create your first dashboard to visualize log events from various streams.
</Text>
<Box mt={4}>
<Button onClick={openCreateDashboardModal}>Create Dashboard</Button>
</Box>
<Stack gap={14} mt={4}>
<Box>
<Button variant="outline" onClick={openImportDashboardModal}>
Import Dashboard
</Button>
</Box>
<Box>
<Button onClick={openCreateDashboardModal}>Create Dashboard</Button>
</Box>
</Stack>
</Stack>
);
};
Expand Down Expand Up @@ -173,6 +334,7 @@ const Dashboard = () => {
<Stack style={{ flex: 1 }} gap={0}>
<DeleteTileModal />
<Toolbar layoutRef={layoutRef} />
<ImportDashboardModal/>
<TilesView onLayoutChange={onLayoutChange} />
</Stack>
);
Expand Down
Loading
Loading