mirror of
https://github.com/kremalicious/umami.git
synced 2024-12-22 17:23:54 +01:00
Refactor components. Add refresh button.
This commit is contained in:
parent
d0ca0819c6
commit
d06c077019
1
assets/redo.svg
Normal file
1
assets/redo.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M500 8h-27.711c-6.739 0-12.157 5.548-11.997 12.286l2.347 98.568C418.075 51.834 341.788 7.73 255.207 8.001 118.82 8.428 7.787 120.009 8 256.396 8.214 393.181 119.165 504 256 504c63.926 0 122.202-24.187 166.178-63.908 5.113-4.618 5.354-12.561.482-17.433l-19.738-19.738c-4.498-4.498-11.753-4.785-16.501-.552C351.787 433.246 306.105 452 256 452c-108.322 0-196-87.662-196-196 0-108.322 87.662-196 196-196 79.545 0 147.941 47.282 178.675 115.302l-126.389-3.009c-6.737-.16-12.286 5.257-12.286 11.997V212c0 6.627 5.373 12 12 12h192c6.627 0 12-5.373 12-12V20c0-6.627-5.373-12-12-12z"/></svg>
|
After Width: | Height: | Size: 653 B |
@ -17,16 +17,18 @@ import DevicesTable from './metrics/DevicesTable';
|
|||||||
import CountriesTable from './metrics/CountriesTable';
|
import CountriesTable from './metrics/CountriesTable';
|
||||||
import EventsTable from './metrics/EventsTable';
|
import EventsTable from './metrics/EventsTable';
|
||||||
import EventsChart from './metrics/EventsChart';
|
import EventsChart from './metrics/EventsChart';
|
||||||
import useFetch from '../hooks/useFetch';
|
import useFetch from 'hooks/useFetch';
|
||||||
|
import Loading from 'components/common/Loading';
|
||||||
|
|
||||||
export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' }) {
|
export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' }) {
|
||||||
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();
|
||||||
const [expand, setExpand] = useState();
|
const [expand, setExpand] = useState();
|
||||||
|
const [refresh, setRefresh] = useState(0);
|
||||||
const [dateRange, setDateRange] = useState(getDateRange(defaultDateRange));
|
const [dateRange, setDateRange] = useState(getDateRange(defaultDateRange));
|
||||||
const { startDate, endDate, unit } = dateRange;
|
const { startDate, endDate, unit } = dateRange;
|
||||||
const { data } = useFetch(`/api/website/${websiteId}`, { websiteId });
|
const { data } = useFetch(`/api/website/${websiteId}`);
|
||||||
|
|
||||||
const BackButton = () => (
|
const BackButton = () => (
|
||||||
<Button
|
<Button
|
||||||
@ -92,6 +94,10 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
|
|||||||
setExpand(getSelectedMenuOption(value));
|
setExpand(getSelectedMenuOption(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleRefresh() {
|
||||||
|
setRefresh(state => state + 1);
|
||||||
|
}
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -100,8 +106,14 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
|
|||||||
<Page>
|
<Page>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className={classNames(styles.chart, 'col')}>
|
<div className={classNames(styles.chart, 'col')}>
|
||||||
<WebsiteHeader websiteId={websiteId} name={data.name} showLink={false} />
|
<WebsiteHeader
|
||||||
|
websiteId={websiteId}
|
||||||
|
title={data.name}
|
||||||
|
onRefresh={handleRefresh}
|
||||||
|
showLink={false}
|
||||||
|
/>
|
||||||
<WebsiteChart
|
<WebsiteChart
|
||||||
|
key={refresh}
|
||||||
websiteId={websiteId}
|
websiteId={websiteId}
|
||||||
onDataLoad={handleDataLoad}
|
onDataLoad={handleDataLoad}
|
||||||
onDateChange={handleDateChange}
|
onDateChange={handleDateChange}
|
||||||
@ -109,6 +121,7 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{!chartLoaded && <Loading />}
|
||||||
{chartLoaded && !expand && (
|
{chartLoaded && !expand && (
|
||||||
<>
|
<>
|
||||||
<div className={classNames(styles.row, 'row')}>
|
<div className={classNames(styles.row, 'row')}>
|
||||||
|
@ -19,10 +19,10 @@ export default function WebsiteList() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
{data?.map(({ website_id, name }) => (
|
{data.map(({ website_id, name }) => (
|
||||||
<div key={website_id} className={styles.website}>
|
<div key={website_id} className={styles.website}>
|
||||||
<WebsiteHeader websiteId={website_id} name={name} showLink />
|
<WebsiteHeader websiteId={website_id} title={name} showLink />
|
||||||
<WebsiteChart key={website_id} title={name} websiteId={website_id} />
|
<WebsiteChart websiteId={website_id} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{data.length === 0 && (
|
{data.length === 0 && (
|
||||||
|
7
components/common/RefreshButton.js
Normal file
7
components/common/RefreshButton.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Button from './Button';
|
||||||
|
import Refresh from 'assets/redo.svg';
|
||||||
|
|
||||||
|
export default function RefreshButton({ onClick }) {
|
||||||
|
return <Button icon={<Refresh />} size="small" onClick={onClick} />;
|
||||||
|
}
|
@ -4,6 +4,7 @@ import classNames from 'classnames';
|
|||||||
import ChartJS from 'chart.js';
|
import ChartJS from 'chart.js';
|
||||||
import styles from './BarChart.module.css';
|
import styles from './BarChart.module.css';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
|
import { formatLongNumber } from '../../lib/format';
|
||||||
|
|
||||||
export default function BarChart({
|
export default function BarChart({
|
||||||
chartId,
|
chartId,
|
||||||
@ -21,7 +22,7 @@ export default function BarChart({
|
|||||||
const chart = useRef();
|
const chart = useRef();
|
||||||
const [tooltip, setTooltip] = useState({});
|
const [tooltip, setTooltip] = useState({});
|
||||||
|
|
||||||
const renderLabel = (label, index, values) => {
|
const renderXLabel = (label, index, values) => {
|
||||||
const d = new Date(values[index].value);
|
const d = new Date(values[index].value);
|
||||||
const n = records;
|
const n = records;
|
||||||
|
|
||||||
@ -40,6 +41,10 @@ export default function BarChart({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderYLabel = label => {
|
||||||
|
return +label > 1 ? formatLongNumber(label) : label;
|
||||||
|
};
|
||||||
|
|
||||||
const renderTooltip = model => {
|
const renderTooltip = model => {
|
||||||
const { opacity, title, body, labelColors } = model;
|
const { opacity, title, body, labelColors } = model;
|
||||||
|
|
||||||
@ -82,7 +87,7 @@ export default function BarChart({
|
|||||||
tooltipFormat: 'ddd MMMM DD YYYY',
|
tooltipFormat: 'ddd MMMM DD YYYY',
|
||||||
},
|
},
|
||||||
ticks: {
|
ticks: {
|
||||||
callback: renderLabel,
|
callback: renderXLabel,
|
||||||
minRotation: 0,
|
minRotation: 0,
|
||||||
maxRotation: 0,
|
maxRotation: 0,
|
||||||
},
|
},
|
||||||
@ -96,6 +101,7 @@ export default function BarChart({
|
|||||||
yAxes: [
|
yAxes: [
|
||||||
{
|
{
|
||||||
ticks: {
|
ticks: {
|
||||||
|
callback: renderYLabel,
|
||||||
beginAtZero: true,
|
beginAtZero: true,
|
||||||
},
|
},
|
||||||
stacked,
|
stacked,
|
||||||
@ -119,7 +125,7 @@ export default function BarChart({
|
|||||||
const { options } = chart.current;
|
const { options } = chart.current;
|
||||||
|
|
||||||
options.scales.xAxes[0].time.unit = unit;
|
options.scales.xAxes[0].time.unit = unit;
|
||||||
options.scales.xAxes[0].ticks.callback = renderLabel;
|
options.scales.xAxes[0].ticks.callback = renderXLabel;
|
||||||
options.animation.duration = animationDuration;
|
options.animation.duration = animationDuration;
|
||||||
|
|
||||||
onUpdate(chart.current);
|
onUpdate(chart.current);
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import MetricCard from './MetricCard';
|
import MetricCard from './MetricCard';
|
||||||
import { formatShortTime, formatNumber, formatLongNumber } from 'lib/format';
|
import Loading from 'components/common/Loading';
|
||||||
import useFetch from 'hooks/useFetch';
|
import useFetch from 'hooks/useFetch';
|
||||||
|
import { formatShortTime, formatNumber, formatLongNumber } from 'lib/format';
|
||||||
import styles from './MetricsBar.module.css';
|
import styles from './MetricsBar.module.css';
|
||||||
|
|
||||||
export default function MetricsBar({ websiteId, startDate, endDate, className }) {
|
export default function MetricsBar({ websiteId, startDate, endDate, className }) {
|
||||||
@ -18,26 +19,28 @@ export default function MetricsBar({ websiteId, startDate, endDate, className })
|
|||||||
setFormat(state => !state);
|
setFormat(state => !state);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data) {
|
const { pageviews, uniques, bounces, totaltime } = data || {};
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { pageviews, uniques, bounces, totaltime } = data;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames(styles.bar, className)} onClick={handleSetFormat}>
|
<div className={classNames(styles.bar, className)} onClick={handleSetFormat}>
|
||||||
<MetricCard label="Views" value={pageviews} format={formatFunc} />
|
{!data ? (
|
||||||
<MetricCard label="Visitors" value={uniques} format={formatFunc} />
|
<Loading />
|
||||||
<MetricCard
|
) : (
|
||||||
label="Bounce rate"
|
<>
|
||||||
value={pageviews ? (bounces / pageviews) * 100 : 0}
|
<MetricCard label="Views" value={pageviews} format={formatFunc} />
|
||||||
format={n => Number(n).toFixed(0) + '%'}
|
<MetricCard label="Visitors" value={uniques} format={formatFunc} />
|
||||||
/>
|
<MetricCard
|
||||||
<MetricCard
|
label="Bounce rate"
|
||||||
label="Average visit time"
|
value={pageviews ? (bounces / pageviews) * 100 : 0}
|
||||||
value={totaltime && pageviews ? totaltime / (pageviews - bounces) : 0}
|
format={n => Number(n).toFixed(0) + '%'}
|
||||||
format={n => formatShortTime(n, ['m', 's'], ' ')}
|
/>
|
||||||
/>
|
<MetricCard
|
||||||
|
label="Average visit time"
|
||||||
|
value={totaltime && pageviews ? totaltime / (pageviews - bounces) : 0}
|
||||||
|
format={n => formatShortTime(n, ['m', 's'], ' ')}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 992px) {
|
@media only screen and (max-width: 992px) {
|
||||||
.container > div:last-child {
|
.bar > div:last-child {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import CheckVisible from 'components/helpers/CheckVisible';
|
import CheckVisible from 'components/helpers/CheckVisible';
|
||||||
import BarChart from './BarChart';
|
import BarChart from './BarChart';
|
||||||
|
import { getDateLength } from '../../lib/date';
|
||||||
|
|
||||||
export default function PageviewsChart({ websiteId, data, unit, className }) {
|
export default function PageviewsChart({ websiteId, data, startDate, endDate, unit, className }) {
|
||||||
const handleUpdate = chart => {
|
const handleUpdate = chart => {
|
||||||
const {
|
const {
|
||||||
data: { datasets },
|
data: { datasets },
|
||||||
@ -43,7 +44,7 @@ export default function PageviewsChart({ websiteId, data, unit, className }) {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
unit={unit}
|
unit={unit}
|
||||||
records={data.pageviews.length}
|
records={getDateLength(startDate, endDate, unit)}
|
||||||
animationDuration={visible ? 300 : 0}
|
animationDuration={visible ? 300 : 0}
|
||||||
onUpdate={handleUpdate}
|
onUpdate={handleUpdate}
|
||||||
/>
|
/>
|
||||||
|
@ -1,38 +1,36 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import PageHeader from 'components/layout/PageHeader';
|
import PageHeader from 'components/layout/PageHeader';
|
||||||
import Link from 'components/common/Link';
|
|
||||||
import Button from 'components/common/Button';
|
import Button from 'components/common/Button';
|
||||||
import ActiveUsers from './ActiveUsers';
|
import ActiveUsers from './ActiveUsers';
|
||||||
import Arrow from 'assets/arrow-right.svg';
|
import Arrow from 'assets/arrow-right.svg';
|
||||||
import styles from './WebsiteHeader.module.css';
|
import styles from './WebsiteHeader.module.css';
|
||||||
|
import RefreshButton from '../common/RefreshButton';
|
||||||
|
import ButtonLayout from '../layout/ButtonLayout';
|
||||||
|
|
||||||
export default function WebsiteHeader({ websiteId, name, showLink = false }) {
|
export default function WebsiteHeader({ websiteId, title, showLink = false, onRefresh }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
{showLink ? (
|
<div className={styles.title}>{title}</div>
|
||||||
<Link href="/website/[...id]" as={`/website/${websiteId}/${name}`} className={styles.title}>
|
|
||||||
{name}
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<div className={styles.title}>{name}</div>
|
|
||||||
)}
|
|
||||||
<ActiveUsers className={styles.active} websiteId={websiteId} />
|
<ActiveUsers className={styles.active} websiteId={websiteId} />
|
||||||
{showLink && (
|
<ButtonLayout>
|
||||||
<Button
|
<RefreshButton onClick={onRefresh} />
|
||||||
icon={<Arrow />}
|
{showLink && (
|
||||||
onClick={() =>
|
<Button
|
||||||
router.push('/website/[...id]', `/website/${websiteId}/${name}`, {
|
icon={<Arrow />}
|
||||||
shallow: true,
|
onClick={() =>
|
||||||
})
|
router.push('/website/[...id]', `/website/${websiteId}/${name}`, {
|
||||||
}
|
shallow: true,
|
||||||
size="small"
|
})
|
||||||
>
|
}
|
||||||
<div>View details</div>
|
size="small"
|
||||||
</Button>
|
>
|
||||||
)}
|
<div>View details</div>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ButtonLayout>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -35,5 +35,5 @@ export default function useFetch(url, params = {}, options = {}) {
|
|||||||
}
|
}
|
||||||
}, [url, ...keys, ...update]);
|
}, [url, ...keys, ...update]);
|
||||||
|
|
||||||
return { data, error };
|
return { data, error, loadData };
|
||||||
}
|
}
|
||||||
|
@ -30,8 +30,7 @@
|
|||||||
],
|
],
|
||||||
"**/*.css": [
|
"**/*.css": [
|
||||||
"stylelint --fix",
|
"stylelint --fix",
|
||||||
"prettier --write",
|
"prettier --write"
|
||||||
"eslint"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"husky": {
|
"husky": {
|
||||||
@ -53,6 +52,7 @@
|
|||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"formik": "^2.1.5",
|
"formik": "^2.1.5",
|
||||||
"geolite2-redist": "^1.0.7",
|
"geolite2-redist": "^1.0.7",
|
||||||
|
"immer": "^7.0.8",
|
||||||
"is-localhost-ip": "^1.4.0",
|
"is-localhost-ip": "^1.4.0",
|
||||||
"jose": "^1.28.0",
|
"jose": "^1.28.0",
|
||||||
"maxmind": "^4.1.4",
|
"maxmind": "^4.1.4",
|
||||||
|
@ -4456,7 +4456,7 @@ ignore@^5.1.4, ignore@^5.1.8:
|
|||||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57"
|
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57"
|
||||||
integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
|
integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
|
||||||
|
|
||||||
immer@^7.0.3:
|
immer@^7.0.3, immer@^7.0.8:
|
||||||
version "7.0.8"
|
version "7.0.8"
|
||||||
resolved "https://registry.yarnpkg.com/immer/-/immer-7.0.8.tgz#41dcbc5669a76500d017bef3ad0d03ce0a1d7c1e"
|
resolved "https://registry.yarnpkg.com/immer/-/immer-7.0.8.tgz#41dcbc5669a76500d017bef3ad0d03ce0a1d7c1e"
|
||||||
integrity sha512-XnpIN8PXBBaOD43U8Z17qg6RQiKQYGDGGCIbz1ixmLGwBkSWwmrmx5X7d+hTtXDM8ur7m5OdLE0PiO+y5RB3pw==
|
integrity sha512-XnpIN8PXBBaOD43U8Z17qg6RQiKQYGDGGCIbz1ixmLGwBkSWwmrmx5X7d+hTtXDM8ur7m5OdLE0PiO+y5RB3pw==
|
||||||
|
Loading…
Reference in New Issue
Block a user