Merge pull request #99 from mikecao/dev

v0.21.0
This commit is contained in:
Mike Cao 2020-09-03 14:54:18 -07:00 committed by GitHub
commit 681852b225
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 471 additions and 602 deletions

View File

@ -82,6 +82,16 @@ To build the umami container and start up a Postgres database, run:
docker-compose up
```
### Getting updates
To get the latest features, simply do a pull, install any new dependencies, and rebuild:
```
git pull
npm install
npm run build
```
## License
MIT

View File

@ -1,19 +1,28 @@
import React from 'react';
import { useDispatch } from 'react-redux';
import React, { useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { setDateRange } from 'redux/actions/websites';
import Button from './Button';
import Refresh from 'assets/redo.svg';
import Dots from 'assets/ellipsis-h.svg';
import { useDateRange } from 'hooks/useDateRange';
import { getDateRange } from '../../lib/date';
export default function RefreshButton({ websiteId }) {
const dispatch = useDispatch();
const dateRange = useDateRange(websiteId);
const [loading, setLoading] = useState(false);
const completed = useSelector(state => state.queries[`/api/website/${websiteId}/metrics`]);
function handleClick() {
if (dateRange) {
dispatch(setDateRange(websiteId, dateRange));
setLoading(true);
dispatch(setDateRange(websiteId, getDateRange(dateRange.value)));
}
}
return <Button icon={<Refresh />} size="small" onClick={handleClick} />;
useEffect(() => {
setLoading(false);
}, [completed]);
return <Button icon={loading ? <Dots /> : <Refresh />} size="small" onClick={handleClick} />;
}

View File

@ -4,7 +4,7 @@ import classNames from 'classnames';
import ChartJS from 'chart.js';
import styles from './BarChart.module.css';
import { format } from 'date-fns';
import { formatLongNumber } from '../../lib/format';
import { formatLongNumber } from 'lib/format';
export default function BarChart({
chartId,
@ -22,30 +22,39 @@ export default function BarChart({
const chart = useRef();
const [tooltip, setTooltip] = useState({});
const renderXLabel = (label, index, values) => {
function renderXLabel(label, index, values) {
const d = new Date(values[index].value);
const n = records;
const w = canvas.current.width;
switch (unit) {
case 'hour':
return format(d, 'ha');
case 'day':
if (n >= 15) {
return index % ~~(n / 15) === 0 ? format(d, 'MMM d') : '';
if (records > 31) {
if (w <= 500) {
return index % 10 === 0 ? format(d, 'M/d') : '';
}
return index % 5 === 0 ? format(d, 'M/d') : '';
}
if (w <= 500) {
return index % 2 === 0 ? format(d, 'MMM d') : '';
}
return format(d, 'EEE M/d');
case 'month':
if (w <= 660) {
return format(d, 'MMM');
}
return format(d, 'MMMM');
default:
return label;
}
};
}
const renderYLabel = label => {
function renderYLabel(label) {
return +label > 1 ? formatLongNumber(label) : label;
};
}
const renderTooltip = model => {
function renderTooltip(model) {
const { opacity, title, body, labelColors } = model;
if (!opacity) {
@ -60,9 +69,9 @@ export default function BarChart({
labelColor: labelColors[0].backgroundColor,
});
}
};
}
const createChart = () => {
function createChart() {
const options = {
animation: {
duration: animationDuration,
@ -119,9 +128,9 @@ export default function BarChart({
},
options,
});
};
}
const updateChart = () => {
function updateChart() {
const { options } = chart.current;
options.scales.xAxes[0].time.unit = unit;
@ -129,7 +138,7 @@ export default function BarChart({
options.animation.duration = animationDuration;
onUpdate(chart.current);
};
}
useEffect(() => {
if (datasets) {

View File

@ -21,7 +21,7 @@ export default function WebsiteHeader({ websiteId, title, showLink = false }) {
<Button
icon={<Arrow />}
onClick={() =>
router.push('/website/[...id]', `/website/${websiteId}/${name}`, {
router.push('/website/[...id]', `/website/${websiteId}/${title}`, {
shallow: true,
})
}

View File

@ -1,7 +1,10 @@
import { useState, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { get } from 'lib/web';
import { updateQuery } from 'redux/actions/queries';
export default function useFetch(url, params = {}, options = {}) {
const dispatch = useDispatch();
const [data, setData] = useState();
const [error, setError] = useState();
const keys = Object.keys(params)
@ -12,7 +15,11 @@ export default function useFetch(url, params = {}, options = {}) {
async function loadData() {
try {
setError(null);
const time = performance.now();
const data = await get(url, params);
dispatch(updateQuery({ url, time: performance.now() - time, completed: Date.now() }));
setData(data);
onDataLoad(data);
} catch (e) {

View File

@ -34,7 +34,6 @@ export default prisma;
export async function runQuery(query) {
return query.catch(e => {
console.error(e);
throw e;
});
}

View File

@ -13,7 +13,11 @@ export const urlFilter = (data, { raw }) => {
const cleanUrl = url => {
try {
const { pathname, searchParams } = new URL(url);
const { pathname, search, searchParams } = new URL(url);
if (search.startsWith('?/')) {
return `${pathname}${search}`;
}
const path = removeTrailingSlash(pathname);
const ref = searchParams.get('ref');

View File

@ -252,8 +252,9 @@ export function getMetrics(website_id, start_at, end_at) {
const db = getDatabase();
if (db === POSTGRESQL) {
return prisma.$queryRaw(
`
return runQuery(
prisma.$queryRaw(
`
select sum(t.c) as "pageviews",
count(distinct t.session_id) as "uniques",
sum(case when t.c = 1 then 1 else 0 end) as "bounces",
@ -269,15 +270,17 @@ export function getMetrics(website_id, start_at, end_at) {
group by 1, 2
) t
`,
website_id,
start_at,
end_at,
website_id,
start_at,
end_at,
),
);
}
if (db === MYSQL) {
return prisma.$queryRaw(
`
return runQuery(
prisma.$queryRaw(
`
select sum(t.c) as "pageviews",
count(distinct t.session_id) as "uniques",
sum(case when t.c = 1 then 1 else 0 end) as "bounces",
@ -293,9 +296,10 @@ export function getMetrics(website_id, start_at, end_at) {
group by 1, 2
) t
`,
website_id,
start_at,
end_at,
website_id,
start_at,
end_at,
),
);
}
@ -313,8 +317,9 @@ export function getPageviews(
const db = getDatabase();
if (db === POSTGRESQL) {
return prisma.$queryRaw(
`
return runQuery(
prisma.$queryRaw(
`
select ${getDateQuery(db, 'created_at', unit, timezone)} t,
count(${count}) y
from pageview
@ -323,15 +328,17 @@ export function getPageviews(
group by 1
order by 1
`,
website_id,
start_at,
end_at,
website_id,
start_at,
end_at,
),
);
}
if (db === MYSQL) {
return prisma.$queryRaw(
`
return runQuery(
prisma.$queryRaw(
`
select ${getDateQuery(db, 'created_at', unit, timezone)} t,
count(${count}) y
from pageview
@ -340,9 +347,10 @@ export function getPageviews(
group by 1
order by 1
`,
website_id,
start_at,
end_at,
website_id,
start_at,
end_at,
),
);
}
@ -355,8 +363,9 @@ export function getRankings(website_id, start_at, end_at, type, table, domain) {
const filter = domain ? `and ${type} not like '%${domain}%'` : '';
if (db === POSTGRESQL) {
return prisma.$queryRaw(
`
return runQuery(
prisma.$queryRaw(
`
select distinct ${type} x, count(*) y
from ${table}
where website_id=$1
@ -365,15 +374,17 @@ export function getRankings(website_id, start_at, end_at, type, table, domain) {
group by 1
order by 2 desc
`,
website_id,
start_at,
end_at,
website_id,
start_at,
end_at,
),
);
}
if (db === MYSQL) {
return prisma.$queryRaw(
`
return runQuery(
prisma.$queryRaw(
`
select distinct ${type} x, count(*) y
from ${table}
where website_id=?
@ -382,9 +393,10 @@ export function getRankings(website_id, start_at, end_at, type, table, domain) {
group by 1
order by 2 desc
`,
website_id,
start_at,
end_at,
website_id,
start_at,
end_at,
),
);
}
@ -396,28 +408,32 @@ export function getActiveVisitors(website_id) {
const date = subMinutes(new Date(), 5);
if (db === POSTGRESQL) {
return prisma.$queryRaw(
`
return runQuery(
prisma.$queryRaw(
`
select count(distinct session_id) x
from pageview
where website_id=$1
and created_at >= $2
`,
website_id,
date,
website_id,
date,
),
);
}
if (db === MYSQL) {
return prisma.$queryRaw(
`
return runQuery(
prisma.$queryRaw(
`
select count(distinct session_id) x
from pageview
where website_id=?
and created_at >= ?
`,
website_id,
date,
website_id,
date,
),
);
}
@ -428,8 +444,9 @@ export function getEvents(website_id, start_at, end_at, timezone = 'utc', unit =
const db = getDatabase();
if (db === POSTGRESQL) {
return prisma.$queryRaw(
`
return runQuery(
prisma.$queryRaw(
`
select
event_value x,
${getDateQuery(db, 'created_at', unit, timezone)} t,
@ -440,15 +457,17 @@ export function getEvents(website_id, start_at, end_at, timezone = 'utc', unit =
group by 1, 2
order by 2
`,
website_id,
start_at,
end_at,
website_id,
start_at,
end_at,
),
);
}
if (db === MYSQL) {
return prisma.$queryRaw(
`
return runQuery(
prisma.$queryRaw(
`
select
event_value x,
${getDateQuery(db, 'created_at', unit, timezone)} t,
@ -459,9 +478,10 @@ export function getEvents(website_id, start_at, end_at, timezone = 'utc', unit =
group by 1, 2
order by 2
`,
website_id,
start_at,
end_at,
website_id,
start_at,
end_at,
),
);
}

View File

@ -1,6 +1,6 @@
{
"name": "umami",
"version": "0.20.0",
"version": "0.21.0",
"description": "A simple, fast, website analytics alternative to Google Analytics. ",
"author": "Mike Cao <mike@mikecao.com>",
"license": "MIT",
@ -39,14 +39,14 @@
}
},
"dependencies": {
"@prisma/client": "2.5.1",
"@prisma/client": "2.6.1",
"@reduxjs/toolkit": "^1.4.0",
"bcrypt": "^5.0.0",
"chart.js": "^2.9.3",
"classnames": "^2.2.6",
"cookie": "^0.4.1",
"cors": "^2.8.5",
"date-fns": "^2.16.0",
"date-fns": "^2.16.1",
"date-fns-tz": "^1.0.10",
"detect-browser": "^5.1.1",
"dotenv": "^8.2.0",
@ -57,7 +57,7 @@
"jose": "^1.28.0",
"maxmind": "^4.1.4",
"moment-timezone": "^0.5.31",
"next": "^9.5.2",
"next": "^9.5.3",
"promise-polyfill": "^8.1.3",
"react": "^16.13.1",
"react-dom": "^16.13.1",
@ -75,30 +75,30 @@
"uuid": "^8.3.0"
},
"devDependencies": {
"@prisma/cli": "2.5.1",
"@prisma/cli": "2.6.1",
"@rollup/plugin-buble": "^0.21.3",
"@rollup/plugin-node-resolve": "^9.0.0",
"@rollup/plugin-replace": "^2.3.3",
"@svgr/webpack": "^5.4.0",
"cross-env": "^7.0.2",
"dotenv-cli": "^3.2.0",
"eslint": "^7.7.0",
"eslint": "^7.8.1",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-react": "^7.20.6",
"eslint-plugin-react-hooks": "^4.1.0",
"husky": "^4.2.5",
"lint-staged": "^10.2.13",
"lint-staged": "^10.3.0",
"npm-run-all": "^4.1.5",
"postcss-flexbugs-fixes": "^4.2.1",
"postcss-import": "^12.0.1",
"postcss-preset-env": "^6.7.0",
"prettier": "^2.1.1",
"prettier-eslint": "^11.0.0",
"rollup": "^2.26.6",
"rollup": "^2.26.9",
"rollup-plugin-hashbang": "^2.2.2",
"rollup-plugin-terser": "^7.0.0",
"stylelint": "^13.6.0",
"rollup-plugin-terser": "^7.0.1",
"stylelint": "^13.7.0",
"stylelint-config-css-modules": "^2.2.0",
"stylelint-config-prettier": "^8.0.1",
"stylelint-config-recommended": "^3.0.0"

17
redux/actions/queries.js Normal file
View File

@ -0,0 +1,17 @@
import { createSlice } from '@reduxjs/toolkit';
const queries = createSlice({
name: 'queries',
initialState: {},
reducers: {
updateQuery(state, action) {
const { url, ...data } = action.payload;
state[url] = data;
return state;
},
},
});
export const { updateQuery } = queries.actions;
export default queries.reducer;

View File

@ -1,33 +1,29 @@
import { createSlice } from '@reduxjs/toolkit';
import produce from 'immer';
const websites = createSlice({
name: 'user',
name: 'websites',
initialState: {},
reducers: {
updateWebsites(state, action) {
state = action.payload;
return state;
},
updateWebsite(state, action) {
const { websiteId, ...data } = action.payload;
state[websiteId] = data;
return state;
},
},
});
export const { updateWebsites } = websites.actions;
export const { updateWebsites, updateWebsite } = websites.actions;
export default websites.reducer;
export function setDateRange(websiteId, dateRange) {
return (dispatch, getState) => {
const state = getState();
let { websites = {} } = state;
websites = produce(websites, draft => {
if (!draft[websiteId]) {
draft[websiteId] = {};
}
draft[websiteId].dateRange = { ...dateRange, modified: Date.now() };
});
return dispatch(updateWebsites(websites));
return dispatch => {
return dispatch(
updateWebsite({ websiteId, dateRange: { ...dateRange, modified: Date.now() } }),
);
};
}

View File

@ -1,5 +1,6 @@
import { combineReducers } from 'redux';
import user from './actions/user';
import websites from './actions/websites';
import queries from './actions/queries';
export default combineReducers({ user, websites });
export default combineReducers({ user, websites, queries });

View File

@ -61,12 +61,13 @@ import { removeTrailingSlash } from '../lib/url';
const handlePush = (state, title, navigatedURL) => {
removeEvents();
currentRef = currentUrl;
const newUrl = navigatedURL.toString();
if (navigatedURL.toString().startsWith('http')) {
const url = new URL(navigatedURL.toString());
if (newUrl.startsWith('http')) {
const url = new URL(newUrl);
currentUrl = `${url.pathname}${url.search}`;
} else {
currentUrl = navigatedURL.toString();
currentUrl = newUrl;
}
pageView();
@ -101,4 +102,8 @@ import { removeTrailingSlash } from '../lib/url';
/* Start */
pageView();
if (!window.umami) {
window.umami = event_value => collect('event', { event_type: 'custom', event_value });
}
})(window);

800
yarn.lock

File diff suppressed because it is too large Load Diff