mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-03 23:07:26 +03:00
Added new reducer for visits overview, and added it to overview page
This commit is contained in:
parent
032e9c53f3
commit
d9e39eee2b
11 changed files with 181 additions and 91 deletions
|
@ -15,6 +15,7 @@ import { ShortUrlDetail } from '../visits/reducers/shortUrlDetail';
|
||||||
import { ShortUrlVisits } from '../visits/reducers/shortUrlVisits';
|
import { ShortUrlVisits } from '../visits/reducers/shortUrlVisits';
|
||||||
import { TagVisits } from '../visits/reducers/tagVisits';
|
import { TagVisits } from '../visits/reducers/tagVisits';
|
||||||
import { DomainsList } from '../domains/reducers/domainsList';
|
import { DomainsList } from '../domains/reducers/domainsList';
|
||||||
|
import { VisitsOverview } from '../visits/reducers/visitsOverview';
|
||||||
|
|
||||||
export interface ShlinkState {
|
export interface ShlinkState {
|
||||||
servers: ServersMap;
|
servers: ServersMap;
|
||||||
|
@ -35,6 +36,7 @@ export interface ShlinkState {
|
||||||
mercureInfo: MercureInfo;
|
mercureInfo: MercureInfo;
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
domainsList: DomainsList;
|
domainsList: DomainsList;
|
||||||
|
visitsOverview: VisitsOverview;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ConnectDecorator = (props: string[] | null, actions?: string[]) => any;
|
export type ConnectDecorator = (props: string[] | null, actions?: string[]) => any;
|
||||||
|
|
|
@ -17,6 +17,7 @@ import tagEditReducer from '../tags/reducers/tagEdit';
|
||||||
import mercureInfoReducer from '../mercure/reducers/mercureInfo';
|
import mercureInfoReducer from '../mercure/reducers/mercureInfo';
|
||||||
import settingsReducer from '../settings/reducers/settings';
|
import settingsReducer from '../settings/reducers/settings';
|
||||||
import domainsListReducer from '../domains/reducers/domainsList';
|
import domainsListReducer from '../domains/reducers/domainsList';
|
||||||
|
import visitsOverviewReducer from '../visits/reducers/visitsOverview';
|
||||||
import { ShlinkState } from '../container/types';
|
import { ShlinkState } from '../container/types';
|
||||||
|
|
||||||
export default combineReducers<ShlinkState>({
|
export default combineReducers<ShlinkState>({
|
||||||
|
@ -38,4 +39,5 @@ export default combineReducers<ShlinkState>({
|
||||||
mercureInfo: mercureInfoReducer,
|
mercureInfo: mercureInfoReducer,
|
||||||
settings: settingsReducer,
|
settings: settingsReducer,
|
||||||
domainsList: domainsListReducer,
|
domainsList: domainsListReducer,
|
||||||
|
visitsOverview: visitsOverviewReducer,
|
||||||
});
|
});
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { prettify } from '../utils/helpers/numbers';
|
||||||
import { TagsList } from '../tags/reducers/tagsList';
|
import { TagsList } from '../tags/reducers/tagsList';
|
||||||
import { ShortUrlsTableProps } from '../short-urls/ShortUrlsTable';
|
import { ShortUrlsTableProps } from '../short-urls/ShortUrlsTable';
|
||||||
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
||||||
|
import { VisitsOverview } from '../visits/reducers/visitsOverview';
|
||||||
import { isServerWithId, SelectedServer } from './data';
|
import { isServerWithId, SelectedServer } from './data';
|
||||||
import './Overview.scss';
|
import './Overview.scss';
|
||||||
|
|
||||||
|
@ -16,18 +17,28 @@ interface OverviewConnectProps {
|
||||||
listTags: Function;
|
listTags: Function;
|
||||||
tagsList: TagsList;
|
tagsList: TagsList;
|
||||||
selectedServer: SelectedServer;
|
selectedServer: SelectedServer;
|
||||||
|
visitsOverview: VisitsOverview;
|
||||||
|
loadVisitsOverview: Function;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Overview = (ShortUrlsTable: FC<ShortUrlsTableProps>) => boundToMercureHub((
|
export const Overview = (ShortUrlsTable: FC<ShortUrlsTableProps>) => boundToMercureHub(({
|
||||||
{ shortUrlsList, listShortUrls, listTags, tagsList, selectedServer }: OverviewConnectProps,
|
shortUrlsList,
|
||||||
) => {
|
listShortUrls,
|
||||||
const { loading, error, shortUrls } = shortUrlsList;
|
listTags,
|
||||||
|
tagsList,
|
||||||
|
selectedServer,
|
||||||
|
loadVisitsOverview,
|
||||||
|
visitsOverview,
|
||||||
|
}: OverviewConnectProps) => {
|
||||||
|
const { loading, shortUrls } = shortUrlsList;
|
||||||
const { loading: loadingTags } = tagsList;
|
const { loading: loadingTags } = tagsList;
|
||||||
|
const { loading: loadingVisits, visitsCount } = visitsOverview;
|
||||||
const serverId = !isServerWithId(selectedServer) ? '' : selectedServer.id;
|
const serverId = !isServerWithId(selectedServer) ? '' : selectedServer.id;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
listShortUrls({ itemsPerPage: 5, orderBy: { dateCreated: 'DESC' } });
|
listShortUrls({ itemsPerPage: 5, orderBy: { dateCreated: 'DESC' } });
|
||||||
listTags();
|
listTags();
|
||||||
|
loadVisitsOverview();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -36,16 +47,14 @@ export const Overview = (ShortUrlsTable: FC<ShortUrlsTableProps>) => boundToMerc
|
||||||
<div className="col-sm-4">
|
<div className="col-sm-4">
|
||||||
<Card className="overview__card mb-2" body>
|
<Card className="overview__card mb-2" body>
|
||||||
<CardTitle tag="h5" className="overview__card-title">Visits</CardTitle>
|
<CardTitle tag="h5" className="overview__card-title">Visits</CardTitle>
|
||||||
<CardText tag="h2">?</CardText>
|
<CardText tag="h2">{loadingVisits ? 'Loading...' : prettify(visitsCount)}</CardText>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-sm-4">
|
<div className="col-sm-4">
|
||||||
<Card className="overview__card mb-2" body>
|
<Card className="overview__card mb-2" body>
|
||||||
<CardTitle tag="h5" className="overview__card-title">Short URLs</CardTitle>
|
<CardTitle tag="h5" className="overview__card-title">Short URLs</CardTitle>
|
||||||
<CardText tag="h2">
|
<CardText tag="h2">
|
||||||
{loading && !error && 'Loading...'}
|
{loading ? 'Loading...' : prettify(shortUrls?.pagination.totalItems ?? 0)}
|
||||||
{error && !loading && 'Failed :('}
|
|
||||||
{!error && !loading && prettify(shortUrls?.pagination.totalItems ?? 0)}
|
|
||||||
</CardText>
|
</CardText>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -46,8 +46,8 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator, withRouter:
|
||||||
|
|
||||||
bottle.serviceFactory('Overview', Overview, 'ShortUrlsTable');
|
bottle.serviceFactory('Overview', Overview, 'ShortUrlsTable');
|
||||||
bottle.decorator('Overview', connect(
|
bottle.decorator('Overview', connect(
|
||||||
[ 'shortUrlsList', 'tagsList', 'selectedServer', 'mercureInfo' ],
|
[ 'shortUrlsList', 'tagsList', 'selectedServer', 'mercureInfo', 'visitsOverview' ],
|
||||||
[ 'listShortUrls', 'listTags', 'createNewVisits', 'loadMercureInfo' ],
|
[ 'listShortUrls', 'listTags', 'createNewVisits', 'loadMercureInfo', 'loadVisitsOverview' ],
|
||||||
));
|
));
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
|
|
8
src/short-urls/CreateShortUrl.scss
Normal file
8
src/short-urls/CreateShortUrl.scss
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
@import '../utils/base';
|
||||||
|
|
||||||
|
.create-short-url__save-btn {
|
||||||
|
@media (max-width: $xsMax) {
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,15 +1,12 @@
|
||||||
import { faAngleDoubleDown as downIcon, faAngleDoubleUp as upIcon } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import { isEmpty, pipe, replace, trim } from 'ramda';
|
import { isEmpty, pipe, replace, trim } from 'ramda';
|
||||||
import { FC, useState } from 'react';
|
import { FC, useState } from 'react';
|
||||||
import { Collapse, FormGroup, Input } from 'reactstrap';
|
import { Button, FormGroup, Input } from 'reactstrap';
|
||||||
import { InputType } from 'reactstrap/lib/Input';
|
import { InputType } from 'reactstrap/lib/Input';
|
||||||
import * as m from 'moment';
|
import * as m from 'moment';
|
||||||
import DateInput, { DateInputProps } from '../utils/DateInput';
|
import DateInput, { DateInputProps } from '../utils/DateInput';
|
||||||
import Checkbox from '../utils/Checkbox';
|
import Checkbox from '../utils/Checkbox';
|
||||||
import { versionMatch, Versions } from '../utils/helpers/version';
|
import { versionMatch, Versions } from '../utils/helpers/version';
|
||||||
import { handleEventPreventingDefault, hasValue } from '../utils/utils';
|
import { handleEventPreventingDefault, hasValue } from '../utils/utils';
|
||||||
import { useToggle } from '../utils/helpers/hooks';
|
|
||||||
import { isReachableServer, SelectedServer } from '../servers/data';
|
import { isReachableServer, SelectedServer } from '../servers/data';
|
||||||
import { formatIsoDate } from '../utils/helpers/date';
|
import { formatIsoDate } from '../utils/helpers/date';
|
||||||
import { TagsSelectorProps } from '../tags/helpers/TagsSelector';
|
import { TagsSelectorProps } from '../tags/helpers/TagsSelector';
|
||||||
|
@ -18,6 +15,7 @@ import { ShortUrlData } from './data';
|
||||||
import { ShortUrlCreation } from './reducers/shortUrlCreation';
|
import { ShortUrlCreation } from './reducers/shortUrlCreation';
|
||||||
import UseExistingIfFoundInfoIcon from './UseExistingIfFoundInfoIcon';
|
import UseExistingIfFoundInfoIcon from './UseExistingIfFoundInfoIcon';
|
||||||
import { CreateShortUrlResultProps } from './helpers/CreateShortUrlResult';
|
import { CreateShortUrlResultProps } from './helpers/CreateShortUrlResult';
|
||||||
|
import './CreateShortUrl.scss';
|
||||||
|
|
||||||
const normalizeTag = pipe(trim, replace(/ /g, '-'));
|
const normalizeTag = pipe(trim, replace(/ /g, '-'));
|
||||||
|
|
||||||
|
@ -51,7 +49,6 @@ const CreateShortUrl = (
|
||||||
DomainSelector: FC<DomainSelectorProps>,
|
DomainSelector: FC<DomainSelectorProps>,
|
||||||
) => ({ createShortUrl, shortUrlCreationResult, resetCreateShortUrl, selectedServer }: CreateShortUrlProps) => {
|
) => ({ createShortUrl, shortUrlCreationResult, resetCreateShortUrl, selectedServer }: CreateShortUrlProps) => {
|
||||||
const [ shortUrlCreation, setShortUrlCreation ] = useState(initialState);
|
const [ shortUrlCreation, setShortUrlCreation ] = useState(initialState);
|
||||||
const [ moreOptionsVisible, toggleMoreOptionsVisible ] = useToggle();
|
|
||||||
|
|
||||||
const changeTags = (tags: string[]) => setShortUrlCreation({ ...shortUrlCreation, tags: tags.map(normalizeTag) });
|
const changeTags = (tags: string[]) => setShortUrlCreation({ ...shortUrlCreation, tags: tags.map(normalizeTag) });
|
||||||
const reset = () => setShortUrlCreation(initialState);
|
const reset = () => setShortUrlCreation(initialState);
|
||||||
|
@ -106,94 +103,89 @@ const CreateShortUrl = (
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Collapse isOpen={moreOptionsVisible}>
|
<div className="form-group">
|
||||||
<div className="form-group">
|
<TagsSelector tags={shortUrlCreation.tags ?? []} onChange={changeTags} />
|
||||||
<TagsSelector tags={shortUrlCreation.tags ?? []} onChange={changeTags} />
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-sm-4">
|
<div className="col-sm-4">
|
||||||
{renderOptionalInput('customSlug', 'Custom slug', 'text', {
|
{renderOptionalInput('customSlug', 'Custom slug', 'text', {
|
||||||
disabled: hasValue(shortUrlCreation.shortCodeLength),
|
disabled: hasValue(shortUrlCreation.shortCodeLength),
|
||||||
})}
|
})}
|
||||||
</div>
|
|
||||||
<div className="col-sm-4">
|
|
||||||
{renderOptionalInput('shortCodeLength', 'Short code length', 'number', {
|
|
||||||
min: 4,
|
|
||||||
disabled: disableShortCodeLength || hasValue(shortUrlCreation.customSlug),
|
|
||||||
...disableShortCodeLength && {
|
|
||||||
title: 'Shlink 2.1.0 or higher is required to be able to provide the short code length',
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<div className="col-sm-4">
|
|
||||||
{!showDomainSelector && renderOptionalInput('domain', 'Domain', 'text', {
|
|
||||||
disabled: disableDomain,
|
|
||||||
...disableDomain && { title: 'Shlink 1.19.0 or higher is required to be able to provide the domain' },
|
|
||||||
})}
|
|
||||||
{showDomainSelector && (
|
|
||||||
<FormGroup>
|
|
||||||
<DomainSelector
|
|
||||||
value={shortUrlCreation.domain}
|
|
||||||
onChange={(domain?: string) => setShortUrlCreation({ ...shortUrlCreation, domain })}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="col-sm-4">
|
||||||
<div className="row">
|
{renderOptionalInput('shortCodeLength', 'Short code length', 'number', {
|
||||||
<div className="col-sm-4">
|
min: 4,
|
||||||
{renderOptionalInput('maxVisits', 'Maximum number of visits allowed', 'number', { min: 1 })}
|
disabled: disableShortCodeLength || hasValue(shortUrlCreation.customSlug),
|
||||||
</div>
|
...disableShortCodeLength && {
|
||||||
<div className="col-sm-4">
|
title: 'Shlink 2.1.0 or higher is required to be able to provide the short code length',
|
||||||
{renderDateInput('validSince', 'Enabled since...', { maxDate: shortUrlCreation.validUntil as m.Moment | undefined })}
|
},
|
||||||
</div>
|
})}
|
||||||
<div className="col-sm-4">
|
|
||||||
{renderDateInput('validUntil', 'Enabled until...', { minDate: shortUrlCreation.validSince as m.Moment | undefined })}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="col-sm-4">
|
||||||
|
{!showDomainSelector && renderOptionalInput('domain', 'Domain', 'text', {
|
||||||
|
disabled: disableDomain,
|
||||||
|
...disableDomain && { title: 'Shlink 1.19.0 or higher is required to be able to provide the domain' },
|
||||||
|
})}
|
||||||
|
{showDomainSelector && (
|
||||||
|
<FormGroup>
|
||||||
|
<DomainSelector
|
||||||
|
value={shortUrlCreation.domain}
|
||||||
|
onChange={(domain?: string) => setShortUrlCreation({ ...shortUrlCreation, domain })}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ForServerVersion minVersion="1.16.0">
|
<div className="row">
|
||||||
<div className="mb-4 row">
|
<div className="col-sm-4">
|
||||||
<div className="col-sm-6 text-center text-sm-left mb-2 mb-sm-0">
|
{renderOptionalInput('maxVisits', 'Maximum number of visits allowed', 'number', { min: 1 })}
|
||||||
<ForServerVersion minVersion="2.4.0">
|
</div>
|
||||||
<Checkbox
|
<div className="col-sm-4">
|
||||||
inline
|
{renderDateInput('validSince', 'Enabled since...', { maxDate: shortUrlCreation.validUntil as m.Moment | undefined })}
|
||||||
checked={shortUrlCreation.validateUrl}
|
</div>
|
||||||
onChange={(validateUrl) => setShortUrlCreation({ ...shortUrlCreation, validateUrl })}
|
<div className="col-sm-4">
|
||||||
>
|
{renderDateInput('validUntil', 'Enabled until...', { minDate: shortUrlCreation.validSince as m.Moment | undefined })}
|
||||||
Validate URL
|
</div>
|
||||||
</Checkbox>
|
</div>
|
||||||
</ForServerVersion>
|
|
||||||
</div>
|
<ForServerVersion minVersion="1.16.0">
|
||||||
<div className="col-sm-6 text-center text-sm-right">
|
<div className="mb-4 row">
|
||||||
|
<div className="col-sm-6 text-center text-sm-left mb-2 mb-sm-0">
|
||||||
|
<ForServerVersion minVersion="2.4.0">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
inline
|
inline
|
||||||
className="mr-2"
|
checked={shortUrlCreation.validateUrl}
|
||||||
checked={shortUrlCreation.findIfExists}
|
onChange={(validateUrl) => setShortUrlCreation({ ...shortUrlCreation, validateUrl })}
|
||||||
onChange={(findIfExists) => setShortUrlCreation({ ...shortUrlCreation, findIfExists })}
|
|
||||||
>
|
>
|
||||||
Use existing URL if found
|
Validate URL
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
<UseExistingIfFoundInfoIcon />
|
</ForServerVersion>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</ForServerVersion>
|
<div className="col-sm-6 text-center text-sm-right">
|
||||||
</Collapse>
|
<Checkbox
|
||||||
|
inline
|
||||||
|
className="mr-2"
|
||||||
|
checked={shortUrlCreation.findIfExists}
|
||||||
|
onChange={(findIfExists) => setShortUrlCreation({ ...shortUrlCreation, findIfExists })}
|
||||||
|
>
|
||||||
|
Use existing URL if found
|
||||||
|
</Checkbox>
|
||||||
|
<UseExistingIfFoundInfoIcon />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ForServerVersion>
|
||||||
|
|
||||||
<div>
|
<div className="text-right">
|
||||||
<button type="button" className="btn btn-outline-secondary" onClick={toggleMoreOptionsVisible}>
|
<Button
|
||||||
<FontAwesomeIcon icon={moreOptionsVisible ? upIcon : downIcon} />
|
outline
|
||||||
|
color="primary"
|
||||||
{moreOptionsVisible ? 'Less' : 'More'} options
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn-outline-primary float-right"
|
|
||||||
disabled={shortUrlCreationResult.saving || isEmpty(shortUrlCreation.longUrl)}
|
disabled={shortUrlCreationResult.saving || isEmpty(shortUrlCreation.longUrl)}
|
||||||
|
className="create-short-url__save-btn"
|
||||||
>
|
>
|
||||||
{shortUrlCreationResult.saving ? 'Creating...' : 'Create'}
|
{shortUrlCreationResult.saving ? 'Creating...' : 'Create'}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CreateShortUrlResult {...shortUrlCreationResult} resetCreateShortUrl={resetCreateShortUrl} />
|
<CreateShortUrlResult {...shortUrlCreationResult} resetCreateShortUrl={resetCreateShortUrl} />
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {
|
||||||
ShlinkShortUrlMeta,
|
ShlinkShortUrlMeta,
|
||||||
ShlinkDomain,
|
ShlinkDomain,
|
||||||
ShlinkDomainsResponse,
|
ShlinkDomainsResponse,
|
||||||
|
ShlinkVisitsOverview,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
const buildShlinkBaseUrl = (url: string, apiVersion: number) => url ? `${url}/rest/v${apiVersion}` : '';
|
const buildShlinkBaseUrl = (url: string, apiVersion: number) => url ? `${url}/rest/v${apiVersion}` : '';
|
||||||
|
@ -50,6 +51,10 @@ export default class ShlinkApiClient {
|
||||||
this.performRequest<{ visits: ShlinkVisits }>(`/tags/${tag}/visits`, 'GET', query)
|
this.performRequest<{ visits: ShlinkVisits }>(`/tags/${tag}/visits`, 'GET', query)
|
||||||
.then(({ data }) => data.visits);
|
.then(({ data }) => data.visits);
|
||||||
|
|
||||||
|
public readonly getVisitsOverview = async (): Promise<ShlinkVisitsOverview> =>
|
||||||
|
this.performRequest<{ visits: ShlinkVisitsOverview }>('/visits', 'GET')
|
||||||
|
.then(({ data }) => data.visits);
|
||||||
|
|
||||||
public readonly getShortUrl = async (shortCode: string, domain?: OptionalString): Promise<ShortUrl> =>
|
public readonly getShortUrl = async (shortCode: string, domain?: OptionalString): Promise<ShortUrl> =>
|
||||||
this.performRequest<ShortUrl>(`/short-urls/${shortCode}`, 'GET', { domain })
|
this.performRequest<ShortUrl>(`/short-urls/${shortCode}`, 'GET', { domain })
|
||||||
.then(({ data }) => data);
|
.then(({ data }) => data);
|
||||||
|
|
|
@ -44,6 +44,10 @@ export interface ShlinkVisits {
|
||||||
pagination?: ShlinkPaginator; // Is only optional in old Shlink versions
|
pagination?: ShlinkPaginator; // Is only optional in old Shlink versions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ShlinkVisitsOverview {
|
||||||
|
visitsCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ShlinkVisitsParams {
|
export interface ShlinkVisitsParams {
|
||||||
domain?: OptionalString;
|
domain?: OptionalString;
|
||||||
page?: number;
|
page?: number;
|
||||||
|
|
52
src/visits/reducers/visitsOverview.ts
Normal file
52
src/visits/reducers/visitsOverview.ts
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import { Action, Dispatch } from 'redux';
|
||||||
|
import { ShlinkVisitsOverview } from '../../utils/services/types';
|
||||||
|
import { ShlinkApiClientBuilder } from '../../utils/services/ShlinkApiClientBuilder';
|
||||||
|
import { GetState } from '../../container/types';
|
||||||
|
import { buildReducer } from '../../utils/helpers/redux';
|
||||||
|
import { CREATE_VISITS, CreateVisitsAction } from './visitCreation';
|
||||||
|
|
||||||
|
/* eslint-disable padding-line-between-statements */
|
||||||
|
export const GET_OVERVIEW_START = 'shlink/visitsOverview/GET_OVERVIEW_START';
|
||||||
|
export const GET_OVERVIEW_ERROR = 'shlink/visitsOverview/GET_OVERVIEW_ERROR';
|
||||||
|
export const GET_OVERVIEW = 'shlink/visitsOverview/GET_OVERVIEW';
|
||||||
|
/* eslint-enable padding-line-between-statements */
|
||||||
|
|
||||||
|
export interface VisitsOverview {
|
||||||
|
visitsCount: number;
|
||||||
|
loading: boolean;
|
||||||
|
error: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetVisitsOverviewAction = ShlinkVisitsOverview & Action<string>;
|
||||||
|
|
||||||
|
const initialState: VisitsOverview = {
|
||||||
|
visitsCount: 0,
|
||||||
|
loading: false,
|
||||||
|
error: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default buildReducer<VisitsOverview, GetVisitsOverviewAction & CreateVisitsAction>({
|
||||||
|
[GET_OVERVIEW_START]: () => ({ ...initialState, loading: true }),
|
||||||
|
[GET_OVERVIEW_ERROR]: () => ({ ...initialState, error: true }),
|
||||||
|
[GET_OVERVIEW]: (_, { visitsCount }) => ({ ...initialState, visitsCount }),
|
||||||
|
[CREATE_VISITS]: ({ visitsCount, ...rest }, { createdVisits }) => ({
|
||||||
|
...rest,
|
||||||
|
visitsCount: visitsCount + createdVisits.length,
|
||||||
|
}),
|
||||||
|
}, initialState);
|
||||||
|
|
||||||
|
export const loadVisitsOverview = (buildShlinkApiClient: ShlinkApiClientBuilder) => () => async (
|
||||||
|
dispatch: Dispatch,
|
||||||
|
getState: GetState,
|
||||||
|
) => {
|
||||||
|
dispatch({ type: GET_OVERVIEW_START });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { getVisitsOverview } = buildShlinkApiClient(getState);
|
||||||
|
const result = await getVisitsOverview();
|
||||||
|
|
||||||
|
dispatch({ type: GET_OVERVIEW, ...result });
|
||||||
|
} catch (e) {
|
||||||
|
dispatch({ type: GET_OVERVIEW_ERROR });
|
||||||
|
}
|
||||||
|
};
|
|
@ -7,6 +7,7 @@ import { createNewVisits } from '../reducers/visitCreation';
|
||||||
import { cancelGetTagVisits, getTagVisits } from '../reducers/tagVisits';
|
import { cancelGetTagVisits, getTagVisits } from '../reducers/tagVisits';
|
||||||
import TagVisits from '../TagVisits';
|
import TagVisits from '../TagVisits';
|
||||||
import { ConnectDecorator } from '../../container/types';
|
import { ConnectDecorator } from '../../container/types';
|
||||||
|
import { loadVisitsOverview } from '../reducers/visitsOverview';
|
||||||
import * as visitsParser from './VisitsParser';
|
import * as visitsParser from './VisitsParser';
|
||||||
|
|
||||||
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
|
@ -35,6 +36,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
bottle.serviceFactory('cancelGetTagVisits', () => cancelGetTagVisits);
|
bottle.serviceFactory('cancelGetTagVisits', () => cancelGetTagVisits);
|
||||||
|
|
||||||
bottle.serviceFactory('createNewVisits', () => createNewVisits);
|
bottle.serviceFactory('createNewVisits', () => createNewVisits);
|
||||||
|
bottle.serviceFactory('loadVisitsOverview', loadVisitsOverview, 'buildShlinkApiClient');
|
||||||
};
|
};
|
||||||
|
|
||||||
export default provideServices;
|
export default provideServices;
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { AxiosInstance, AxiosRequestConfig } from 'axios';
|
||||||
import ShlinkApiClient from '../../../src/utils/services/ShlinkApiClient';
|
import ShlinkApiClient from '../../../src/utils/services/ShlinkApiClient';
|
||||||
import { OptionalString } from '../../../src/utils/utils';
|
import { OptionalString } from '../../../src/utils/utils';
|
||||||
import { Mock } from 'ts-mockery';
|
import { Mock } from 'ts-mockery';
|
||||||
import { ShlinkDomain } from '../../../src/utils/services/types';
|
import { ShlinkDomain, ShlinkVisitsOverview } from '../../../src/utils/services/types';
|
||||||
|
|
||||||
describe('ShlinkApiClient', () => {
|
describe('ShlinkApiClient', () => {
|
||||||
const createAxios = (data: AxiosRequestConfig) => (async () => Promise.resolve(data)) as unknown as AxiosInstance;
|
const createAxios = (data: AxiosRequestConfig) => (async () => Promise.resolve(data)) as unknown as AxiosInstance;
|
||||||
|
@ -269,4 +269,18 @@ describe('ShlinkApiClient', () => {
|
||||||
expect(result).toEqual(expectedData);
|
expect(result).toEqual(expectedData);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getVisitsOverview', () => {
|
||||||
|
it('returns visits overview', async () => {
|
||||||
|
const expectedData = Mock.all<ShlinkVisitsOverview>();
|
||||||
|
const resp = { visits: expectedData };
|
||||||
|
const axiosSpy = createAxiosMock({ data: resp });
|
||||||
|
const { getVisitsOverview } = new ShlinkApiClient(axiosSpy, '', '');
|
||||||
|
|
||||||
|
const result = await getVisitsOverview();
|
||||||
|
|
||||||
|
expect(axiosSpy).toHaveBeenCalled();
|
||||||
|
expect(result).toEqual(expectedData);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue