Added useFetch hook. Updated database check.

This commit is contained in:
Mike Cao 2020-08-30 15:29:31 -07:00
parent 7a81dda7b6
commit d0ca0819c6
14 changed files with 146 additions and 237 deletions

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useState } from 'react';
import classNames from 'classnames';
import WebsiteChart from 'components/metrics/WebsiteChart';
import WorldMap from 'components/common/WorldMap';
@ -7,7 +7,6 @@ import WebsiteHeader from 'components/metrics/WebsiteHeader';
import MenuLayout from 'components/layout/MenuLayout';
import Button from 'components/common/Button';
import { getDateRange } from 'lib/date';
import { get } from 'lib/web';
import Arrow from 'assets/arrow-right.svg';
import styles from './WebsiteDetails.module.css';
import PagesTable from './metrics/PagesTable';
@ -18,15 +17,16 @@ import DevicesTable from './metrics/DevicesTable';
import CountriesTable from './metrics/CountriesTable';
import EventsTable from './metrics/EventsTable';
import EventsChart from './metrics/EventsChart';
import useFetch from '../hooks/useFetch';
export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' }) {
const [data, setData] = useState();
const [chartLoaded, setChartLoaded] = useState(false);
const [countryData, setCountryData] = useState();
const [eventsData, setEventsData] = useState();
const [dateRange, setDateRange] = useState(getDateRange(defaultDateRange));
const [expand, setExpand] = useState();
const [dateRange, setDateRange] = useState(getDateRange(defaultDateRange));
const { startDate, endDate, unit } = dateRange;
const { data } = useFetch(`/api/website/${websiteId}`, { websiteId });
const BackButton = () => (
<Button
@ -76,10 +76,6 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
return menuOptions.find(e => e.value === value);
}
async function loadData() {
setData(await get(`/api/website/${websiteId}`));
}
function handleDataLoad() {
if (!chartLoaded) setTimeout(() => setChartLoaded(true), 300);
}
@ -96,12 +92,6 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
setExpand(getSelectedMenuOption(value));
}
useEffect(() => {
if (websiteId) {
loadData();
}
}, [websiteId]);
if (!data) {
return null;
}

View File

