Updated fetch hook API.

This commit is contained in:
Mike Cao 2020-10-09 12:39:03 -07:00
commit 69b317386a
21 changed files with 112 additions and 76 deletions

7
components/common/Tag.js Normal file
View File

@ -0,0 +1,7 @@
import React from 'react';
import classNames from 'classnames';
import styles from './Tag.module.css';
export default function Tag({ className, children }) {
return <span className={classNames(styles.tag, className)}>{children}</span>;
}

View File

@ -1,4 +1,4 @@
.type { .tag {
font-size: var(--font-size-small); font-size: var(--font-size-small);
padding: 2px 4px; padding: 2px 4px;
border: 1px solid var(--gray300); border: 1px solid var(--gray300);

View File

@ -5,7 +5,10 @@ import useFetch from 'hooks/useFetch';
import styles from './ActiveUsers.module.css'; import styles from './ActiveUsers.module.css';
export default function ActiveUsers({ websiteId, token, className }) { export default function ActiveUsers({ websiteId, token, className }) {
const { data } = useFetch(`/api/website/${websiteId}/active`, { token }, { interval: 60000 }); const { data } = useFetch(`/api/website/${websiteId}/active`, {
params: { token },
interval: 60000,
});
const count = useMemo(() => { const count = useMemo(() => {
return data?.[0]?.x || 0; return data?.[0]?.x || 0;
}, [data]); }, [data]);

View File

@ -17,14 +17,16 @@ export default function EventsChart({ websiteId, token }) {
const { data } = useFetch( const { data } = useFetch(
`/api/website/${websiteId}/events`, `/api/website/${websiteId}/events`,
{ {
start_at: +startDate, params: {
end_at: +endDate, start_at: +startDate,
unit, end_at: +endDate,
tz: timezone, unit,
url: query.url, tz: timezone,
token, url: query.url,
token,
},
}, },
{ update: [modified] }, [modified],
); );
const datasets = useMemo(() => { const datasets = useMemo(() => {
if (!data) return []; if (!data) return [];

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import MetricsTable from './MetricsTable'; import MetricsTable from './MetricsTable';
import styles from './EventsTable.module.css'; import Tag from 'components/common/Tag';
export default function EventsTable({ websiteId, token, limit, onDataLoad }) { export default function EventsTable({ websiteId, token, limit, onDataLoad }) {
return ( return (
@ -22,7 +22,7 @@ const Label = ({ value }) => {
const [event, label] = value.split(':'); const [event, label] = value.split(':');
return ( return (
<> <>
<span className={styles.type}>{event}</span> <Tag>{event}</Tag>
{label} {label}
</> </>
); );

View File

@ -18,17 +18,19 @@ export default function MetricsBar({ websiteId, token, className }) {
query: { url }, query: { url },
} = usePageQuery(); } = usePageQuery();
console.log({ modified });
const { data, error, loading } = useFetch( const { data, error, loading } = useFetch(
`/api/website/${websiteId}/stats`, `/api/website/${websiteId}/stats`,
{ {
start_at: +startDate, params: {
end_at: +endDate, start_at: +startDate,
url, end_at: +endDate,
token, url,
}, token,
{ },
update: [modified],
}, },
[modified],
); );
const formatFunc = format ? formatLongNumber : formatNumber; const formatFunc = format ? formatLongNumber : formatNumber;

View File

@ -40,14 +40,18 @@ export default function MetricsTable({
const { data, loading, error } = useFetch( const { data, loading, error } = useFetch(
`/api/website/${websiteId}/metrics`, `/api/website/${websiteId}/metrics`,
{ {
type, params: {
start_at: +startDate, type,
end_at: +endDate, start_at: +startDate,
domain: websiteDomain, end_at: +endDate,
url, domain: websiteDomain,
token, url,
token,
},
onDataLoad,
delay: 300,
}, },
{ onDataLoad, delay: 300, update: [modified] }, [modified],
); );
const [format, setFormat] = useState(true); const [format, setFormat] = useState(true);
const formatFunc = format ? formatLongNumber : formatNumber; const formatFunc = format ? formatLongNumber : formatNumber;

View File

@ -4,7 +4,7 @@ import tinycolor from 'tinycolor2';
import CheckVisible from 'components/helpers/CheckVisible'; import CheckVisible from 'components/helpers/CheckVisible';
import BarChart from './BarChart'; import BarChart from './BarChart';
import useTheme from 'hooks/useTheme'; import useTheme from 'hooks/useTheme';
import { THEME_COLORS } from 'lib/constants'; import { THEME_COLORS, DEFAULT_ANIMATION_DURATION } from 'lib/constants';
export default function PageviewsChart({ export default function PageviewsChart({
websiteId, websiteId,
@ -13,7 +13,7 @@ export default function PageviewsChart({
records, records,
className, className,
loading, loading,
animationDuration = 300, animationDuration = DEFAULT_ANIMATION_DURATION,
}) { }) {
const intl = useIntl(); const intl = useIntl();
const [theme] = useTheme(); const [theme] = useTheme();

View File

@ -2,6 +2,7 @@ import React, { useMemo, useRef } from 'react';
import { format, parseISO, startOfMinute, subMinutes, isBefore } from 'date-fns'; import { format, parseISO, startOfMinute, subMinutes, isBefore } from 'date-fns';
import PageviewsChart from './PageviewsChart'; import PageviewsChart from './PageviewsChart';
import { getDateArray } from 'lib/date'; import { getDateArray } from 'lib/date';
import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
function mapData(data) { function mapData(data) {
let last = 0; let last = 0;
@ -44,7 +45,7 @@ export default function RealtimeChart({ data, unit, ...props }) {
prevEndDate.current = endDate; prevEndDate.current = endDate;
return 0; return 0;
} }
return 300; return DEFAULT_ANIMATION_DURATION;
}, [data]); }, [data]);
return ( return (

View File

@ -5,6 +5,7 @@ import firstBy from 'thenby';
import { format } from 'date-fns'; import { format } from 'date-fns';
import Icon from 'components/common/Icon'; import Icon from 'components/common/Icon';
import Table, { TableRow } from 'components/common/Table'; import Table, { TableRow } from 'components/common/Table';
import Tag from 'components/common/Tag';
import useLocale from 'hooks/useLocale'; import useLocale from 'hooks/useLocale';
import useCountryNames from 'hooks/useCountryNames'; import useCountryNames from 'hooks/useCountryNames';
import { BROWSERS } from 'lib/constants'; import { BROWSERS } from 'lib/constants';
@ -92,7 +93,7 @@ export default function RealtimeLog({ data, websites }) {
if (event_type) { if (event_type) {
return ( return (
<div> <div>
<span className={styles.event}>{event_type}</span> {event_value} <Tag>{event_type}</Tag> {event_value}
</div> </div>
); );
} }

View File

@ -12,14 +12,6 @@
overflow: auto; overflow: auto;
} }
.event {
font-size: var(--font-size-small);
padding: 2px 4px;
border: 1px solid var(--gray300);
border-radius: 4px;
margin-right: 10px;
}
.icon { .icon {
align-self: center; align-self: center;
margin-right: 20px; margin-right: 20px;

View File

@ -35,14 +35,17 @@ export default function WebsiteChart({
const { data, loading, error } = useFetch( const { data, loading, error } = useFetch(
`/api/website/${websiteId}/pageviews`, `/api/website/${websiteId}/pageviews`,
{ {
start_at: +startDate, params: {
end_at: +endDate, start_at: +startDate,
unit, end_at: +endDate,
tz: timezone, unit,
url, tz: timezone,
token, url,
token,
},
onDataLoad,
}, },
{ onDataLoad, update: [modified] }, [modified],
); );
const chartData = useMemo(() => { const chartData = useMemo(() => {

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useMemo } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { subMinutes, startOfMinute } from 'date-fns'; import { subMinutes, startOfMinute } from 'date-fns';
import Page from 'components/layout/Page'; import Page from 'components/layout/Page';
@ -18,19 +18,34 @@ function mergeData(state, data, time) {
.filter(({ created_at }) => new Date(created_at).getTime() >= time); .filter(({ created_at }) => new Date(created_at).getTime() >= time);
} }
function filterWebsite(data, id) {
return data.filter(({ website_id }) => website_id === id);
}
export default function RealtimeDashboard() { export default function RealtimeDashboard() {
const [data, setData] = useState(); const [data, setData] = useState();
const [website, setWebsite] = useState(); const [website, setWebsite] = useState();
const { data: init, loading } = useFetch('/api/realtime', { type: 'init' }); const { data: init, loading } = useFetch('/api/realtime', { params: { type: 'init' } });
const { data: updates } = useFetch( const { data: updates } = useFetch('/api/realtime', {
'/api/realtime', params: { type: 'update', start_at: data?.timestamp },
{ type: 'update', start_at: data?.timestamp }, disabled: !init?.token || !data,
{ interval: REALTIME_INTERVAL,
disabled: !init?.token || !data, headers: { 'x-umami-token': init?.token },
interval: REALTIME_INTERVAL, });
headers: { 'x-umami-token': init?.token },
}, const realtimeData = useMemo(() => {
); if (website) {
const { website_id } = website;
const { pageviews, sessions, events, ...props } = data;
return {
pageviews: filterWebsite(pageviews, website_id),
sessions: filterWebsite(sessions, website_id),
events: filterWebsite(events, website_id),
...props,
};
}
return data;
}, [data, website]);
useEffect(() => { useEffect(() => {
if (init && !data) { if (init && !data) {
@ -70,10 +85,15 @@ export default function RealtimeDashboard() {
</div> </div>
<DropDown value={selectedValue} options={options} onChange={handleSelect} /> <DropDown value={selectedValue} options={options} onChange={handleSelect} />
</PageHeader> </PageHeader>
<RealtimeChart websiteId={website?.website_id} data={data} unit="minute" records={30} /> <RealtimeChart
websiteId={website?.website_id}
data={realtimeData}
unit="minute"
records={REALTIME_RANGE}
/>
<div className="row"> <div className="row">
<div className="col-12"> <div className="col-12">
<RealtimeLog data={data} websites={websites} /> <RealtimeLog data={realtimeData} websites={websites} />
</div> </div>
</div> </div>
</Page> </Page>

View File

@ -31,7 +31,7 @@ const views = {
}; };
export default function WebsiteDetails({ websiteId, token }) { export default function WebsiteDetails({ websiteId, token }) {
const { data } = useFetch(`/api/website/${websiteId}`, { token }); const { data } = useFetch(`/api/website/${websiteId}`, { params: { token } });
const [chartLoaded, setChartLoaded] = useState(false); const [chartLoaded, setChartLoaded] = useState(false);
const [countryData, setCountryData] = useState(); const [countryData, setCountryData] = useState();
const [eventsData, setEventsData] = useState(); const [eventsData, setEventsData] = useState();

View File

@ -9,7 +9,7 @@ import Arrow from 'assets/arrow-right.svg';
import styles from './WebsiteList.module.css'; import styles from './WebsiteList.module.css';
export default function WebsiteList({ userId }) { export default function WebsiteList({ userId }) {
const { data } = useFetch('/api/websites', { user_id: userId }); const { data } = useFetch('/api/websites', { params: { user_id: userId } });
if (!data) { if (!data) {
return null; return null;

View File

@ -25,7 +25,7 @@ export default function AccountSettings() {
const [deleteAccount, setDeleteAccount] = useState(); const [deleteAccount, setDeleteAccount] = useState();
const [saved, setSaved] = useState(0); const [saved, setSaved] = useState(0);
const [message, setMessage] = useState(); const [message, setMessage] = useState();
const { data } = useFetch(`/api/accounts`, {}, { update: [saved] }); const { data } = useFetch(`/api/accounts`, {}, [saved]);
const Checkmark = ({ is_admin }) => (is_admin ? <Icon icon={<Check />} size="medium" /> : null); const Checkmark = ({ is_admin }) => (is_admin ? <Icon icon={<Check />} size="medium" /> : null);

View File

@ -29,7 +29,7 @@ export default function WebsiteSettings() {
const [showUrl, setShowUrl] = useState(); const [showUrl, setShowUrl] = useState();
const [saved, setSaved] = useState(0); const [saved, setSaved] = useState(0);
const [message, setMessage] = useState(); const [message, setMessage] = useState();
const { data } = useFetch(`/api/websites`, {}, { update: [saved] }); const { data } = useFetch(`/api/websites`, {}, [saved]);
const Buttons = row => ( const Buttons = row => (
<ButtonLayout align="right"> <ButtonLayout align="right">

View File

@ -4,14 +4,14 @@ import { get } from 'lib/web';
import { updateQuery } from 'redux/actions/queries'; import { updateQuery } from 'redux/actions/queries';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
export default function useFetch(url, params = {}, options = {}) { export default function useFetch(url, options = {}, update = []) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const [data, setData] = useState(); const [data, setData] = useState();
const [status, setStatus] = useState(); const [status, setStatus] = useState();
const [error, setError] = useState(); const [error, setError] = useState();
const [loading, setLoadiing] = useState(false); const [loading, setLoadiing] = useState(false);
const { basePath } = useRouter(); const { basePath } = useRouter();
const { update = [], onDataLoad = () => {}, disabled, headers, interval, delay = 0 } = options; const { params, disabled, headers, interval, delay = 0, onDataLoad } = options;
async function loadData() { async function loadData() {
try { try {
@ -30,7 +30,7 @@ export default function useFetch(url, params = {}, options = {}) {
} }
setStatus(status); setStatus(status);
onDataLoad(data); onDataLoad?.(data);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
setError(e); setError(e);
@ -41,17 +41,17 @@ export default function useFetch(url, params = {}, options = {}) {
useEffect(() => { useEffect(() => {
if (url && !disabled) { if (url && !disabled) {
if (!data) { setTimeout(() => loadData(), delay);
setTimeout(() => loadData(), delay);
}
const id = interval ? setInterval(() => loadData(), interval) : null;
return () => {
clearInterval(id);
};
} }
}, [data, url, disabled, ...update]); }, [url, disabled, ...update]);
useEffect(() => {
const id = interval ? setInterval(() => loadData(), interval) : null;
return () => {
clearInterval(id);
};
}, [interval, params]);
return { data, status, error, loading }; return { data, status, error, loading };
} }

View File

@ -51,6 +51,7 @@ export const EVENT_COLORS = [
'#ffec16', '#ffec16',
]; ];
export const DEFAULT_ANIMATION_DURATION = 300;
export const DEFAULT_DATE_RANGE = '24hour'; export const DEFAULT_DATE_RANGE = '24hour';
export const POSTGRESQL = 'postgresql'; export const POSTGRESQL = 'postgresql';

View File

@ -1,6 +1,6 @@
{ {
"name": "umami", "name": "umami",
"version": "0.82.0", "version": "0.88.0",
"description": "A simple, fast, website analytics alternative to Google Analytics. ", "description": "A simple, fast, website analytics alternative to Google Analytics. ",
"author": "Mike Cao <mike@mikecao.com>", "author": "Mike Cao <mike@mikecao.com>",
"license": "MIT", "license": "MIT",

View File

@ -8,7 +8,7 @@ export default function SharePage() {
const router = useRouter(); const router = useRouter();
const { id } = router.query; const { id } = router.query;
const shareId = id?.[0]; const shareId = id?.[0];
const { data } = useFetch(shareId ? `/api/share/${shareId}` : null); const { data } = useFetch(`/api/share/${shareId}`, { disabled: !shareId });
if (!data) { if (!data) {
return null; return null;