mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2026-04-20 05:26:20 +00:00
Implemented short URLs exporting
This commit is contained in:
33
src/common/services/ReportExporter.ts
Normal file
33
src/common/services/ReportExporter.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { CsvJson } from 'csvjson';
|
||||
import { NormalizedVisit } from '../../visits/types';
|
||||
import { ExportableShortUrl } from '../../short-urls/data';
|
||||
import { saveCsv } from '../../utils/helpers/files';
|
||||
|
||||
export class ReportExporter {
|
||||
public constructor(
|
||||
private readonly window: Window,
|
||||
private readonly csvjson: CsvJson,
|
||||
) {}
|
||||
|
||||
public readonly exportVisits = (filename: string, visits: NormalizedVisit[]) => {
|
||||
if (!visits.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.exportCsv(filename, visits);
|
||||
};
|
||||
|
||||
public readonly exportShortUrls = (shortUrls: ExportableShortUrl[]) => {
|
||||
if (!shortUrls.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.exportCsv('short_urls.csv', shortUrls);
|
||||
};
|
||||
|
||||
private readonly exportCsv = (filename: string, rows: object[]) => {
|
||||
const csv = this.csvjson.toCSV(rows, { headers: 'key', wrap: true });
|
||||
|
||||
saveCsv(this.window, csv, filename);
|
||||
};
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { ConnectDecorator } from '../../container/types';
|
||||
import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServer';
|
||||
import { sidebarNotPresent, sidebarPresent } from '../reducers/sidebar';
|
||||
import { ImageDownloader } from './ImageDownloader';
|
||||
import { ReportExporter } from './ReportExporter';
|
||||
|
||||
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||
// Services
|
||||
@@ -19,6 +20,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||
bottle.constant('axios', axios);
|
||||
|
||||
bottle.service('ImageDownloader', ImageDownloader, 'axios', 'window');
|
||||
bottle.service('ReportExporter', ReportExporter, 'window', 'csvjson');
|
||||
|
||||
// Components
|
||||
bottle.serviceFactory('ScrollToTop', ScrollToTop);
|
||||
|
||||
@@ -14,11 +14,11 @@ import { DateRange } from '../utils/dates/types';
|
||||
import { supportsAllTagsFiltering } from '../utils/helpers/features';
|
||||
import { SelectedServer } from '../servers/data';
|
||||
import { TooltipToggleSwitch } from '../utils/TooltipToggleSwitch';
|
||||
import { ExportBtn } from '../utils/ExportBtn';
|
||||
import { OrderDir } from '../utils/helpers/ordering';
|
||||
import { OrderingDropdown } from '../utils/OrderingDropdown';
|
||||
import { useShortUrlsQuery } from './helpers/hooks';
|
||||
import { SHORT_URLS_ORDERABLE_FIELDS, ShortUrlsOrder, ShortUrlsOrderableFields } from './data';
|
||||
import { ExportShortUrlsBtnProps } from './helpers/ExportShortUrlsBtn';
|
||||
import './ShortUrlsFilteringBar.scss';
|
||||
|
||||
export interface ShortUrlsFilteringProps {
|
||||
@@ -26,13 +26,15 @@ export interface ShortUrlsFilteringProps {
|
||||
order: ShortUrlsOrder;
|
||||
handleOrderBy: (orderField?: ShortUrlsOrderableFields, orderDir?: OrderDir) => void;
|
||||
className?: string;
|
||||
shortUrlsAmount?: number;
|
||||
}
|
||||
|
||||
const dateOrNull = (date?: string) => date ? parseISO(date) : null;
|
||||
|
||||
const ShortUrlsFilteringBar = (colorGenerator: ColorGenerator): FC<ShortUrlsFilteringProps> => (
|
||||
{ selectedServer, className, order, handleOrderBy },
|
||||
) => {
|
||||
const ShortUrlsFilteringBar = (
|
||||
colorGenerator: ColorGenerator,
|
||||
ExportShortUrlsBtn: FC<ExportShortUrlsBtnProps>,
|
||||
): FC<ShortUrlsFilteringProps> => ({ selectedServer, className, shortUrlsAmount, order, handleOrderBy }) => {
|
||||
const [{ search, tags, startDate, endDate, tagsMode = 'any' }, toFirstPage ] = useShortUrlsQuery();
|
||||
const setDates = pipe(
|
||||
({ startDate, endDate }: DateRange) => ({
|
||||
@@ -61,7 +63,7 @@ const ShortUrlsFilteringBar = (colorGenerator: ColorGenerator): FC<ShortUrlsFilt
|
||||
|
||||
<Row className="flex-column-reverse flex-lg-row">
|
||||
<div className="col-lg-4 col-xl-6 mt-3">
|
||||
<ExportBtn className="btn-md-block" amount={4} onClick={() => {}} />
|
||||
<ExportShortUrlsBtn amount={shortUrlsAmount} />
|
||||
</div>
|
||||
<div className="col-12 d-block d-lg-none mt-3">
|
||||
<OrderingDropdown items={SHORT_URLS_ORDERABLE_FIELDS} order={order} onChange={handleOrderBy} />
|
||||
|
||||
@@ -65,6 +65,7 @@ const ShortUrlsList = (
|
||||
<>
|
||||
<ShortUrlsFilteringBar
|
||||
selectedServer={selectedServer}
|
||||
shortUrlsAmount={shortUrlsList.shortUrls?.pagination.totalItems}
|
||||
order={actualOrderBy}
|
||||
handleOrderBy={handleOrderBy}
|
||||
className="mb-3"
|
||||
|
||||
@@ -63,3 +63,12 @@ export const SHORT_URLS_ORDERABLE_FIELDS = {
|
||||
export type ShortUrlsOrderableFields = keyof typeof SHORT_URLS_ORDERABLE_FIELDS;
|
||||
|
||||
export type ShortUrlsOrder = Order<ShortUrlsOrderableFields>;
|
||||
|
||||
export interface ExportableShortUrl {
|
||||
createdAt: string;
|
||||
title: string;
|
||||
shortUrl: string;
|
||||
longUrl: string;
|
||||
tags: string;
|
||||
visits: number;
|
||||
}
|
||||
|
||||
66
src/short-urls/helpers/ExportShortUrlsBtn.tsx
Normal file
66
src/short-urls/helpers/ExportShortUrlsBtn.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { FC } from 'react';
|
||||
import { ExportBtn } from '../../utils/ExportBtn';
|
||||
import { useToggle } from '../../utils/helpers/hooks';
|
||||
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
||||
import { isServerWithId, SelectedServer } from '../../servers/data';
|
||||
import { ShortUrl } from '../data';
|
||||
import { ReportExporter } from '../../common/services/ReportExporter';
|
||||
import { useShortUrlsQuery } from './hooks';
|
||||
|
||||
export interface ExportShortUrlsBtnProps {
|
||||
amount?: number;
|
||||
}
|
||||
|
||||
interface ExportShortUrlsBtnConnectProps extends ExportShortUrlsBtnProps {
|
||||
selectedServer: SelectedServer;
|
||||
}
|
||||
|
||||
const itemsPerPage = 10;
|
||||
|
||||
export const ExportShortUrlsBtn = (
|
||||
buildShlinkApiClient: ShlinkApiClientBuilder,
|
||||
{ exportShortUrls }: ReportExporter,
|
||||
): FC<ExportShortUrlsBtnConnectProps> => ({ amount = 0, selectedServer }) => {
|
||||
const [{ tags, search, startDate, endDate, orderBy, tagsMode }] = useShortUrlsQuery();
|
||||
const [ loading,, startLoading, stopLoading ] = useToggle();
|
||||
const exportAllUrls = () => {
|
||||
if (!isServerWithId(selectedServer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const totalPages = amount / itemsPerPage;
|
||||
const { listShortUrls } = buildShlinkApiClient(selectedServer);
|
||||
const loadAllUrls = async (page = 1): Promise<ShortUrl[]> => {
|
||||
const { data } = await listShortUrls(
|
||||
{ page: `${page}`, tags, searchTerm: search, startDate, endDate, orderBy, tagsMode, itemsPerPage },
|
||||
);
|
||||
|
||||
if (page >= totalPages) {
|
||||
return data;
|
||||
}
|
||||
|
||||
// TODO Support paralelization
|
||||
return data.concat(await loadAllUrls(page + 1));
|
||||
};
|
||||
|
||||
startLoading();
|
||||
loadAllUrls()
|
||||
.then((shortUrls) => {
|
||||
exportShortUrls(shortUrls.map((shortUrl) => ({
|
||||
createdAt: shortUrl.dateCreated,
|
||||
shortUrl: shortUrl.shortUrl,
|
||||
longUrl: shortUrl.longUrl,
|
||||
title: shortUrl.title ?? '',
|
||||
tags: shortUrl.tags.join(','),
|
||||
visits: shortUrl.visitsCount,
|
||||
})));
|
||||
stopLoading();
|
||||
})
|
||||
.catch((e) => {
|
||||
// TODO Handle error properly
|
||||
console.error('An error occurred while exporting short URLs', e); // eslint-disable-line no-console
|
||||
});
|
||||
};
|
||||
|
||||
return <ExportBtn loading={loading} className="btn-md-block" amount={amount} onClick={exportAllUrls} />;
|
||||
};
|
||||
@@ -16,6 +16,7 @@ import QrCodeModal from '../helpers/QrCodeModal';
|
||||
import { ShortUrlForm } from '../ShortUrlForm';
|
||||
import { EditShortUrl } from '../EditShortUrl';
|
||||
import { getShortUrlDetail } from '../reducers/shortUrlDetail';
|
||||
import { ExportShortUrlsBtn } from '../helpers/ExportShortUrlsBtn';
|
||||
|
||||
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||
// Components
|
||||
@@ -49,7 +50,10 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||
bottle.serviceFactory('QrCodeModal', QrCodeModal, 'ImageDownloader', 'ForServerVersion');
|
||||
bottle.decorator('QrCodeModal', connect([ 'selectedServer' ]));
|
||||
|
||||
bottle.serviceFactory('ShortUrlsFilteringBar', ShortUrlsFilteringBar, 'ColorGenerator');
|
||||
bottle.serviceFactory('ShortUrlsFilteringBar', ShortUrlsFilteringBar, 'ColorGenerator', 'ExportShortUrlsBtn');
|
||||
|
||||
bottle.serviceFactory('ExportShortUrlsBtn', ExportShortUrlsBtn, 'buildShlinkApiClient', 'ReportExporter');
|
||||
bottle.decorator('ExportShortUrlsBtn', connect([ 'selectedServer' ]));
|
||||
|
||||
// Actions
|
||||
bottle.serviceFactory('listShortUrls', listShortUrls, 'buildShlinkApiClient');
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { FC } from 'react';
|
||||
import { Button } from 'reactstrap';
|
||||
import { Button, ButtonProps } from 'reactstrap';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faFileDownload } from '@fortawesome/free-solid-svg-icons';
|
||||
import { prettify } from './helpers/numbers';
|
||||
|
||||
interface ExportBtnProps {
|
||||
onClick: () => void;
|
||||
interface ExportBtnProps extends Omit<ButtonProps, 'outline' | 'color' | 'disabled'> {
|
||||
amount?: number;
|
||||
className?: string;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export const ExportBtn: FC<ExportBtnProps> = ({ onClick, className, amount = 0 }) => (
|
||||
<Button outline color="primary" className={className} onClick={onClick}>
|
||||
<FontAwesomeIcon icon={faFileDownload} /> Export ({prettify(amount)})
|
||||
export const ExportBtn: FC<ExportBtnProps> = ({ amount = 0, loading = false, ...rest }) => (
|
||||
<Button {...rest} outline color="primary" disabled={loading}>
|
||||
<FontAwesomeIcon icon={faFileDownload} /> {loading ? 'Exporting...' : <>Export ({prettify(amount)})</>}
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -2,9 +2,9 @@ import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
||||
import { ShlinkVisitsParams } from '../api/types';
|
||||
import { Topics } from '../mercure/helpers/Topics';
|
||||
import { useGoBack } from '../utils/helpers/hooks';
|
||||
import { ReportExporter } from '../common/services/ReportExporter';
|
||||
import VisitsStats from './VisitsStats';
|
||||
import { NormalizedVisit, VisitsInfo, VisitsParams } from './types';
|
||||
import { VisitsExporter } from './services/VisitsExporter';
|
||||
import { CommonVisitsProps } from './types/CommonVisitsProps';
|
||||
import { toApiParams } from './types/helpers';
|
||||
import { NonOrphanVisitsHeader } from './NonOrphanVisitsHeader';
|
||||
@@ -15,7 +15,7 @@ export interface NonOrphanVisitsProps extends CommonVisitsProps {
|
||||
cancelGetNonOrphanVisits: () => void;
|
||||
}
|
||||
|
||||
export const NonOrphanVisits = ({ exportVisits }: VisitsExporter) => boundToMercureHub(({
|
||||
export const NonOrphanVisits = ({ exportVisits }: ReportExporter) => boundToMercureHub(({
|
||||
getNonOrphanVisits,
|
||||
nonOrphanVisits,
|
||||
cancelGetNonOrphanVisits,
|
||||
|
||||
@@ -2,10 +2,10 @@ import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
||||
import { ShlinkVisitsParams } from '../api/types';
|
||||
import { Topics } from '../mercure/helpers/Topics';
|
||||
import { useGoBack } from '../utils/helpers/hooks';
|
||||
import { ReportExporter } from '../common/services/ReportExporter';
|
||||
import VisitsStats from './VisitsStats';
|
||||
import { OrphanVisitsHeader } from './OrphanVisitsHeader';
|
||||
import { NormalizedVisit, OrphanVisitType, VisitsInfo, VisitsParams } from './types';
|
||||
import { VisitsExporter } from './services/VisitsExporter';
|
||||
import { CommonVisitsProps } from './types/CommonVisitsProps';
|
||||
import { toApiParams } from './types/helpers';
|
||||
|
||||
@@ -19,7 +19,7 @@ export interface OrphanVisitsProps extends CommonVisitsProps {
|
||||
cancelGetOrphanVisits: () => void;
|
||||
}
|
||||
|
||||
export const OrphanVisits = ({ exportVisits }: VisitsExporter) => boundToMercureHub(({
|
||||
export const OrphanVisits = ({ exportVisits }: ReportExporter) => boundToMercureHub(({
|
||||
getOrphanVisits,
|
||||
orphanVisits,
|
||||
cancelGetOrphanVisits,
|
||||
|
||||
@@ -6,10 +6,10 @@ import { parseQuery } from '../utils/helpers/query';
|
||||
import { Topics } from '../mercure/helpers/Topics';
|
||||
import { ShortUrlDetail } from '../short-urls/reducers/shortUrlDetail';
|
||||
import { useGoBack } from '../utils/helpers/hooks';
|
||||
import { ReportExporter } from '../common/services/ReportExporter';
|
||||
import { ShortUrlVisits as ShortUrlVisitsState } from './reducers/shortUrlVisits';
|
||||
import ShortUrlVisitsHeader from './ShortUrlVisitsHeader';
|
||||
import VisitsStats from './VisitsStats';
|
||||
import { VisitsExporter } from './services/VisitsExporter';
|
||||
import { NormalizedVisit, VisitsParams } from './types';
|
||||
import { CommonVisitsProps } from './types/CommonVisitsProps';
|
||||
import { toApiParams } from './types/helpers';
|
||||
@@ -22,7 +22,7 @@ export interface ShortUrlVisitsProps extends CommonVisitsProps {
|
||||
cancelGetShortUrlVisits: () => void;
|
||||
}
|
||||
|
||||
const ShortUrlVisits = ({ exportVisits }: VisitsExporter) => boundToMercureHub(({
|
||||
const ShortUrlVisits = ({ exportVisits }: ReportExporter) => boundToMercureHub(({
|
||||
shortUrlVisits,
|
||||
shortUrlDetail,
|
||||
getShortUrlVisits,
|
||||
|
||||
@@ -4,10 +4,10 @@ import ColorGenerator from '../utils/services/ColorGenerator';
|
||||
import { ShlinkVisitsParams } from '../api/types';
|
||||
import { Topics } from '../mercure/helpers/Topics';
|
||||
import { useGoBack } from '../utils/helpers/hooks';
|
||||
import { ReportExporter } from '../common/services/ReportExporter';
|
||||
import { TagVisits as TagVisitsState } from './reducers/tagVisits';
|
||||
import TagVisitsHeader from './TagVisitsHeader';
|
||||
import VisitsStats from './VisitsStats';
|
||||
import { VisitsExporter } from './services/VisitsExporter';
|
||||
import { NormalizedVisit } from './types';
|
||||
import { CommonVisitsProps } from './types/CommonVisitsProps';
|
||||
import { toApiParams } from './types/helpers';
|
||||
@@ -18,7 +18,7 @@ export interface TagVisitsProps extends CommonVisitsProps {
|
||||
cancelGetTagVisits: () => void;
|
||||
}
|
||||
|
||||
const TagVisits = (colorGenerator: ColorGenerator, { exportVisits }: VisitsExporter) => boundToMercureHub(({
|
||||
const TagVisits = (colorGenerator: ColorGenerator, { exportVisits }: ReportExporter) => boundToMercureHub(({
|
||||
getTagVisits,
|
||||
tagVisits,
|
||||
cancelGetTagVisits,
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { CsvJson } from 'csvjson';
|
||||
import { NormalizedVisit } from '../types';
|
||||
import { saveCsv } from '../../utils/helpers/files';
|
||||
|
||||
export class VisitsExporter {
|
||||
public constructor(
|
||||
private readonly window: Window,
|
||||
private readonly csvjson: CsvJson,
|
||||
) {}
|
||||
|
||||
public readonly exportVisits = (filename: string, visits: NormalizedVisit[]) => {
|
||||
if (!visits.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const csv = this.csvjson.toCSV(visits, { headers: 'key' });
|
||||
|
||||
saveCsv(this.window, csv, filename);
|
||||
};
|
||||
}
|
||||
@@ -12,31 +12,30 @@ import { cancelGetNonOrphanVisits, getNonOrphanVisits } from '../reducers/nonOrp
|
||||
import { ConnectDecorator } from '../../container/types';
|
||||
import { loadVisitsOverview } from '../reducers/visitsOverview';
|
||||
import * as visitsParser from './VisitsParser';
|
||||
import { VisitsExporter } from './VisitsExporter';
|
||||
|
||||
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||
// Components
|
||||
bottle.serviceFactory('MapModal', () => MapModal);
|
||||
|
||||
bottle.serviceFactory('ShortUrlVisits', ShortUrlVisits, 'VisitsExporter');
|
||||
bottle.serviceFactory('ShortUrlVisits', ShortUrlVisits, 'ReportExporter');
|
||||
bottle.decorator('ShortUrlVisits', connect(
|
||||
[ 'shortUrlVisits', 'shortUrlDetail', 'mercureInfo', 'settings', 'selectedServer' ],
|
||||
[ 'getShortUrlVisits', 'getShortUrlDetail', 'cancelGetShortUrlVisits', 'createNewVisits', 'loadMercureInfo' ],
|
||||
));
|
||||
|
||||
bottle.serviceFactory('TagVisits', TagVisits, 'ColorGenerator', 'VisitsExporter');
|
||||
bottle.serviceFactory('TagVisits', TagVisits, 'ColorGenerator', 'ReportExporter');
|
||||
bottle.decorator('TagVisits', connect(
|
||||
[ 'tagVisits', 'mercureInfo', 'settings', 'selectedServer' ],
|
||||
[ 'getTagVisits', 'cancelGetTagVisits', 'createNewVisits', 'loadMercureInfo' ],
|
||||
));
|
||||
|
||||
bottle.serviceFactory('OrphanVisits', OrphanVisits, 'VisitsExporter');
|
||||
bottle.serviceFactory('OrphanVisits', OrphanVisits, 'ReportExporter');
|
||||
bottle.decorator('OrphanVisits', connect(
|
||||
[ 'orphanVisits', 'mercureInfo', 'settings', 'selectedServer' ],
|
||||
[ 'getOrphanVisits', 'cancelGetOrphanVisits', 'createNewVisits', 'loadMercureInfo' ],
|
||||
));
|
||||
|
||||
bottle.serviceFactory('NonOrphanVisits', NonOrphanVisits, 'VisitsExporter');
|
||||
bottle.serviceFactory('NonOrphanVisits', NonOrphanVisits, 'ReportExporter');
|
||||
bottle.decorator('NonOrphanVisits', connect(
|
||||
[ 'nonOrphanVisits', 'mercureInfo', 'settings', 'selectedServer' ],
|
||||
[ 'getNonOrphanVisits', 'cancelGetNonOrphanVisits', 'createNewVisits', 'loadMercureInfo' ],
|
||||
@@ -44,7 +43,6 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||
|
||||
// Services
|
||||
bottle.serviceFactory('VisitsParser', () => visitsParser);
|
||||
bottle.service('VisitsExporter', VisitsExporter, 'window', 'csvjson');
|
||||
|
||||
// Actions
|
||||
bottle.serviceFactory('getShortUrlVisits', getShortUrlVisits, 'buildShlinkApiClient');
|
||||
|
||||
Reference in New Issue
Block a user