mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 04:37:11 +01:00
Added download functionality.
This commit is contained in:
parent
0debe89d05
commit
7670ec4136
17 changed files with 216 additions and 5 deletions
|
|
@ -107,6 +107,7 @@
|
||||||
"next": "15.3.3",
|
"next": "15.3.3",
|
||||||
"node-fetch": "^3.2.8",
|
"node-fetch": "^3.2.8",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
|
"papaparse": "^5.5.3",
|
||||||
"prisma": "6.7.0",
|
"prisma": "6.7.0",
|
||||||
"pure-rand": "^6.1.0",
|
"pure-rand": "^6.1.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
|
|
|
||||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
|
|
@ -131,6 +131,9 @@ importers:
|
||||||
npm-run-all:
|
npm-run-all:
|
||||||
specifier: ^4.1.5
|
specifier: ^4.1.5
|
||||||
version: 4.1.5
|
version: 4.1.5
|
||||||
|
papaparse:
|
||||||
|
specifier: ^5.5.3
|
||||||
|
version: 5.5.3
|
||||||
prisma:
|
prisma:
|
||||||
specifier: 6.7.0
|
specifier: 6.7.0
|
||||||
version: 6.7.0(typescript@5.8.3)
|
version: 6.7.0(typescript@5.8.3)
|
||||||
|
|
@ -5107,6 +5110,9 @@ packages:
|
||||||
pako@1.0.11:
|
pako@1.0.11:
|
||||||
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
|
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
|
||||||
|
|
||||||
|
papaparse@5.5.3:
|
||||||
|
resolution: {integrity: sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==}
|
||||||
|
|
||||||
parent-module@1.0.1:
|
parent-module@1.0.1:
|
||||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
@ -12275,6 +12281,8 @@ snapshots:
|
||||||
|
|
||||||
pako@1.0.11: {}
|
pako@1.0.11: {}
|
||||||
|
|
||||||
|
papaparse@5.5.3: {}
|
||||||
|
|
||||||
parent-module@1.0.1:
|
parent-module@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
callsites: 3.1.0
|
callsites: 3.1.0
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { ReportContext } from './Report';
|
import { ReportContext } from './Report';
|
||||||
import styles from './ReportBody.module.css';
|
import styles from './ReportBody.module.css';
|
||||||
|
import { DownloadButton } from '@/components/input/DownloadButton';
|
||||||
|
|
||||||
export function ReportBody({ children }) {
|
export function ReportBody({ children }) {
|
||||||
const { report } = useContext(ReportContext);
|
const { report } = useContext(ReportContext);
|
||||||
|
|
@ -9,7 +10,12 @@ export function ReportBody({ children }) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className={styles.body}>{children}</div>;
|
return (
|
||||||
|
<div className={styles.body}>
|
||||||
|
<DownloadButton filename={report.name} data={report.data} />
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ReportBody;
|
export default ReportBody;
|
||||||
|
|
|
||||||
|
|
@ -31,5 +31,9 @@ export default function ReportPage({ reportId }: { reportId: string }) {
|
||||||
|
|
||||||
const ReportComponent = reports[report.type];
|
const ReportComponent = reports[report.type];
|
||||||
|
|
||||||
|
if (!ReportComponent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return <ReportComponent reportId={reportId} />;
|
return <ReportComponent reportId={reportId} />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Dropdown, Item } from 'react-basics';
|
import { Dropdown, Item } from 'react-basics';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useDateRange, useMessages, useSticky } from '@/components/hooks';
|
import { useDateRange, useMessages, useNavigation, useSticky } from '@/components/hooks';
|
||||||
import WebsiteDateFilter from '@/components/input/WebsiteDateFilter';
|
import WebsiteDateFilter from '@/components/input/WebsiteDateFilter';
|
||||||
import MetricCard from '@/components/metrics/MetricCard';
|
import MetricCard from '@/components/metrics/MetricCard';
|
||||||
import MetricsBar from '@/components/metrics/MetricsBar';
|
import MetricsBar from '@/components/metrics/MetricsBar';
|
||||||
|
|
@ -8,6 +8,7 @@ import { formatShortTime, formatLongNumber } from '@/lib/format';
|
||||||
import useWebsiteStats from '@/components/hooks/queries/useWebsiteStats';
|
import useWebsiteStats from '@/components/hooks/queries/useWebsiteStats';
|
||||||
import useStore, { setWebsiteDateCompare } from '@/store/websites';
|
import useStore, { setWebsiteDateCompare } from '@/store/websites';
|
||||||
import WebsiteFilterButton from './WebsiteFilterButton';
|
import WebsiteFilterButton from './WebsiteFilterButton';
|
||||||
|
import { ExportButton } from '@/components/input/ExportButton';
|
||||||
import styles from './WebsiteMetricsBar.module.css';
|
import styles from './WebsiteMetricsBar.module.css';
|
||||||
|
|
||||||
export function WebsiteMetricsBar({
|
export function WebsiteMetricsBar({
|
||||||
|
|
@ -31,6 +32,9 @@ export function WebsiteMetricsBar({
|
||||||
websiteId,
|
websiteId,
|
||||||
compareMode && dateCompare,
|
compareMode && dateCompare,
|
||||||
);
|
);
|
||||||
|
const {
|
||||||
|
query: { view },
|
||||||
|
} = useNavigation();
|
||||||
const isAllTime = dateRange.value === 'all';
|
const isAllTime = dateRange.value === 'all';
|
||||||
|
|
||||||
const { pageviews, visitors, visits, bounces, totaltime } = data || {};
|
const { pageviews, visitors, visits, bounces, totaltime } = data || {};
|
||||||
|
|
@ -109,7 +113,10 @@ export function WebsiteMetricsBar({
|
||||||
</MetricsBar>
|
</MetricsBar>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.actions}>
|
<div className={styles.actions}>
|
||||||
{showFilter && <WebsiteFilterButton websiteId={websiteId} />}
|
<div>
|
||||||
|
{showFilter && <WebsiteFilterButton websiteId={websiteId} />}
|
||||||
|
{!view && <ExportButton websiteId={websiteId} />}
|
||||||
|
</div>
|
||||||
<WebsiteDateFilter websiteId={websiteId} showAllTime={!compareMode} />
|
<WebsiteDateFilter websiteId={websiteId} showAllTime={!compareMode} />
|
||||||
{compareMode && (
|
{compareMode && (
|
||||||
<div className={styles.vs}>
|
<div className={styles.vs}>
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ export default function WebsiteTableView({ websiteId }: { websiteId: string }) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const tableProps = {
|
const tableProps = {
|
||||||
websiteId,
|
websiteId,
|
||||||
|
allowDownload: false,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
};
|
};
|
||||||
const isSharePage = pathname.includes('/share/');
|
const isSharePage = pathname.includes('/share/');
|
||||||
|
|
|
||||||
75
src/app/api/websites/[websiteId]/export/route.ts
Normal file
75
src/app/api/websites/[websiteId]/export/route.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
import JSZip from 'jszip';
|
||||||
|
import Papa from 'papaparse';
|
||||||
|
import { getRequestFilters, parseRequest } from '@/lib/request';
|
||||||
|
import { unauthorized, json } from '@/lib/response';
|
||||||
|
import { canViewWebsite } from '@/lib/auth';
|
||||||
|
import { pagingParams } from '@/lib/schema';
|
||||||
|
import { getEventMetrics, getWebsiteEvents } from '@/queries';
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
request: Request,
|
||||||
|
{ params }: { params: Promise<{ websiteId: string }> },
|
||||||
|
) {
|
||||||
|
const schema = z.object({
|
||||||
|
startAt: z.coerce.number().int(),
|
||||||
|
endAt: z.coerce.number().int(),
|
||||||
|
...pagingParams,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { auth, query, error } = await parseRequest(request, schema);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return error();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { websiteId } = await params;
|
||||||
|
const { startAt, endAt } = query;
|
||||||
|
|
||||||
|
if (!(await canViewWebsite(auth, websiteId))) {
|
||||||
|
return unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
const startDate = new Date(+startAt);
|
||||||
|
const endDate = new Date(+endAt);
|
||||||
|
const limit = 10;
|
||||||
|
const offset = 0;
|
||||||
|
|
||||||
|
const filters = {
|
||||||
|
...(await getRequestFilters(query)),
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
};
|
||||||
|
|
||||||
|
const [events, pages, referrers, browsers, os, devices, countries] = await Promise.all([
|
||||||
|
getWebsiteEvents(websiteId, { startDate, endDate }, query),
|
||||||
|
getEventMetrics(websiteId, 'url', filters, limit, offset),
|
||||||
|
getEventMetrics(websiteId, 'referrer', filters, limit, offset),
|
||||||
|
getEventMetrics(websiteId, 'browser', filters, limit, offset),
|
||||||
|
getEventMetrics(websiteId, 'os', filters, limit, offset),
|
||||||
|
getEventMetrics(websiteId, 'device', filters, limit, offset),
|
||||||
|
getEventMetrics(websiteId, 'country', filters, limit, offset),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const zip = new JSZip();
|
||||||
|
|
||||||
|
const parse = (data: any) => {
|
||||||
|
return Papa.unparse(data, {
|
||||||
|
header: true,
|
||||||
|
skipEmptyLines: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
zip.file('events.csv', parse(events?.data));
|
||||||
|
zip.file('pages.csv', parse(pages));
|
||||||
|
zip.file('referrers.csv', parse(referrers));
|
||||||
|
zip.file('browsers.csv', parse(browsers));
|
||||||
|
zip.file('os.csv', parse(os));
|
||||||
|
zip.file('devices.csv', parse(devices));
|
||||||
|
zip.file('countries.csv', parse(countries));
|
||||||
|
|
||||||
|
const content = await zip.generateAsync({ type: 'nodebuffer' });
|
||||||
|
const base64 = content.toString('base64');
|
||||||
|
|
||||||
|
return json({ zip: base64 });
|
||||||
|
}
|
||||||
1
src/assets/download.svg
Normal file
1
src/assets/download.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg id="Layer_1" enable-background="new 0 0 100 100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path d="m97.4999924 82.6562576.0000076-11.298912c0-1.957756-1.5870743-3.544838-3.544838-3.544838h-4.785324c-1.9577637 0-3.544838 1.5870743-3.544838 3.5448303l-.0000076 11.2989121c0 1.639595-1.329155 2.96875-2.96875 2.96875l-65.3124924-.0000229c-1.639596 0-2.96875-1.329155-2.968749-2.96875l.0000038-11.298912c0-1.957756-1.5870762-3.544838-3.544836-3.544838h-4.7853256c-1.9577594 0-3.5448372 1.5870743-3.544838 3.544838l-.0000036 11.298912c-.0000026 8.1979752 6.6457672 14.84375 14.8437443 14.84375l65.3124965.0000229c8.1979751 0 14.84375-6.6457672 14.84375-14.8437424z"/><path d="m29.6809349 44.1050034-3.3884087 3.3884048c-1.3843441 1.384346-1.384346 3.6288109-.0000019 5.0131569l19.5066929 19.5067101c2.3174515 2.3200302 6.0768623 2.3221207 8.3968925.0046768.0015564-.0015564.0031128-.0031204.0046692-.0046768l19.5067177-19.5066948c1.384346-1.3843422 1.384346-3.6288109 0-5.0131569l-3.3884125-3.3884048c-1.3843384-1.384346-3.6288071-1.384346-5.0131531-.0000038l-9.3684235 9.3684196.0000153-47.4285965c0-1.9577589-1.5870781-3.544837-3.5448341-3.5448377l-4.7853279-.0000014c-1.9577599-.0000007-3.544838 1.5870759-3.544838 3.5448353l-.0000153 47.4285965-9.3684158-9.3684235c-1.3843459-1.384346-3.6288127-1.384346-5.0131568-.0000038z"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
1
src/assets/export.svg
Normal file
1
src/assets/export.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg id="Layer_1" enable-background="new 0 0 24 24" height="512" viewBox="0 0 24 24" width="512" xmlns="http://www.w3.org/2000/svg"><switch><g><path d="m8.7 7.7 2.3-2.3v9.6c0 .6.4 1 1 1s1-.4 1-1v-9.6l2.3 2.3c.4.4 1 .4 1.4 0 .4-.4.4-1 0-1.4l-4-4c-.1-.1-.2-.2-.3-.2-.2-.1-.5-.1-.8 0-.1 0-.2.1-.3.2l-4 4c-.4.4-.4 1 0 1.4s1 .4 1.4 0zm12.3 6.3c-.6 0-1 .4-1 1v4c0 .6-.4 1-1 1h-14c-.6 0-1-.4-1-1v-4c0-.6-.4-1-1-1s-1 .4-1 1v4c0 1.7 1.3 3 3 3h14c1.7 0 3-1.3 3-3v-4c0-.6-.4-1-1-1z"/></g></switch></svg>
|
||||||
|
After Width: | Height: | Size: 493 B |
|
|
@ -29,7 +29,7 @@ export function useReport(
|
||||||
data.parameters = {
|
data.parameters = {
|
||||||
...defaultParameters?.parameters,
|
...defaultParameters?.parameters,
|
||||||
...data.parameters,
|
...data.parameters,
|
||||||
dateRange: parseDateRange(dateRange.value),
|
dateRange: dateRange ? parseDateRange(dateRange?.value) : {},
|
||||||
};
|
};
|
||||||
|
|
||||||
setReport(data);
|
setReport(data);
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ import Change from '@/assets/change.svg';
|
||||||
import Clock from '@/assets/clock.svg';
|
import Clock from '@/assets/clock.svg';
|
||||||
import Compare from '@/assets/compare.svg';
|
import Compare from '@/assets/compare.svg';
|
||||||
import Dashboard from '@/assets/dashboard.svg';
|
import Dashboard from '@/assets/dashboard.svg';
|
||||||
|
import Download from '@/assets/download.svg';
|
||||||
|
import Export from '@/assets/export.svg';
|
||||||
import Eye from '@/assets/eye.svg';
|
import Eye from '@/assets/eye.svg';
|
||||||
import Gear from '@/assets/gear.svg';
|
import Gear from '@/assets/gear.svg';
|
||||||
import Globe from '@/assets/globe.svg';
|
import Globe from '@/assets/globe.svg';
|
||||||
|
|
@ -37,6 +39,8 @@ const icons = {
|
||||||
Clock,
|
Clock,
|
||||||
Compare,
|
Compare,
|
||||||
Dashboard,
|
Dashboard,
|
||||||
|
Download,
|
||||||
|
Export,
|
||||||
Eye,
|
Eye,
|
||||||
Gear,
|
Gear,
|
||||||
Globe,
|
Globe,
|
||||||
|
|
|
||||||
41
src/components/input/DownloadButton.tsx
Normal file
41
src/components/input/DownloadButton.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import Papa from 'papaparse';
|
||||||
|
import { Button, Icon, TooltipPopup } from 'react-basics';
|
||||||
|
import Icons from '@/components/icons';
|
||||||
|
import { useMessages } from '@/components/hooks';
|
||||||
|
|
||||||
|
export function DownloadButton({
|
||||||
|
filename = 'data',
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
filename?: string;
|
||||||
|
data?: any;
|
||||||
|
onClick?: () => void;
|
||||||
|
}) {
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
||||||
|
const handleClick = async () => {
|
||||||
|
downloadCsv(`${filename}.csv`, Papa.unparse(data));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipPopup label={formatMessage(labels.download)} position="top">
|
||||||
|
<Button variant="quiet" onClick={handleClick} disabled={!data}>
|
||||||
|
<Icon>
|
||||||
|
<Icons.Download />
|
||||||
|
</Icon>
|
||||||
|
</Button>
|
||||||
|
</TooltipPopup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadCsv(filename: string, data: any) {
|
||||||
|
const blob = new Blob([data], { type: 'text/csv' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
a.click();
|
||||||
|
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
47
src/components/input/ExportButton.tsx
Normal file
47
src/components/input/ExportButton.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Icon, TooltipPopup, LoadingButton } from 'react-basics';
|
||||||
|
import Icons from '@/components/icons';
|
||||||
|
import { useMessages, useApi } from '@/components/hooks';
|
||||||
|
import { useFilterParams } from '@/components/hooks/useFilterParams';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
|
export function ExportButton({ websiteId }: { websiteId: string }) {
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const params = useFilterParams(websiteId);
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const { get } = useApi();
|
||||||
|
|
||||||
|
const handleClick = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const { zip } = await get(`/websites/${websiteId}/export`, { ...params, ...searchParams });
|
||||||
|
|
||||||
|
const binary = atob(zip);
|
||||||
|
const bytes = new Uint8Array(binary.length);
|
||||||
|
for (let i = 0; i < binary.length; i++) {
|
||||||
|
bytes[i] = binary.charCodeAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob([bytes], { type: 'application/zip' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'download.zip';
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipPopup label={formatMessage(labels.download)} position="top">
|
||||||
|
<LoadingButton variant="quiet" isLoading={isLoading} onClick={handleClick}>
|
||||||
|
<Icon>
|
||||||
|
<Icons.Download />
|
||||||
|
</Icon>
|
||||||
|
</LoadingButton>
|
||||||
|
</TooltipPopup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -316,6 +316,7 @@ export const labels = defineMessages({
|
||||||
other: { id: 'label.other', defaultMessage: 'Other' },
|
other: { id: 'label.other', defaultMessage: 'Other' },
|
||||||
chart: { id: 'label.chart', defaultMessage: 'Chart' },
|
chart: { id: 'label.chart', defaultMessage: 'Chart' },
|
||||||
table: { id: 'label.table', defaultMessage: 'Table' },
|
table: { id: 'label.table', defaultMessage: 'Table' },
|
||||||
|
download: { id: 'label.download', defaultMessage: 'Download' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const messages = defineMessages({
|
export const messages = defineMessages({
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ export function EventsTable({ onLabelClick, ...props }: EventsTableProps) {
|
||||||
metric={formatMessage(labels.actions)}
|
metric={formatMessage(labels.actions)}
|
||||||
onDataLoad={handleDataLoad}
|
onDataLoad={handleDataLoad}
|
||||||
renderLabel={renderLabel}
|
renderLabel={renderLabel}
|
||||||
|
allowDownload={false}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,13 @@
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import {
|
||||||
import Icons from '@/components/icons';
|
import Icons from '@/components/icons';
|
||||||
import ListTable, { ListTableProps } from './ListTable';
|
import ListTable, { ListTableProps } from './ListTable';
|
||||||
import styles from './MetricsTable.module.css';
|
import styles from './MetricsTable.module.css';
|
||||||
|
import { DownloadButton } from '@/components/input/DownloadButton';
|
||||||
|
|
||||||
export interface MetricsTableProps extends ListTableProps {
|
export interface MetricsTableProps extends ListTableProps {
|
||||||
websiteId: string;
|
websiteId: string;
|
||||||
|
|
@ -29,6 +30,7 @@ export interface MetricsTableProps extends ListTableProps {
|
||||||
searchFormattedValues?: boolean;
|
searchFormattedValues?: boolean;
|
||||||
showMore?: boolean;
|
showMore?: boolean;
|
||||||
params?: { [key: string]: any };
|
params?: { [key: string]: any };
|
||||||
|
allowDownload?: boolean;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -44,6 +46,7 @@ export function MetricsTable({
|
||||||
searchFormattedValues = false,
|
searchFormattedValues = false,
|
||||||
showMore = true,
|
showMore = true,
|
||||||
params,
|
params,
|
||||||
|
allowDownload = true,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: MetricsTableProps) {
|
}: MetricsTableProps) {
|
||||||
|
|
@ -104,7 +107,10 @@ export function MetricsTable({
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{children}
|
<div className={styles.buttons}>
|
||||||
|
{children}
|
||||||
|
{allowDownload && <DownloadButton filename={type} data={filteredData} />}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{data && !error && (
|
{data && !error && (
|
||||||
<ListTable {...(props as ListTableProps)} data={filteredData} className={className} />
|
<ListTable {...(props as ListTableProps)} data={filteredData} className={className} />
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue