mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2026-04-19 21:16:18 +00:00
Added button to export visits as CSV
This commit is contained in:
@@ -5,7 +5,8 @@ import { Topics } from '../mercure/helpers/Topics';
|
||||
import { Settings } from '../settings/reducers/settings';
|
||||
import VisitsStats from './VisitsStats';
|
||||
import { OrphanVisitsHeader } from './OrphanVisitsHeader';
|
||||
import { VisitsInfo } from './types';
|
||||
import { NormalizedVisit, VisitsInfo } from './types';
|
||||
import { VisitsExporter } from './services/VisitsExporter';
|
||||
|
||||
export interface OrphanVisitsProps extends RouteComponentProps {
|
||||
getOrphanVisits: (params: ShlinkVisitsParams) => void;
|
||||
@@ -14,21 +15,26 @@ export interface OrphanVisitsProps extends RouteComponentProps {
|
||||
settings: Settings;
|
||||
}
|
||||
|
||||
export const OrphanVisits = boundToMercureHub(({
|
||||
export const OrphanVisits = ({ exportVisits }: VisitsExporter) => boundToMercureHub(({
|
||||
history: { goBack },
|
||||
match: { url },
|
||||
getOrphanVisits,
|
||||
orphanVisits,
|
||||
cancelGetOrphanVisits,
|
||||
settings,
|
||||
}: OrphanVisitsProps) => (
|
||||
<VisitsStats
|
||||
getVisits={getOrphanVisits}
|
||||
cancelGetVisits={cancelGetOrphanVisits}
|
||||
visitsInfo={orphanVisits}
|
||||
baseUrl={url}
|
||||
settings={settings}
|
||||
>
|
||||
<OrphanVisitsHeader orphanVisits={orphanVisits} goBack={goBack} />
|
||||
</VisitsStats>
|
||||
), () => [ Topics.orphanVisits() ]);
|
||||
}: OrphanVisitsProps) => {
|
||||
const exportCsv = (visits: NormalizedVisit[]) => exportVisits('orphan_visits.csv', visits);
|
||||
|
||||
return (
|
||||
<VisitsStats
|
||||
getVisits={getOrphanVisits}
|
||||
cancelGetVisits={cancelGetOrphanVisits}
|
||||
visitsInfo={orphanVisits}
|
||||
baseUrl={url}
|
||||
settings={settings}
|
||||
exportCsv={exportCsv}
|
||||
>
|
||||
<OrphanVisitsHeader orphanVisits={orphanVisits} goBack={goBack} />
|
||||
</VisitsStats>
|
||||
);
|
||||
}, () => [ Topics.orphanVisits() ]);
|
||||
|
||||
@@ -9,6 +9,8 @@ import { Settings } from '../settings/reducers/settings';
|
||||
import { ShortUrlVisits as ShortUrlVisitsState } from './reducers/shortUrlVisits';
|
||||
import ShortUrlVisitsHeader from './ShortUrlVisitsHeader';
|
||||
import VisitsStats from './VisitsStats';
|
||||
import { VisitsExporter } from './services/VisitsExporter';
|
||||
import { NormalizedVisit } from './types';
|
||||
|
||||
export interface ShortUrlVisitsProps extends RouteComponentProps<{ shortCode: string }> {
|
||||
getShortUrlVisits: (shortCode: string, query?: ShlinkVisitsParams) => void;
|
||||
@@ -19,7 +21,7 @@ export interface ShortUrlVisitsProps extends RouteComponentProps<{ shortCode: st
|
||||
settings: Settings;
|
||||
}
|
||||
|
||||
const ShortUrlVisits = boundToMercureHub(({
|
||||
const ShortUrlVisits = ({ exportVisits }: VisitsExporter) => boundToMercureHub(({
|
||||
history: { goBack },
|
||||
match: { params, url },
|
||||
location: { search },
|
||||
@@ -33,6 +35,10 @@ const ShortUrlVisits = boundToMercureHub(({
|
||||
const { shortCode } = params;
|
||||
const { domain } = parseQuery<{ domain?: string }>(search);
|
||||
const loadVisits = (params: Partial<ShlinkVisitsParams>) => getShortUrlVisits(shortCode, { ...params, domain });
|
||||
const exportCsv = (visits: NormalizedVisit[]) => exportVisits(
|
||||
`short-url_${shortUrlDetail.shortUrl?.shortUrl.replace(/https?:\/\//g, '')}_visits.csv`,
|
||||
visits,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
getShortUrlDetail(shortCode, domain);
|
||||
@@ -46,6 +52,7 @@ const ShortUrlVisits = boundToMercureHub(({
|
||||
baseUrl={url}
|
||||
domain={domain}
|
||||
settings={settings}
|
||||
exportCsv={exportCsv}
|
||||
>
|
||||
<ShortUrlVisitsHeader shortUrlDetail={shortUrlDetail} shortUrlVisits={shortUrlVisits} goBack={goBack} />
|
||||
</VisitsStats>
|
||||
|
||||
@@ -7,6 +7,8 @@ import { Settings } from '../settings/reducers/settings';
|
||||
import { TagVisits as TagVisitsState } from './reducers/tagVisits';
|
||||
import TagVisitsHeader from './TagVisitsHeader';
|
||||
import VisitsStats from './VisitsStats';
|
||||
import { VisitsExporter } from './services/VisitsExporter';
|
||||
import { NormalizedVisit } from './types';
|
||||
|
||||
export interface TagVisitsProps extends RouteComponentProps<{ tag: string }> {
|
||||
getTagVisits: (tag: string, query: any) => void;
|
||||
@@ -15,7 +17,7 @@ export interface TagVisitsProps extends RouteComponentProps<{ tag: string }> {
|
||||
settings: Settings;
|
||||
}
|
||||
|
||||
const TagVisits = (colorGenerator: ColorGenerator) => boundToMercureHub(({
|
||||
const TagVisits = (colorGenerator: ColorGenerator, { exportVisits }: VisitsExporter) => boundToMercureHub(({
|
||||
history: { goBack },
|
||||
match: { params, url },
|
||||
getTagVisits,
|
||||
@@ -25,6 +27,7 @@ const TagVisits = (colorGenerator: ColorGenerator) => boundToMercureHub(({
|
||||
}: TagVisitsProps) => {
|
||||
const { tag } = params;
|
||||
const loadVisits = (params: ShlinkVisitsParams) => getTagVisits(tag, params);
|
||||
const exportCsv = (visits: NormalizedVisit[]) => exportVisits(`tag_${tag}_visits.csv`, visits);
|
||||
|
||||
return (
|
||||
<VisitsStats
|
||||
@@ -33,6 +36,7 @@ const TagVisits = (colorGenerator: ColorGenerator) => boundToMercureHub(({
|
||||
visitsInfo={tagVisits}
|
||||
baseUrl={url}
|
||||
settings={settings}
|
||||
exportCsv={exportCsv}
|
||||
>
|
||||
<TagVisitsHeader tagVisits={tagVisits} goBack={goBack} colorGenerator={colorGenerator} />
|
||||
</VisitsStats>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { isEmpty, propEq, values } from 'ramda';
|
||||
import { useState, useEffect, useMemo, FC } from 'react';
|
||||
import { Button, Card, Nav, NavLink, Progress, Row } from 'reactstrap';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faCalendarAlt, faMapMarkedAlt, faList, faChartPie } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faCalendarAlt, faMapMarkedAlt, faList, faChartPie, faFileExport } from '@fortawesome/free-solid-svg-icons';
|
||||
import { IconDefinition } from '@fortawesome/fontawesome-common-types';
|
||||
import { Route, Switch, NavLink as RouterNavLink, Redirect } from 'react-router-dom';
|
||||
import { Location } from 'history';
|
||||
@@ -30,6 +30,7 @@ export interface VisitsStatsProps {
|
||||
cancelGetVisits: () => void;
|
||||
baseUrl: string;
|
||||
domain?: string;
|
||||
exportCsv: (visits: NormalizedVisit[]) => void;
|
||||
}
|
||||
|
||||
interface VisitsNavLinkProps {
|
||||
@@ -76,7 +77,7 @@ const VisitsNavLink: FC<VisitsNavLinkProps & { to: string }> = ({ subPath, title
|
||||
);
|
||||
|
||||
const VisitsStats: FC<VisitsStatsProps> = (
|
||||
{ children, visitsInfo, getVisits, cancelGetVisits, baseUrl, domain, settings },
|
||||
{ children, visitsInfo, getVisits, cancelGetVisits, baseUrl, domain, settings, exportCsv },
|
||||
) => {
|
||||
const initialInterval: DateInterval = settings.visits?.defaultInterval ?? 'last30Days';
|
||||
const [ dateRange, setDateRange ] = useState<DateRange>(intervalToDateRange(initialInterval));
|
||||
@@ -266,6 +267,9 @@ const VisitsStats: FC<VisitsStatsProps> = (
|
||||
>
|
||||
Clear selection {highlightedVisits.length > 0 && <>({highlightedVisits.length})</>}
|
||||
</Button>
|
||||
<Button outline color="primary" onClick={() => exportCsv(normalizedVisits)}>
|
||||
Export <FontAwesomeIcon icon={faFileExport} />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
24
src/visits/services/VisitsExporter.ts
Normal file
24
src/visits/services/VisitsExporter.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { CsvJson } from 'csvjson';
|
||||
import { head, keys } from 'ramda';
|
||||
import { NormalizedVisit } from '../types';
|
||||
import { saveCsv } from '../../utils/helpers/csv';
|
||||
|
||||
export class VisitsExporter {
|
||||
public constructor(
|
||||
private readonly window: Window,
|
||||
private readonly csvjson: CsvJson,
|
||||
) {}
|
||||
|
||||
public readonly exportVisits = (filename: string, visits: NormalizedVisit[]) => {
|
||||
try {
|
||||
const csv = this.csvjson.toCSV(visits, {
|
||||
headers: keys(head(visits)).join(','),
|
||||
});
|
||||
|
||||
saveCsv(this.window, csv, filename);
|
||||
} catch (e) {
|
||||
// FIXME Handle error
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -11,24 +11,25 @@ import { cancelGetOrphanVisits, getOrphanVisits } from '../reducers/orphanVisits
|
||||
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);
|
||||
bottle.serviceFactory('ShortUrlVisits', ShortUrlVisits, 'VisitsExporter');
|
||||
bottle.decorator('ShortUrlVisits', connect(
|
||||
[ 'shortUrlVisits', 'shortUrlDetail', 'mercureInfo', 'settings' ],
|
||||
[ 'getShortUrlVisits', 'getShortUrlDetail', 'cancelGetShortUrlVisits', 'createNewVisits', 'loadMercureInfo' ],
|
||||
));
|
||||
|
||||
bottle.serviceFactory('TagVisits', TagVisits, 'ColorGenerator');
|
||||
bottle.serviceFactory('TagVisits', TagVisits, 'ColorGenerator', 'VisitsExporter');
|
||||
bottle.decorator('TagVisits', connect(
|
||||
[ 'tagVisits', 'mercureInfo', 'settings' ],
|
||||
[ 'getTagVisits', 'cancelGetTagVisits', 'createNewVisits', 'loadMercureInfo' ],
|
||||
));
|
||||
|
||||
bottle.serviceFactory('OrphanVisits', () => OrphanVisits);
|
||||
bottle.serviceFactory('OrphanVisits', OrphanVisits, 'VisitsExporter');
|
||||
bottle.decorator('OrphanVisits', connect(
|
||||
[ 'orphanVisits', 'mercureInfo', 'settings' ],
|
||||
[ 'getOrphanVisits', 'cancelGetOrphanVisits', 'createNewVisits', 'loadMercureInfo' ],
|
||||
@@ -36,6 +37,7 @@ 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