@ -1,25 +1,17 @@
import React, { useState, useEffect } from 'react';
import React from 'react';
import { useRouter } from 'next/router';
import WebsiteHeader from 'components/metrics/WebsiteHeader';
import WebsiteChart from 'components/metrics/WebsiteChart';
import Page from 'components/layout/Page';
import Button from 'components/common/Button';
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
import useFetch from 'hooks/useFetch';
import Arrow from 'assets/arrow-right.svg';
import { get } from 'lib/web';
import styles from './WebsiteList.module.css';
export default function WebsiteList() {
const [data, setData] = useState();
const router = useRouter();
async function loadData() {
setData(await get(`/api/websites`));
}
useEffect(() => {
loadData();
}, []);
const { data } = useFetch('/api/websites');
if (!data) {
return null;

View File

@ -1,32 +1,20 @@
import React, { useState, useEffect } from 'react';
import React, { useMemo } from 'react';
import { useSpring, animated } from 'react-spring';
import classNames from 'classnames';
import { get } from 'lib/web';
import useFetch from 'hooks/useFetch';
import styles from './ActiveUsers.module.css';
export default function ActiveUsers({ websiteId, className }) {
const [count, setCount] = useState(0);
async function loadData() {
const result = await get(`/api/website/${websiteId}/active`);
setCount(result?.[0]?.x);
}
const { data } = useFetch(`/api/website/${websiteId}/active`, {}, { interval: 60000 });
const count = useMemo(() => {
return data?.[0]?.x || 0;
}, [data]);
const props = useSpring({
x: count,
from: { x: 0 },
});
useEffect(() => {
loadData();
const id = setInterval(() => loadData(), 60000);
return () => {
clearInterval(id);
};
}, []);
if (count === 0) {
return null;
}

View File

@ -1,10 +1,8 @@
import React, { useState, useEffect, useMemo } from 'react';
import classNames from 'classnames';
import React, { useMemo } from 'react';
import tinycolor from 'tinycolor2';
import BarChart from './BarChart';
import { get } from 'lib/web';
import { getTimezone, getDateArray, getDateLength } from 'lib/date';
import styles from './BarChart.module.css';
import useFetch from 'hooks/useFetch';
const COLORS = [
'#2680eb',
@ -18,31 +16,15 @@ const COLORS = [
];
export default function EventsChart({ websiteId, startDate, endDate, unit }) {
const [data, setData] = useState();
const { data } = useFetch(`/api/website/${websiteId}/events`, {
start_at: +startDate,
end_at: +endDate,
unit,
tz: getTimezone(),
});
const datasets = useMemo(() => {
if (!data) return [];
return Object.keys(data).map((key, index) => {
const color = tinycolor(COLORS[index]);
return {
label: key,
data: data[key],
lineTension: 0,
backgroundColor: color.setAlpha(0.4).toRgbString(),
borderColor: color.setAlpha(0.5).toRgbString(),
borderWidth: 1,
};
});
}, [data]);
async function loadData() {
const data = await get(`/api/website/${websiteId}/events`, {
start_at: +startDate,
end_at: +endDate,
unit,
tz: getTimezone(),
});
const map = data.reduce((obj, { x, t, y }) => {
if (!obj[x]) {
obj[x] = [];
@ -57,8 +39,18 @@ export default function EventsChart({ websiteId, startDate, endDate, unit }) {
map[key] = getDateArray(map[key], startDate, endDate, unit);
});
setData(map);
}
return Object.keys(map).map((key, index) => {
const color = tinycolor(COLORS[index]);
return {
label: key,
data: map[key],
lineTension: 0,
backgroundColor: color.setAlpha(0.4).toRgbString(),
borderColor: color.setAlpha(0.5).toRgbString(),
borderWidth: 1,
};
});
}, [data]);
function handleCreate(options) {
const legend = {
@ -74,10 +66,6 @@ export default function EventsChart({ websiteId, startDate, endDate, unit }) {
chart.update();
}
useEffect(() => {
loadData();
}, [websiteId, startDate, endDate]);
if (!data) {
return null;
}

View File

@ -1,33 +1,28 @@
import React, { useState, useEffect } from 'react';
import React, { useState } from 'react';
import classNames from 'classnames';
import MetricCard from './MetricCard';
import { get } from 'lib/web';
import { formatShortTime, formatNumber, formatLongNumber } from 'lib/format';
import useFetch from 'hooks/useFetch';
import styles from './MetricsBar.module.css';
export default function MetricsBar({ websiteId, startDate, endDate, className }) {
const [data, setData] = useState({});
const { data } = useFetch(`/api/website/${websiteId}/metrics`, {
start_at: +startDate,
end_at: +endDate,
});
const [format, setFormat] = useState(true);
const { pageviews, uniques, bounces, totaltime } = data;
const formatFunc = format ? formatLongNumber : formatNumber;
async function loadData() {
setData(
await get(`/api/website/${websiteId}/metrics`, {
start_at: +startDate,
end_at: +endDate,
}),
);
}
function handleSetFormat() {
setFormat(state => !state);
}
useEffect(() => {
loadData();
}, [websiteId, startDate, endDate]);
if (!data) {
return null;
}
const { pageviews, uniques, bounces, totaltime } = data;
return (
<div className={classNames(styles.bar, className)} onClick={handleSetFormat}>

View File

@ -1,14 +1,14 @@
import React, { useState, useEffect, useMemo } from 'react';
import React, { useState, useMemo } from 'react';
import { FixedSizeList } from 'react-window';
import { useSpring, animated, config } from 'react-spring';
import classNames from 'classnames';
import Button from 'components/common/Button';
import Loading from 'components/common/Loading';
import useFetch from 'hooks/useFetch';
import Arrow from 'assets/arrow-right.svg';
import { get } from 'lib/web';
import { percentFilter } from 'lib/filters';
import { formatNumber, formatLongNumber } from 'lib/format';
import styles from './MetricsTable.module.css';
import Loading from '../common/Loading';
export default function MetricsTable({
title,
@ -27,7 +27,16 @@ export default function MetricsTable({
onDataLoad = () => {},
onExpand = () => {},
}) {
const [data, setData] = useState();
const { data } = useFetch(
`/api/website/${websiteId}/rankings`,
{
type,
start_at: +startDate,
end_at: +endDate,
domain: websiteDomain,
},
{ onDataLoad },
);
const [format, setFormat] = useState(true);
const formatFunc = format ? formatLongNumber : formatNumber;
const shouldAnimate = limit > 0;
@ -43,18 +52,6 @@ export default function MetricsTable({
return [];
}, [data, dataFilter, filterOptions]);
async function loadData() {
const data = await get(`/api/website/${websiteId}/rankings`, {
type,
start_at: +startDate,
end_at: +endDate,
domain: websiteDomain,
});
setData(data);
onDataLoad(data);
}
const handleSetFormat = () => setFormat(state => !state);
const getRow = row => {
@ -76,12 +73,6 @@ export default function MetricsTable({
return <div style={style}>{getRow(rankings[index])}</div>;
};
useEffect(() => {
if (websiteId) {
loadData();
}
}, [websiteId, startDate, endDate, type]);
return (
<div className={classNames(styles.container, className)}>
{data ? (

View File

@ -1,11 +1,11 @@
import React, { useState, useEffect, useMemo } from 'react';
import React, { useState, useMemo } from 'react';
import classNames from 'classnames';
import PageviewsChart from './PageviewsChart';
import MetricsBar from './MetricsBar';
import QuickButtons from './QuickButtons';
import DateFilter from '../common/DateFilter';
import StickyHeader from '../helpers/StickyHeader';
import { get } from 'lib/web';
import DateFilter from 'components/common/DateFilter';
import StickyHeader from 'components/helpers/StickyHeader';
import useFetch from 'hooks/useFetch';
import { getDateArray, getDateRange, getTimezone } from 'lib/date';
import styles from './WebsiteChart.module.css';
@ -16,9 +16,18 @@ export default function WebsiteChart({
onDataLoad = () => {},
onDateChange = () => {},
}) {
const [data, setData] = useState();
const [dateRange, setDateRange] = useState(getDateRange(defaultDateRange));
const { startDate, endDate, unit, value } = dateRange;
const { data } = useFetch(
`/api/website/${websiteId}/pageviews`,
{
start_at: +startDate,
end_at: +endDate,
unit,
tz: getTimezone(),
},
{ onDataLoad },
);
const [pageviews, uniques] = useMemo(() => {
if (data) {
@ -35,22 +44,6 @@ export default function WebsiteChart({
onDateChange(values);
}
async function loadData() {
const data = await get(`/api/website/${websiteId}/pageviews`, {
start_at: +startDate,
end_at: +endDate,
unit,
tz: getTimezone(),
});
setData(data);
onDataLoad(data);
}
useEffect(() => {
loadData();
}, [websiteId, startDate, endDate, unit]);
return (
<>
<div className={classNames(styles.header, 'row')}>

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState } from 'react';
import classNames from 'classnames';
import PageHeader from 'components/layout/PageHeader';
import Button from 'components/common/Button';
@ -7,20 +7,20 @@ import Table from 'components/common/Table';
import Modal from 'components/common/Modal';
import AccountEditForm from 'components/forms/AccountEditForm';
import ButtonLayout from 'components/layout/ButtonLayout';
import DeleteForm from 'components/forms/DeleteForm';
import useFetch from 'hooks/useFetch';
import Pen from 'assets/pen.svg';
import Plus from 'assets/plus.svg';
import Trash from 'assets/trash.svg';
import Check from 'assets/check.svg';
import { get } from 'lib/web';
import styles from './AccountSettings.module.css';
import DeleteForm from '../forms/DeleteForm';
export default function AccountSettings() {
const [data, setData] = useState();
const [addAccount, setAddAccount] = useState();
const [editAccount, setEditAccount] = useState();
const [deleteAccount, setDeleteAccount] = useState();
const [saved, setSaved] = useState(0);
const { data } = useFetch(`/api/accounts`, {}, { update: [saved] });
const Checkmark = ({ is_admin }) => (is_admin ? <Icon icon={<Check />} size="medium" /> : null);
@ -61,14 +61,6 @@ export default function AccountSettings() {
setDeleteAccount(null);
}
async function loadData() {
setData(await get(`/api/accounts`));
}
useEffect(() => {
loadData();
}, [saved]);
if (!data) {
return null;
}

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState } from 'react';
import classNames from 'classnames';
import Table from 'components/common/Table';
import Button from 'components/common/Button';
@ -15,17 +15,17 @@ import Trash from 'assets/trash.svg';
import Plus from 'assets/plus.svg';
import Code from 'assets/code.svg';
import Link from 'assets/link.svg';
import { get } from 'lib/web';
import styles from './WebsiteSettings.module.css';
import useFetch from '../../hooks/useFetch';
export default function WebsiteSettings() {
const [data, setData] = useState();
const [editWebsite, setEditWebsite] = useState();
const [deleteWebsite, setDeleteWebsite] = useState();
const [addWebsite, setAddWebsite] = useState();
const [showCode, setShowCode] = useState();
const [showUrl, setShowUrl] = useState();
const [saved, setSaved] = useState(0);
const { data } = useFetch(`/api/websites`, {}, { update: [saved] });
const Buttons = row => (
<ButtonLayout>
@ -77,14 +77,6 @@ export default function WebsiteSettings() {
setShowUrl(null);
}
async function loadData() {
setData(await get(`/api/websites`));
}
useEffect(() => {
loadData();
}, [saved]);
if (!data) {
return null;
}

39
hooks/useFetch.js Normal file
View File

@ -0,0 +1,39 @@
import { useState, useEffect } from 'react';
import { get } from 'lib/web';
export default function useFetch(url, params = {}, options = {}) {
const [data, setData] = useState();
const [error, setError] = useState();
const keys = Object.keys(params)
.sort()
.map(key => params[key]);
const { update = [], onDataLoad = () => {} } = options;
async function loadData() {
try {
setError(null);
const data = await get(url, params);
setData(data);
onDataLoad(data);
} catch (e) {
console.error(e);
setError(e);
}
}
useEffect(() => {
if (url) {
const { interval } = options;
loadData();
const id = interval ? setInterval(() => loadData(), interval) : null;
return () => {
clearInterval(id);
};
}
}, [url, ...keys, ...update]);
return { data, error };
}

View File

@ -1,39 +1,30 @@
export const AUTH_COOKIE_NAME = 'umami.auth';
export const POSTGRESQL = 'postgresql';
export const MYSQL = 'mysql';
export const MYSQL_DATE_FORMATS = {
minute: '%Y-%m-%d %H:%i:00',
hour: '%Y-%m-%d %H:00:00',
day: '%Y-%m-%d',
month: '%Y-%m-01',
year: '%Y-01-01',
};
export const POSTGRESQL_DATE_FORMATS = {
minute: 'YYYY-MM-DD HH24:MI:00',
hour: 'YYYY-MM-DD HH24:00:00',
day: 'YYYY-MM-DD',
month: 'YYYY-MM-01',
year: 'YYYY-01-01',
};
export const DOMAIN_REGEX = /((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}/;
export const DESKTOP_SCREEN_WIDTH = 1920;
export const LAPTOP_SCREEN_WIDTH = 1024;
export const MOBILE_SCREEN_WIDTH = 479;
export const OPERATING_SYSTEMS = [
'iOS',
'Android OS',
'BlackBerry OS',
'Windows Mobile',
'Amazon OS',
'Windows 3.11',
'Windows 95',
'Windows 98',
'Windows 2000',
'Windows XP',
'Windows Server 2003',
'Windows Vista',
'Windows 7',
'Windows 8',
'Windows 8.1',
'Windows 10',
'Windows ME',
'Open BSD',
'Sun OS',
'Linux',
'Mac OS',
'QNX',
'BeOS',
'OS/2',
'Chrome OS',
];
export const DESKTOP_OS = [
'Windows 3.11',
'Windows 95',

View File

@ -1,25 +1,7 @@
import moment from 'moment-timezone';
import prisma, { runQuery } from 'lib/db';
import { subMinutes } from 'date-fns';
const POSTGRESQL = 'postgresql';
const MYSQL = 'mysql';
const MYSQL_DATE_FORMATS = {
minute: '%Y-%m-%d %H:%i:00',
hour: '%Y-%m-%d %H:00:00',
day: '%Y-%m-%d',
month: '%Y-%m-01',
year: '%Y-01-01',
};
const POSTGRESQL_DATE_FORMATS = {
minute: 'YYYY-MM-DD HH24:MI:00',
hour: 'YYYY-MM-DD HH24:00:00',
day: 'YYYY-MM-DD',
month: 'YYYY-MM-01',
year: 'YYYY-01-01',
};
import { MYSQL, POSTGRESQL, MYSQL_DATE_FORMATS, POSTGRESQL_DATE_FORMATS } from 'lib/constants';
export function getDatabase() {
return (

View File

@ -1,46 +1,22 @@
import React, { useState, useEffect } from 'react';
import React from 'react';
import { useRouter } from 'next/router';
import Layout from 'components/layout/Layout';
import WebsiteDetails from 'components/WebsiteDetails';
import NotFound from 'pages/404';
import { get } from 'lib/web';
import useFetch from 'hooks/useFetch';
export default function SharePage() {
const [loading, setLoading] = useState(true);
const [websiteId, setWebsiteId] = useState();
const [notFound, setNotFound] = useState(false);
const router = useRouter();
const { id } = router.query;
const shareId = id?.[0];
const { data } = useFetch(shareId ? `/api/share/${shareId}` : null);
async function loadData() {
const website = await get(`/api/share/${id?.[0]}`);
if (website) {
setWebsiteId(website.website_id);
} else if (typeof window !== 'undefined') {
setNotFound(true);
}
}
useEffect(() => {
if (id) {
loadData().finally(() => {
setLoading(false);
});
} else {
setLoading(false);
}
}, [id]);
if (loading) return null;
if (!id || notFound) {
return <NotFound />;
if (!data) {
return null;
}
return (
<Layout>
<WebsiteDetails websiteId={websiteId} />
<WebsiteDetails websiteId={data.website_id} />
</Layout>
);
}

View File

@ -5,8 +5,8 @@ const path = require('path');
const databaseType =
process.env.DATABASE_TYPE || (process.env.DATABASE_URL && process.env.DATABASE_URL.split(':')[0]);
if (!databaseType) {
throw new Error('Database schema not specified');
if (!databaseType || !['mysql', 'postgresql'].includes(databaseType)) {
throw new Error('Missing or invalid database');
}
console.log(`Database schema detected: ${databaseType}`);