Skip to content
This repository was archived by the owner on May 13, 2025. It is now read-only.

Commit 22ab966

Browse files
authored
ShareableURLS/LogTable (#352)
1 parent 5bc176e commit 22ab966

File tree

10 files changed

+338
-53
lines changed

10 files changed

+338
-53
lines changed

src/components/Header/ShareButton.tsx

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { Stack, Menu, px } from '@mantine/core';
2+
import { IconCopy, IconShare, IconFileTypeCsv, IconBraces } from '@tabler/icons-react';
3+
import IconButton from '../Button/IconButton';
4+
import { useAppStore } from '@/layouts/MainLayout/providers/AppProvider';
5+
import { useCallback } from 'react';
6+
import { copyTextToClipboard } from '@/utils';
7+
import { downloadDataAsCSV, downloadDataAsJson } from '@/utils/exportHelpers';
8+
import { makeExportData, useLogsStore } from '@/pages/Stream/providers/LogsProvider';
9+
10+
const renderShareIcon = () => <IconShare size={px('1rem')} stroke={1.5} />;
11+
12+
export default function ShareButton() {
13+
const [isSecureHTTPContext] = useAppStore((store) => store.isSecureHTTPContext);
14+
const [currentStream] = useAppStore((store) => store.currentStream);
15+
const [filteredData] = useLogsStore((store) => store.data.filteredData);
16+
const [tableOpts] = useLogsStore((store) => store.tableOpts);
17+
const { headers } = tableOpts;
18+
19+
const exportHandler = useCallback(
20+
(fileType: string | null) => {
21+
const filename = `${currentStream}-logs`;
22+
if (fileType === 'CSV') {
23+
downloadDataAsCSV(makeExportData(filteredData, headers, 'CSV'), filename);
24+
} else if (fileType === 'JSON') {
25+
downloadDataAsJson(makeExportData(filteredData, headers, 'JSON'), filename);
26+
}
27+
},
28+
[currentStream, filteredData, headers],
29+
);
30+
const copyUrl = useCallback(() => {
31+
copyTextToClipboard(window.location.href);
32+
}, [window.location.href]);
33+
return (
34+
<Menu width={200} position="bottom" withArrow shadow="md">
35+
<Menu.Target>
36+
<Stack style={{ padding: '0', background: 'transparent' }}>
37+
<IconButton renderIcon={renderShareIcon} size={36} tooltipLabel="Share" />
38+
</Stack>
39+
</Menu.Target>
40+
<Menu.Dropdown>
41+
<Menu.Item leftSection={<IconFileTypeCsv size={15} stroke={1.02} />} onClick={() => exportHandler('CSV')}>
42+
Export CSV
43+
</Menu.Item>
44+
<Menu.Item leftSection={<IconBraces size={15} stroke={1.02} />} onClick={() => exportHandler('JSON')}>
45+
Export JSON
46+
</Menu.Item>
47+
{isSecureHTTPContext && (
48+
<Menu.Item leftSection={<IconCopy size={15} stroke={1.02} />} onClick={copyUrl}>
49+
Copy URL
50+
</Menu.Item>
51+
)}
52+
</Menu.Dropdown>
53+
</Menu>
54+
);
55+
}

src/hooks/useQueryLogs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const { parseQuery } = filterStoreReducers;
1717

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

2323
export const useQueryLogs = () => {

src/pages/Dashboards/Toolbar.tsx

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
import TimeRange from '@/components/Header/TimeRange';
2-
import { Box, Button, FileInput, Modal, px, Stack, Text, TextInput } from '@mantine/core';
3-
import { IconCheck, IconFileDownload, IconPencil, IconPlus, IconShare, IconTrash } from '@tabler/icons-react';
2+
import { Box, Button, FileInput, Modal, px, Stack, Text, Menu, TextInput } from '@mantine/core';
3+
import {
4+
IconCheck,
5+
IconCopy,
6+
IconFileArrowRight,
7+
IconFileDownload,
8+
IconPencil,
9+
IconPlus,
10+
IconShare,
11+
IconTrash,
12+
} from '@tabler/icons-react';
413
import classes from './styles/toolbar.module.css';
514
import { useDashboardsStore, dashboardsStoreReducers, sortTilesByOrder } from './providers/DashboardsProvider';
615
import { ChangeEvent, useCallback, useState } from 'react';
@@ -10,6 +19,8 @@ import _ from 'lodash';
1019
import ReactGridLayout, { Layout } from 'react-grid-layout';
1120
import { Dashboard } from '@/@types/parseable/api/dashboards';
1221
import { exportJson } from '@/utils/exportHelpers';
22+
import { copyTextToClipboard } from '@/utils';
23+
import { useAppStore } from '@/layouts/MainLayout/providers/AppProvider';
1324

1425
const {
1526
toggleEditDashboardModal,
@@ -183,16 +194,40 @@ const DeleteDashboardButton = () => {
183194
};
184195

185196
const ShareDashbboardButton = (props: { dashboard: Dashboard }) => {
197+
const [isSecureHTTPContext] = useAppStore((store) => store.isSecureHTTPContext);
186198
const { dashboard } = props;
187-
const onClick = useCallback(async () => {
199+
const downloadDashboard = useCallback(async () => {
188200
const sanitizedConfig = _.omit(dashboard, ['dashboard_id', 'user_id', 'version']);
189201
const { tiles } = dashboard;
190202
const sanitizedTiles = _.map(tiles, (tile) => {
191203
return _.omit(tile, 'tile_id');
192204
});
193205
return exportJson(JSON.stringify({ ...sanitizedConfig, tiles: sanitizedTiles }, null, 2), dashboard.name);
194206
}, [dashboard]);
195-
return <IconButton renderIcon={renderShareIcon} size={36} onClick={onClick} tooltipLabel="Share Dashboard" />;
207+
208+
const copyUrl = useCallback(() => {
209+
copyTextToClipboard(window.location.href);
210+
}, [window.location.href]);
211+
212+
return (
213+
<Menu width={200} position="bottom" withArrow shadow="md">
214+
<Menu.Target>
215+
<Stack style={{ padding: '0', background: 'transparent' }}>
216+
<IconButton renderIcon={renderShareIcon} size={36} tooltipLabel="Share Dashboard" />
217+
</Stack>
218+
</Menu.Target>
219+
<Menu.Dropdown>
220+
<Menu.Item leftSection={<IconFileArrowRight size={15} stroke={1.02} />} onClick={downloadDashboard}>
221+
Export
222+
</Menu.Item>
223+
{isSecureHTTPContext && (
224+
<Menu.Item leftSection={<IconCopy size={15} stroke={1.02} />} onClick={copyUrl}>
225+
Copy URL
226+
</Menu.Item>
227+
)}
228+
</Menu.Dropdown>
229+
</Menu>
230+
);
196231
};
197232

198233
const ImportTileModal = () => {

src/pages/Dashboards/hooks/useParamsController.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ const paramsStringToParamsObj = (searchParams: URLSearchParams): Record<string,
6969
};
7070

7171
const useParamsController = () => {
72-
const [isStoreSyncd, setStoreSyncd] = useState(false);
72+
const [isStoreSynced, setStoreSynced] = useState(false);
7373
const [activeDashboard, setDashboardsStore] = useDashboardsStore((store) => store.activeDashboard);
7474
const [timeRange, setLogsStore] = useLogsStore((store) => store.timeRange);
7575
const [searchParams, setSearchParams] = useSearchParams();
@@ -78,12 +78,12 @@ const useParamsController = () => {
7878
useEffect(() => {
7979
const storeAsParams = storeToParamsObj({ dashboardId, timeRange });
8080
const presentParams = paramsStringToParamsObj(searchParams);
81-
syncTimeRangeToStore(storeAsParams, presentParams)
82-
setStoreSyncd(true);
81+
syncTimeRangeToStore(storeAsParams, presentParams);
82+
setStoreSynced(true);
8383
}, []);
8484

8585
useEffect(() => {
86-
if (isStoreSyncd) {
86+
if (isStoreSynced) {
8787
const storeAsParams = storeToParamsObj({ dashboardId, timeRange });
8888
const presentParams = paramsStringToParamsObj(searchParams);
8989
if (_.isEqual(storeAsParams, presentParams)) return;
@@ -93,7 +93,7 @@ const useParamsController = () => {
9393
}, [dashboardId, timeRange.startTime.toISOString(), timeRange.endTime.toISOString()]);
9494

9595
useEffect(() => {
96-
if (!isStoreSyncd) return;
96+
if (!isStoreSynced) return;
9797

9898
const storeAsParams = storeToParamsObj({ dashboardId, timeRange });
9999
const presentParams = paramsStringToParamsObj(searchParams);
@@ -103,7 +103,7 @@ const useParamsController = () => {
103103
setDashboardsStore((store) => selectDashboard(store, presentParams.id));
104104
}
105105

106-
syncTimeRangeToStore(storeAsParams, presentParams)
106+
syncTimeRangeToStore(storeAsParams, presentParams);
107107
}, [searchParams]);
108108

109109
const syncTimeRangeToStore = useCallback(
@@ -131,7 +131,7 @@ const useParamsController = () => {
131131
[],
132132
);
133133

134-
return { isStoreSyncd };
134+
return { isStoreSynced };
135135
};
136136

137137
export default useParamsController;

src/pages/Dashboards/index.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,14 @@ const LoadingView = () => {
2323
const Dashboards = () => {
2424
const [dashboards] = useDashboardsStore((store) => store.dashboards);
2525
const [createTileFormOpen] = useDashboardsStore((store) => store.createTileFormOpen);
26-
const { isStoreSyncd } = useParamsController();
26+
const { isStoreSynced } = useParamsController();
2727
const { updateTimeRange } = useSyncTimeRange();
2828
const { fetchDashboards } = useDashboardsQuery({ updateTimeRange });
2929
useEffect(() => {
30-
if (isStoreSyncd) {
30+
if (isStoreSynced) {
3131
fetchDashboards();
3232
}
33-
}, [isStoreSyncd]);
33+
}, [isStoreSynced]);
3434

3535
return (
3636
<Box
@@ -42,7 +42,7 @@ const Dashboards = () => {
4242
width: '100%',
4343
overflow: 'hidden',
4444
}}>
45-
{dashboards === null || !isStoreSyncd ? (
45+
{dashboards === null || !isStoreSynced ? (
4646
<LoadingView />
4747
) : createTileFormOpen ? (
4848
<CreateTileForm />

src/pages/Stream/Views/Explore/Footer.tsx

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
11
import { FC, useCallback } from 'react';
22
import { useLogsStore, logsStoreReducers, LOAD_LIMIT, LOG_QUERY_LIMITS } from '../../providers/LogsProvider';
3-
import { useAppStore } from '@/layouts/MainLayout/providers/AppProvider';
43
import { usePagination } from '@mantine/hooks';
5-
import { downloadDataAsCSV, downloadDataAsJson } from '@/utils/exportHelpers';
6-
import { Box, Center, Group, Loader, Menu, Pagination, px, Stack, Tooltip } from '@mantine/core';
4+
import { Box, Center, Group, Loader, Menu, Pagination, Stack, Tooltip } from '@mantine/core';
75
import _ from 'lodash';
86
import { Text } from '@mantine/core';
97
import { HumanizeNumber } from '@/utils/formatBytes';
10-
import IconButton from '@/components/Button/IconButton';
11-
import { IconDownload, IconSelector } from '@tabler/icons-react';
8+
import { IconSelector } from '@tabler/icons-react';
129
import useMountedState from '@/hooks/useMountedState';
1310
import classes from '../../styles/Footer.module.css';
1411
import { LOGS_FOOTER_HEIGHT } from '@/constants/theme';
1512

16-
const { setPageAndPageData, setCurrentPage, setCurrentOffset, makeExportData } = logsStoreReducers;
13+
const { setPageAndPageData, setCurrentPage, setCurrentOffset } = logsStoreReducers;
1714

1815
const TotalCount = (props: { totalCount: number }) => {
1916
return (
@@ -23,8 +20,6 @@ const TotalCount = (props: { totalCount: number }) => {
2320
);
2421
};
2522

26-
const renderExportIcon = () => <IconDownload size={px('0.8rem')} stroke={1.8} />;
27-
2823
const TotalLogsCount = (props: { hasTableLoaded: boolean; isFetchingCount: boolean; isTableEmpty: boolean }) => {
2924
const [{ totalCount, perPage, pageData }] = useLogsStore((store) => store.tableOpts);
3025
const displayedCount = _.size(pageData);
@@ -94,10 +89,8 @@ const LimitControl: FC = () => {
9489
};
9590

9691
const Footer = (props: { loaded: boolean; hasNoData: boolean; isFetchingCount: boolean }) => {
97-
const [currentStream] = useAppStore((store) => store.currentStream);
9892
const [tableOpts, setLogsStore] = useLogsStore((store) => store.tableOpts);
99-
const [filteredData] = useLogsStore((store) => store.data.filteredData);
100-
const { totalPages, currentOffset, currentPage, perPage, headers, totalCount } = tableOpts;
93+
const { totalPages, currentOffset, currentPage, perPage, totalCount } = tableOpts;
10194

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

126-
const exportHandler = useCallback(
127-
(fileType: string | null) => {
128-
const filename = `${currentStream}-logs`;
129-
if (fileType === 'CSV') {
130-
downloadDataAsCSV(makeExportData(filteredData, headers, 'CSV'), filename);
131-
} else if (fileType === 'JSON') {
132-
downloadDataAsJson(makeExportData(filteredData, headers, 'JSON'), filename);
133-
}
134-
},
135-
[currentStream, filteredData, headers],
136-
);
137-
138119
return (
139-
<Stack className={classes.footerContainer} gap={0} style={{height: LOGS_FOOTER_HEIGHT}}>
120+
<Stack className={classes.footerContainer} gap={0} style={{ height: LOGS_FOOTER_HEIGHT }}>
140121
<Stack w="100%" justify="center" align="flex-start">
141122
<TotalLogsCount
142123
hasTableLoaded={props.loaded}
@@ -190,7 +171,7 @@ const Footer = (props: { loaded: boolean; hasNoData: boolean; isFetchingCount: b
190171
) : null}
191172
</Stack>
192173
<Stack w="100%" align="flex-end" style={{ flexDirection: 'row', justifyContent: 'flex-end' }}>
193-
{props.loaded && (
174+
{/* {props.loaded && (
194175
<Menu position="top">
195176
<Menu.Target>
196177
<div>
@@ -206,7 +187,7 @@ const Footer = (props: { loaded: boolean; hasNoData: boolean; isFetchingCount: b
206187
</Menu.Item>
207188
</Menu.Dropdown>
208189
</Menu>
209-
)}
190+
)} */}
210191
<LimitControl />
211192
</Stack>
212193
</Stack>

src/pages/Stream/components/PrimaryToolbar.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { notifications } from '@mantine/notifications';
1313
import { useParams } from 'react-router-dom';
1414
import _ from 'lodash';
1515
import StreamingButton from '@/components/Header/StreamingButton';
16+
import ShareButton from '@/components/Header/ShareButton';
1617
import { useLogsStore, logsStoreReducers } from '../providers/LogsProvider';
1718
import { filterStoreReducers, useFilterStore } from '../providers/FilterProvider';
1819
import classes from './styles/PrimaryToolbar.module.css';
@@ -89,7 +90,7 @@ const ViewToggle = () => {
8990

9091
const PrimaryToolbar = () => {
9192
const [maximized] = useAppStore((store) => store.maximized);
92-
const [hasDeleteStreamAccess] = useAppStore(store => store.userAccessMap.hasDeleteStreamAccess)
93+
const [hasDeleteStreamAccess] = useAppStore((store) => store.userAccessMap.hasDeleteStreamAccess);
9394
const { view } = useParams();
9495

9596
useEffect(() => {
@@ -119,6 +120,7 @@ const PrimaryToolbar = () => {
119120
<RefreshInterval />
120121
<RefreshNow />
121122
<ViewToggle />
123+
<ShareButton />
122124
<MaximizeButton />
123125
</Stack>
124126
) : view === 'live-tail' ? (

0 commit comments

Comments
 (0)