mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 01:20:24 +03:00
Defined how to parse visit stats and how to render them
This commit is contained in:
parent
a75c7309f7
commit
d97cbdde5d
7 changed files with 196 additions and 13 deletions
|
@ -19,6 +19,7 @@
|
|||
"bootstrap": "^4.1.1",
|
||||
"case-sensitive-paths-webpack-plugin": "2.1.1",
|
||||
"chalk": "1.1.3",
|
||||
"chart.js": "^2.7.2",
|
||||
"css-loader": "0.28.7",
|
||||
"dotenv": "4.0.0",
|
||||
"dotenv-expand": "4.2.0",
|
||||
|
@ -44,6 +45,7 @@
|
|||
"raf": "3.4.0",
|
||||
"ramda": "^0.25.0",
|
||||
"react": "^16.3.2",
|
||||
"react-chartjs-2": "^2.7.4",
|
||||
"react-copy-to-clipboard": "^5.0.1",
|
||||
"react-datepicker": "^1.5.0",
|
||||
"react-dev-utils": "^5.0.1",
|
||||
|
|
|
@ -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();
|
33
yarn.lock
33
yarn.lock
|
@ -1437,6 +1437,26 @@ chardet@^0.4.0:
|
|||
version "0.4.2"
|
||||
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2"
|
||||
|
||||
chart.js@^2.7.2:
|
||||
version "2.7.2"
|
||||
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.7.2.tgz#3c9fde4dc5b95608211bdefeda7e5d33dffa5714"
|
||||
dependencies:
|
||||
chartjs-color "^2.1.0"
|
||||
moment "^2.10.2"
|
||||
|
||||
chartjs-color-string@^0.5.0:
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/chartjs-color-string/-/chartjs-color-string-0.5.0.tgz#8d3752d8581d86687c35bfe2cb80ac5213ceb8c1"
|
||||
dependencies:
|
||||
color-name "^1.0.0"
|
||||
|
||||
chartjs-color@^2.1.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/chartjs-color/-/chartjs-color-2.2.0.tgz#84a2fb755787ed85c39dd6dd8c7b1d88429baeae"
|
||||
dependencies:
|
||||
chartjs-color-string "^0.5.0"
|
||||
color-convert "^0.5.3"
|
||||
|
||||
cheerio@^1.0.0-rc.2:
|
||||
version "1.0.0-rc.2"
|
||||
resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.2.tgz#4b9f53a81b27e4d5dac31c0ffd0cfa03cc6830db"
|
||||
|
@ -1589,6 +1609,10 @@ collection-visit@^1.0.0:
|
|||
map-visit "^1.0.0"
|
||||
object-visit "^1.0.0"
|
||||
|
||||
color-convert@^0.5.3:
|
||||
version "0.5.3"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-0.5.3.tgz#bdb6c69ce660fadffe0b0007cc447e1b9f7282bd"
|
||||
|
||||
color-convert@^1.3.0, color-convert@^1.9.0:
|
||||
version "1.9.2"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.2.tgz#49881b8fba67df12a96bdf3f56c0aab9e7913147"
|
||||
|
@ -4905,7 +4929,7 @@ mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdi
|
|||
dependencies:
|
||||
minimist "0.0.8"
|
||||
|
||||
moment@^2.22.2:
|
||||
moment@^2.10.2, moment@^2.22.2:
|
||||
version "2.22.2"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66"
|
||||
|
||||
|
@ -6070,6 +6094,13 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.1.7:
|
|||
minimist "^1.2.0"
|
||||
strip-json-comments "~2.0.1"
|
||||
|
||||
react-chartjs-2@^2.7.4:
|
||||
version "2.7.4"
|
||||
resolved "https://registry.yarnpkg.com/react-chartjs-2/-/react-chartjs-2-2.7.4.tgz#e41ea4e81491dc78347111126a48e96ee57db1a6"
|
||||
dependencies:
|
||||
lodash "^4.17.4"
|
||||
prop-types "^15.5.8"
|
||||
|
||||
react-copy-to-clipboard@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.1.tgz#8eae107bb400be73132ed3b6a7b4fb156090208e"
|
||||
|
|
Loading…
Reference in a new issue