Skip to content

Commit

Permalink
ShareableURLS/LogTable (#352)
Browse files Browse the repository at this point in the history
  • Loading branch information
Koustavd18 authored Nov 1, 2024
1 parent 5bc176e commit 22ab966
Show file tree
Hide file tree
Showing 10 changed files with 338 additions and 53 deletions.
55 changes: 55 additions & 0 deletions src/components/Header/ShareButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Stack, Menu, px } from '@mantine/core';
import { IconCopy, IconShare, IconFileTypeCsv, IconBraces } from '@tabler/icons-react';
import IconButton from '../Button/IconButton';
import { useAppStore } from '@/layouts/MainLayout/providers/AppProvider';
import { useCallback } from 'react';
import { copyTextToClipboard } from '@/utils';
import { downloadDataAsCSV, downloadDataAsJson } from '@/utils/exportHelpers';
import { makeExportData, useLogsStore } from '@/pages/Stream/providers/LogsProvider';

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

export default function ShareButton() {
const [isSecureHTTPContext] = useAppStore((store) => store.isSecureHTTPContext);
const [currentStream] = useAppStore((store) => store.currentStream);
const [filteredData] = useLogsStore((store) => store.data.filteredData);
const [tableOpts] = useLogsStore((store) => store.tableOpts);
const { headers } = tableOpts;

const exportHandler = useCallback(
(fileType: string | null) => {
const filename = `${currentStream}-logs`;
if (fileType === 'CSV') {
downloadDataAsCSV(makeExportData(filteredData, headers, 'CSV'), filename);
} else if (fileType === 'JSON') {
downloadDataAsJson(makeExportData(filteredData, headers, 'JSON'), filename);
}
},
[currentStream, filteredData, headers],
);
const copyUrl = useCallback(() => {
copyTextToClipboard(window.location.href);
}, [window.location.href]);
return (
<Menu width={200} position="bottom" withArrow shadow="md">
<Menu.Target>
<Stack style={{ padding: '0', background: 'transparent' }}>
<IconButton renderIcon={renderShareIcon} size={36} tooltipLabel="Share" />
</Stack>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item leftSection={<IconFileTypeCsv size={15} stroke={1.02} />} onClick={() => exportHandler('CSV')}>
Export CSV
</Menu.Item>
<Menu.Item leftSection={<IconBraces size={15} stroke={1.02} />} onClick={() => exportHandler('JSON')}>
Export JSON
</Menu.Item>
{isSecureHTTPContext && (
<Menu.Item leftSection={<IconCopy size={15} stroke={1.02} />} onClick={copyUrl}>
Copy URL
</Menu.Item>
)}
</Menu.Dropdown>
</Menu>
);
}
2 changes: 1 addition & 1 deletion src/hooks/useQueryLogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const { parseQuery } = filterStoreReducers;

const appendOffsetToQuery = (query: string, offset: number) => {
const hasOffset = query.toLowerCase().includes('offset');
return hasOffset ? query.replace(/offset\s+\d+/i, `OFFSET ${offset}`) : `${query} OFFSET ${offset}`;
return !hasOffset ? query.replace(/offset\s+\d+/i, `OFFSET ${offset}`) : `${query}`;
};

export const useQueryLogs = () => {
Expand Down
43 changes: 39 additions & 4 deletions src/pages/Dashboards/Toolbar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import TimeRange from '@/components/Header/TimeRange';
import { Box, Button, FileInput, Modal, px, Stack, Text, TextInput } from '@mantine/core';
import { IconCheck, IconFileDownload, IconPencil, IconPlus, IconShare, IconTrash } from '@tabler/icons-react';
import { Box, Button, FileInput, Modal, px, Stack, Text, Menu, TextInput } from '@mantine/core';
import {
IconCheck,
IconCopy,
IconFileArrowRight,
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';
Expand All @@ -10,6 +19,8 @@ import _ from 'lodash';
import ReactGridLayout, { Layout } from 'react-grid-layout';
import { Dashboard } from '@/@types/parseable/api/dashboards';
import { exportJson } from '@/utils/exportHelpers';
import { copyTextToClipboard } from '@/utils';
import { useAppStore } from '@/layouts/MainLayout/providers/AppProvider';

const {
toggleEditDashboardModal,
Expand Down Expand Up @@ -183,16 +194,40 @@ const DeleteDashboardButton = () => {
};

const ShareDashbboardButton = (props: { dashboard: Dashboard }) => {
const [isSecureHTTPContext] = useAppStore((store) => store.isSecureHTTPContext);
const { dashboard } = props;
const onClick = useCallback(async () => {
const downloadDashboard = useCallback(async () => {
const sanitizedConfig = _.omit(dashboard, ['dashboard_id', 'user_id', 'version']);
const { tiles } = dashboard;
const sanitizedTiles = _.map(tiles, (tile) => {
return _.omit(tile, 'tile_id');
});
return exportJson(JSON.stringify({ ...sanitizedConfig, tiles: sanitizedTiles }, null, 2), dashboard.name);
}, [dashboard]);
return <IconButton renderIcon={renderShareIcon} size={36} onClick={onClick} tooltipLabel="Share Dashboard" />;

const copyUrl = useCallback(() => {
copyTextToClipboard(window.location.href);
}, [window.location.href]);

return (
<Menu width={200} position="bottom" withArrow shadow="md">
<Menu.Target>
<Stack style={{ padding: '0', background: 'transparent' }}>
<IconButton renderIcon={renderShareIcon} size={36} tooltipLabel="Share Dashboard" />
</Stack>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item leftSection={<IconFileArrowRight size={15} stroke={1.02} />} onClick={downloadDashboard}>
Export
</Menu.Item>
{isSecureHTTPContext && (
<Menu.Item leftSection={<IconCopy size={15} stroke={1.02} />} onClick={copyUrl}>
Copy URL
</Menu.Item>
)}
</Menu.Dropdown>
</Menu>
);
};

const ImportTileModal = () => {
Expand Down
14 changes: 7 additions & 7 deletions src/pages/Dashboards/hooks/useParamsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ const paramsStringToParamsObj = (searchParams: URLSearchParams): Record<string,
};

const useParamsController = () => {
const [isStoreSyncd, setStoreSyncd] = useState(false);
const [isStoreSynced, setStoreSynced] = useState(false);
const [activeDashboard, setDashboardsStore] = useDashboardsStore((store) => store.activeDashboard);
const [timeRange, setLogsStore] = useLogsStore((store) => store.timeRange);
const [searchParams, setSearchParams] = useSearchParams();
Expand All @@ -78,12 +78,12 @@ const useParamsController = () => {
useEffect(() => {
const storeAsParams = storeToParamsObj({ dashboardId, timeRange });
const presentParams = paramsStringToParamsObj(searchParams);
syncTimeRangeToStore(storeAsParams, presentParams)
setStoreSyncd(true);
syncTimeRangeToStore(storeAsParams, presentParams);
setStoreSynced(true);
}, []);

useEffect(() => {
if (isStoreSyncd) {
if (isStoreSynced) {
const storeAsParams = storeToParamsObj({ dashboardId, timeRange });
const presentParams = paramsStringToParamsObj(searchParams);
if (_.isEqual(storeAsParams, presentParams)) return;
Expand All @@ -93,7 +93,7 @@ const useParamsController = () => {
}, [dashboardId, timeRange.startTime.toISOString(), timeRange.endTime.toISOString()]);

useEffect(() => {
if (!isStoreSyncd) return;
if (!isStoreSynced) return;

const storeAsParams = storeToParamsObj({ dashboardId, timeRange });
const presentParams = paramsStringToParamsObj(searchParams);
Expand All @@ -103,7 +103,7 @@ const useParamsController = () => {
setDashboardsStore((store) => selectDashboard(store, presentParams.id));
}

syncTimeRangeToStore(storeAsParams, presentParams)
syncTimeRangeToStore(storeAsParams, presentParams);
}, [searchParams]);

const syncTimeRangeToStore = useCallback(
Expand Down Expand Up @@ -131,7 +131,7 @@ const useParamsController = () => {
[],
);

return { isStoreSyncd };
return { isStoreSynced };
};

export default useParamsController;
8 changes: 4 additions & 4 deletions src/pages/Dashboards/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ const LoadingView = () => {
const Dashboards = () => {
const [dashboards] = useDashboardsStore((store) => store.dashboards);
const [createTileFormOpen] = useDashboardsStore((store) => store.createTileFormOpen);
const { isStoreSyncd } = useParamsController();
const { isStoreSynced } = useParamsController();
const { updateTimeRange } = useSyncTimeRange();
const { fetchDashboards } = useDashboardsQuery({ updateTimeRange });
useEffect(() => {
if (isStoreSyncd) {
if (isStoreSynced) {
fetchDashboards();
}
}, [isStoreSyncd]);
}, [isStoreSynced]);

return (
<Box
Expand All @@ -42,7 +42,7 @@ const Dashboards = () => {
width: '100%',
overflow: 'hidden',
}}>
{dashboards === null || !isStoreSyncd ? (
{dashboards === null || !isStoreSynced ? (
<LoadingView />
) : createTileFormOpen ? (
<CreateTileForm />
Expand Down
33 changes: 7 additions & 26 deletions src/pages/Stream/Views/Explore/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
import { FC, useCallback } from 'react';
import { useLogsStore, logsStoreReducers, LOAD_LIMIT, LOG_QUERY_LIMITS } from '../../providers/LogsProvider';
import { useAppStore } from '@/layouts/MainLayout/providers/AppProvider';
import { usePagination } from '@mantine/hooks';
import { downloadDataAsCSV, downloadDataAsJson } from '@/utils/exportHelpers';
import { Box, Center, Group, Loader, Menu, Pagination, px, Stack, Tooltip } from '@mantine/core';
import { Box, Center, Group, Loader, Menu, Pagination, Stack, Tooltip } from '@mantine/core';
import _ from 'lodash';
import { Text } from '@mantine/core';
import { HumanizeNumber } from '@/utils/formatBytes';
import IconButton from '@/components/Button/IconButton';
import { IconDownload, IconSelector } from '@tabler/icons-react';
import { IconSelector } from '@tabler/icons-react';
import useMountedState from '@/hooks/useMountedState';
import classes from '../../styles/Footer.module.css';
import { LOGS_FOOTER_HEIGHT } from '@/constants/theme';

const { setPageAndPageData, setCurrentPage, setCurrentOffset, makeExportData } = logsStoreReducers;
const { setPageAndPageData, setCurrentPage, setCurrentOffset } = logsStoreReducers;

const TotalCount = (props: { totalCount: number }) => {
return (
Expand All @@ -23,8 +20,6 @@ const TotalCount = (props: { totalCount: number }) => {
);
};

const renderExportIcon = () => <IconDownload size={px('0.8rem')} stroke={1.8} />;

const TotalLogsCount = (props: { hasTableLoaded: boolean; isFetchingCount: boolean; isTableEmpty: boolean }) => {
const [{ totalCount, perPage, pageData }] = useLogsStore((store) => store.tableOpts);
const displayedCount = _.size(pageData);
Expand Down Expand Up @@ -94,10 +89,8 @@ const LimitControl: FC = () => {
};

const Footer = (props: { loaded: boolean; hasNoData: boolean; isFetchingCount: boolean }) => {
const [currentStream] = useAppStore((store) => store.currentStream);
const [tableOpts, setLogsStore] = useLogsStore((store) => store.tableOpts);
const [filteredData] = useLogsStore((store) => store.data.filteredData);
const { totalPages, currentOffset, currentPage, perPage, headers, totalCount } = tableOpts;
const { totalPages, currentOffset, currentPage, perPage, totalCount } = tableOpts;

const onPageChange = useCallback((page: number) => {
setLogsStore((store) => setPageAndPageData(store, page));
Expand All @@ -123,20 +116,8 @@ const Footer = (props: { loaded: boolean; hasNoData: boolean; isFetchingCount: b
[currentOffset],
);

const exportHandler = useCallback(
(fileType: string | null) => {
const filename = `${currentStream}-logs`;
if (fileType === 'CSV') {
downloadDataAsCSV(makeExportData(filteredData, headers, 'CSV'), filename);
} else if (fileType === 'JSON') {
downloadDataAsJson(makeExportData(filteredData, headers, 'JSON'), filename);
}
},
[currentStream, filteredData, headers],
);

return (
<Stack className={classes.footerContainer} gap={0} style={{height: LOGS_FOOTER_HEIGHT}}>
<Stack className={classes.footerContainer} gap={0} style={{ height: LOGS_FOOTER_HEIGHT }}>
<Stack w="100%" justify="center" align="flex-start">
<TotalLogsCount
hasTableLoaded={props.loaded}
Expand Down Expand Up @@ -190,7 +171,7 @@ const Footer = (props: { loaded: boolean; hasNoData: boolean; isFetchingCount: b
) : null}
</Stack>
<Stack w="100%" align="flex-end" style={{ flexDirection: 'row', justifyContent: 'flex-end' }}>
{props.loaded && (
{/* {props.loaded && (
<Menu position="top">
<Menu.Target>
<div>
Expand All @@ -206,7 +187,7 @@ const Footer = (props: { loaded: boolean; hasNoData: boolean; isFetchingCount: b
</Menu.Item>
</Menu.Dropdown>
</Menu>
)}
)} */}
<LimitControl />
</Stack>
</Stack>
Expand Down
4 changes: 3 additions & 1 deletion src/pages/Stream/components/PrimaryToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { notifications } from '@mantine/notifications';
import { useParams } from 'react-router-dom';
import _ from 'lodash';
import StreamingButton from '@/components/Header/StreamingButton';
import ShareButton from '@/components/Header/ShareButton';
import { useLogsStore, logsStoreReducers } from '../providers/LogsProvider';
import { filterStoreReducers, useFilterStore } from '../providers/FilterProvider';
import classes from './styles/PrimaryToolbar.module.css';
Expand Down Expand Up @@ -89,7 +90,7 @@ const ViewToggle = () => {

const PrimaryToolbar = () => {
const [maximized] = useAppStore((store) => store.maximized);
const [hasDeleteStreamAccess] = useAppStore(store => store.userAccessMap.hasDeleteStreamAccess)
const [hasDeleteStreamAccess] = useAppStore((store) => store.userAccessMap.hasDeleteStreamAccess);
const { view } = useParams();

useEffect(() => {
Expand Down Expand Up @@ -119,6 +120,7 @@ const PrimaryToolbar = () => {
<RefreshInterval />
<RefreshNow />
<ViewToggle />
<ShareButton />
<MaximizeButton />
</Stack>
) : view === 'live-tail' ? (
Expand Down
Loading

0 comments on commit 22ab966

Please sign in to comment.