mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2026-02-27 20:26:40 +00:00
Defined how to parse visit stats and how to render them
This commit is contained in:
@@ -31,13 +31,13 @@ export class ShlinkApiClient {
|
||||
const filteredOptions = reject(value => isEmpty(value) || isNil(value), options);
|
||||
return this._performRequest('/short-codes', 'POST', {}, filteredOptions)
|
||||
.then(resp => resp.data)
|
||||
.catch(e => this._handleAuthError(e, this.listShortUrls, [filteredOptions]));
|
||||
.catch(e => this._handleAuthError(e, this.createShortUrl, [filteredOptions]));
|
||||
};
|
||||
|
||||
getShortUrlVisits = shortCode =>
|
||||
this._performRequest(`/short-codes/${shortCode}/visits`, 'GET')
|
||||
getShortUrlVisits = (shortCode, dates) =>
|
||||
this._performRequest(`/short-codes/${shortCode}/visits`, 'GET', dates)
|
||||
.then(resp => resp.data.visits.data)
|
||||
.catch(e => this._handleAuthError(e, this.listShortUrls, [shortCode]));
|
||||
.catch(e => this._handleAuthError(e, this.getShortUrlVisits, [shortCode, dates]));
|
||||
|
||||
_performRequest = async (url, method = 'GET', params = {}, data = {}) => {
|
||||
if (isEmpty(this._token)) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import selectedServerReducer from '../servers/reducers/selectedServer';
|
||||
import shortUrlsListReducer from '../short-urls/reducers/shortUrlsList';
|
||||
import shortUrlsListParamsReducer from '../short-urls/reducers/shortUrlsListParams';
|
||||
import shortUrlCreationResultReducer from '../short-urls/reducers/shortUrlCreationResult';
|
||||
import shortUrlVisitsReducer from '../short-urls/reducers/shortUrlVisits';
|
||||
|
||||
export default combineReducers({
|
||||
servers: serversReducer,
|
||||
@@ -12,4 +13,5 @@ export default combineReducers({
|
||||
shortUrlsList: shortUrlsListReducer,
|
||||
shortUrlsListParams: shortUrlsListParamsReducer,
|
||||
shortUrlCreationResult: shortUrlCreationResultReducer,
|
||||
shortUrlVisits: shortUrlVisitsReducer
|
||||
});
|
||||
|
||||
@@ -1,25 +1,68 @@
|
||||
import React from 'react';
|
||||
import { Doughnut, HorizontalBar } from 'react-chartjs-2';
|
||||
import { connect } from 'react-redux';
|
||||
import { pick } from 'ramda';
|
||||
import { Card, CardBody, CardHeader } from 'reactstrap';
|
||||
import { getShortUrlVisits } from './reducers/shortUrlVisits';
|
||||
import VisitsParser from '../visits/services/VisitsParser';
|
||||
|
||||
export class ShortUrlsVisits extends React.Component {
|
||||
state = { startDate: '', endDate: '' };
|
||||
|
||||
componentDidMount() {
|
||||
const { match: { params } } = this.props;
|
||||
this.props.getShortUrlVisits(params.shortCode);
|
||||
this.props.getShortUrlVisits(params.shortCode, this.state);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { match: { params }, selectedServer } = this.props;
|
||||
const { match: { params }, selectedServer, visitsParser, shortUrlVisits } = this.props;
|
||||
const serverUrl = selectedServer ? selectedServer.url : '';
|
||||
const shortUrl = `${serverUrl}/${params.shortCode}`;
|
||||
const generateGraphData = stats => ({
|
||||
labels: Object.keys(stats),
|
||||
datasets: Object.values(stats)
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="short-urls-container">
|
||||
<div className="card bg-light">
|
||||
<div className="card-body">
|
||||
<Card className="bg-light">
|
||||
<CardBody>
|
||||
<h2>Visit stats for <a target="_blank" href={shortUrl}>{shortUrl}</a></h2>
|
||||
{/* TODO Once Shlink's API allows it, add total visits counter, long URL and creation time */}
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-md-6">
|
||||
<Card className="mt-4">
|
||||
<CardHeader>Operating systems</CardHeader>
|
||||
<CardBody>
|
||||
<Doughnut data={generateGraphData(visitsParser.processOsStats(shortUrlVisits))} />
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<Card className="mt-4">
|
||||
<CardHeader>Browsers</CardHeader>
|
||||
<CardBody>
|
||||
<Doughnut data={generateGraphData(visitsParser.processBrowserStats(shortUrlVisits))} />
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<Card className="mt-4">
|
||||
<CardHeader>Countries</CardHeader>
|
||||
<CardBody>
|
||||
<HorizontalBar data={generateGraphData(visitsParser.processCountriesStats(shortUrlVisits))} />
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<Card className="mt-4">
|
||||
<CardHeader>Referrers</CardHeader>
|
||||
<CardBody>
|
||||
<HorizontalBar data={generateGraphData(visitsParser.processReferrersStats(shortUrlVisits))} />
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -27,6 +70,10 @@ export class ShortUrlsVisits extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(pick(['selectedServer']), {
|
||||
ShortUrlsVisits.defaultProps = {
|
||||
visitsParser: VisitsParser
|
||||
};
|
||||
|
||||
export default connect(pick(['selectedServer', 'shortUrlVisits']), {
|
||||
getShortUrlVisits
|
||||
})(ShortUrlsVisits);
|
||||
|
||||
@@ -34,11 +34,11 @@ export default function dispatch (state = initialState, action) {
|
||||
}
|
||||
}
|
||||
|
||||
export const getShortUrlVisits = shortCode => async dispatch => {
|
||||
export const getShortUrlVisits = (shortCode, dates) => async dispatch => {
|
||||
dispatch({ type: GET_SHORT_URL_VISITS_START });
|
||||
|
||||
try {
|
||||
const visits = await ShlinkApiClient.getShortUrlVisits(shortCode);
|
||||
const visits = await ShlinkApiClient.getShortUrlVisits(shortCode, dates);
|
||||
dispatch({ visits, type: GET_SHORT_URL_VISITS });
|
||||
} catch (e) {
|
||||
dispatch({ type: GET_SHORT_URL_VISITS_ERROR });
|
||||
|
||||
101
src/visits/services/VisitsParser.js
Normal file
101
src/visits/services/VisitsParser.js
Normal file
@@ -0,0 +1,101 @@
|
||||
import { forEach, isNil, isEmpty } from 'ramda';
|
||||
|
||||
const osFromUserAgent = userAgent => {
|
||||
const lowerUserAgent = userAgent.toLowerCase();
|
||||
|
||||
switch (true) {
|
||||
case (lowerUserAgent.indexOf('linux') >= 0):
|
||||
return 'Linux';
|
||||
case (lowerUserAgent.indexOf('windows') >= 0):
|
||||
return 'Windows';
|
||||
case (lowerUserAgent.indexOf('mac') >= 0):
|
||||
return 'MacOS';
|
||||
case (lowerUserAgent.indexOf('mobi') >= 0):
|
||||
return 'Mobile';
|
||||
default:
|
||||
return 'Others';
|
||||
}
|
||||
};
|
||||
|
||||
const browserFromUserAgent = userAgent => {
|
||||
const lowerUserAgent = userAgent.toLowerCase();
|
||||
|
||||
switch (true) {
|
||||
case (lowerUserAgent.indexOf('firefox') >= 0):
|
||||
return 'Firefox';
|
||||
case (lowerUserAgent.indexOf('chrome') >= 0):
|
||||
return 'Chrome';
|
||||
case (lowerUserAgent.indexOf('safari') >= 0):
|
||||
return 'Safari';
|
||||
case (lowerUserAgent.indexOf('opera') >= 0):
|
||||
return 'Opera';
|
||||
case (lowerUserAgent.indexOf('msie') >= 0):
|
||||
return 'Internet Explorer';
|
||||
default:
|
||||
return 'Others';
|
||||
}
|
||||
};
|
||||
|
||||
const extractDomain = url => {
|
||||
const domain = url.indexOf('://') > -1 ? url.split('/')[2] : url.split('/')[0];
|
||||
return domain.split(':')[0];
|
||||
};
|
||||
|
||||
// FIXME Refactor these foreach statements which mutate a stats object
|
||||
export class VisitsParser {
|
||||
processOsStats = visits => {
|
||||
const stats = {};
|
||||
|
||||
forEach(visit => {
|
||||
const userAgent = visit.userAgent;
|
||||
const os = isNil(userAgent) ? 'Others' : osFromUserAgent(userAgent);
|
||||
|
||||
stats[os] = typeof stats[os] === 'undefined' ? 1 : stats[os] + 1;
|
||||
}, visits);
|
||||
|
||||
return stats;
|
||||
};
|
||||
|
||||
processBrowserStats = visits => {
|
||||
const stats = {};
|
||||
|
||||
forEach(visit => {
|
||||
const userAgent = visit.userAgent;
|
||||
const browser = isNil(userAgent) ? 'Others' : browserFromUserAgent(userAgent);
|
||||
|
||||
stats[browser] = typeof stats[browser] === 'undefined' ? 1 : stats[browser] + 1;
|
||||
}, visits);
|
||||
|
||||
return stats;
|
||||
};
|
||||
|
||||
processReferrersStats = visits => {
|
||||
const stats = {};
|
||||
|
||||
forEach(visit => {
|
||||
const notHasDomain = isNil(visit.referer) || isEmpty(visit.referer);
|
||||
const domain = notHasDomain ? 'Unknown' : extractDomain(visit.referer);
|
||||
|
||||
stats[domain] = typeof stats[domain] === 'undefined' ? 1 : stats[domain] + 1;
|
||||
}, visits);
|
||||
|
||||
return stats;
|
||||
};
|
||||
|
||||
processCountriesStats = visits => {
|
||||
const stats = {};
|
||||
|
||||
forEach(({ visitLocation }) => {
|
||||
const notHasCountry = isNil(visitLocation)
|
||||
|| isNil(visitLocation.countryName)
|
||||
|| isEmpty(visitLocation.countryName);
|
||||
const country = notHasCountry ? 'Unknown' : visitLocation.countryName;
|
||||
|
||||
stats[country] = typeof stats[country] === 'undefined' ? 1 : stats[country] + 1;
|
||||
}, visits);
|
||||
|
||||
return stats;
|
||||
};
|
||||
}
|
||||
|
||||
export default new VisitsParser();
|
||||
Reference in New Issue
Block a user