mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 04:37:11 +01:00
Merge branch 'umami-software:master' into ui-rtl
This commit is contained in:
commit
1520f21b82
26 changed files with 480 additions and 363 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
|
||||
import { StatusLight } from 'react-basics';
|
||||
import { StatusLight, Loading } from 'react-basics';
|
||||
import classNames from 'classnames';
|
||||
import Chart from 'chart.js/auto';
|
||||
import HoverTooltip from 'components/common/HoverTooltip';
|
||||
|
|
@ -39,6 +39,26 @@ export default function BarChart({
|
|||
return +label > 1000 ? formatLongNumber(label) : label;
|
||||
};
|
||||
|
||||
const renderXLabel = useCallback(
|
||||
(label, index, values) => {
|
||||
const d = new Date(values[index].value);
|
||||
|
||||
switch (unit) {
|
||||
case 'minute':
|
||||
return dateFormat(d, 'H:mm', locale);
|
||||
case 'hour':
|
||||
return dateFormat(d, 'p', locale);
|
||||
case 'day':
|
||||
return dateFormat(d, 'MMM d', locale);
|
||||
case 'month':
|
||||
return dateFormat(d, 'MMM', locale);
|
||||
default:
|
||||
return label;
|
||||
}
|
||||
},
|
||||
[locale, unit],
|
||||
);
|
||||
|
||||
const renderTooltip = useCallback(
|
||||
model => {
|
||||
const { opacity, labelColors, dataPoints } = model.tooltip;
|
||||
|
|
@ -115,6 +135,7 @@ export default function BarChart({
|
|||
color: colors.text,
|
||||
autoSkip: false,
|
||||
maxRotation: 0,
|
||||
callback: renderXLabel,
|
||||
},
|
||||
},
|
||||
y: {
|
||||
|
|
@ -135,7 +156,7 @@ export default function BarChart({
|
|||
},
|
||||
},
|
||||
};
|
||||
}, [animationDuration, renderTooltip, stacked, colors, unit]);
|
||||
}, [animationDuration, renderTooltip, renderXLabel, stacked, colors, unit, locale]);
|
||||
|
||||
const createChart = () => {
|
||||
Chart.defaults.font.family = 'Inter';
|
||||
|
|
@ -158,7 +179,9 @@ export default function BarChart({
|
|||
|
||||
chart.current.options = getOptions();
|
||||
|
||||
chart.current.data.datasets = datasets;
|
||||
if (datasets.length) {
|
||||
chart.current.data.datasets = datasets;
|
||||
}
|
||||
|
||||
chart.current.update();
|
||||
|
||||
|
|
@ -173,11 +196,12 @@ export default function BarChart({
|
|||
updateChart();
|
||||
}
|
||||
}
|
||||
}, [datasets, unit, theme, animationDuration, locale, loading]);
|
||||
}, [datasets, unit, theme, animationDuration, locale]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classNames(styles.chart, className)}>
|
||||
{loading && <Loading position="page" icon="dots" />}
|
||||
<canvas ref={canvas} />
|
||||
</div>
|
||||
<Legend chart={chart.current} />
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useEffect } from 'react';
|
||||
import { StatusLight } from 'react-basics';
|
||||
import { colord } from 'colord';
|
||||
import classNames from 'classnames';
|
||||
|
|
@ -9,7 +10,7 @@ export default function Legend({ chart }) {
|
|||
const { locale } = useLocale();
|
||||
const forceUpdate = useForceUpdate();
|
||||
|
||||
function handleClick(index) {
|
||||
const handleClick = index => {
|
||||
const meta = chart.getDatasetMeta(index);
|
||||
|
||||
meta.hidden = meta.hidden === null ? !chart.data.datasets[index].hidden : null;
|
||||
|
|
@ -17,7 +18,11 @@ export default function Legend({ chart }) {
|
|||
chart.update();
|
||||
|
||||
forceUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
forceUpdate();
|
||||
}, [locale]);
|
||||
|
||||
if (!chart?.legend?.legendItems.find(({ text }) => text)) {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import BarChart from './BarChart';
|
|||
import { THEME_COLORS } from 'lib/constants';
|
||||
import useTheme from 'hooks/useTheme';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
|
||||
export default function PageviewsChart({
|
||||
websiteId,
|
||||
|
|
@ -16,6 +17,7 @@ export default function PageviewsChart({
|
|||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const [theme] = useTheme();
|
||||
const { locale } = useLocale();
|
||||
|
||||
const colors = useMemo(() => {
|
||||
const primaryColor = colord(THEME_COLORS[theme].primary);
|
||||
|
|
@ -52,7 +54,7 @@ export default function PageviewsChart({
|
|||
...colors.views,
|
||||
},
|
||||
];
|
||||
}, [data]);
|
||||
}, [data, locale, colors]);
|
||||
|
||||
return (
|
||||
<BarChart
|
||||
|
|
|
|||
|
|
@ -9,14 +9,26 @@ export default function RealtimeHeader({ data = {} }) {
|
|||
return (
|
||||
<div className={styles.header}>
|
||||
<div className={styles.metrics}>
|
||||
<MetricCard label={formatMessage(labels.views)} value={pageviews?.length} hideComparison />
|
||||
<MetricCard
|
||||
className={styles.card}
|
||||
label={formatMessage(labels.views)}
|
||||
value={pageviews?.length}
|
||||
hideComparison
|
||||
/>
|
||||
<MetricCard
|
||||
className={styles.card}
|
||||
label={formatMessage(labels.visitors)}
|
||||
value={visitors?.length}
|
||||
hideComparison
|
||||
/>
|
||||
<MetricCard label={formatMessage(labels.events)} value={events?.length} hideComparison />
|
||||
<MetricCard
|
||||
className={styles.card}
|
||||
label={formatMessage(labels.events)}
|
||||
value={events?.length}
|
||||
hideComparison
|
||||
/>
|
||||
<MetricCard
|
||||
className={styles.card}
|
||||
label={formatMessage(labels.countries)}
|
||||
value={countries?.length}
|
||||
hideComparison
|
||||
|
|
|
|||
|
|
@ -7,4 +7,15 @@
|
|||
|
||||
.metrics {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.card {
|
||||
justify-self: flex-start;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 992px) {
|
||||
.card {
|
||||
flex-basis: calc(50% - 20px);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export default function PasswordEditForm({ onSave, onClose }) {
|
|||
name="newPassword"
|
||||
rules={{
|
||||
required: 'Required',
|
||||
minLength: { value: 8, message: formatMessage(messages.minPasswordLength) },
|
||||
minLength: { value: 8, message: formatMessage(messages.minPasswordLength, { n: 8 }) },
|
||||
}}
|
||||
>
|
||||
<PasswordField autoComplete="new-password" />
|
||||
|
|
@ -48,7 +48,7 @@ export default function PasswordEditForm({ onSave, onClose }) {
|
|||
name="confirmPassword"
|
||||
rules={{
|
||||
required: formatMessage(labels.required),
|
||||
minLength: { value: 8, message: formatMessage(messages.minPasswordLength) },
|
||||
minLength: { value: 8, message: formatMessage(messages.minPasswordLength, { n: 8 }) },
|
||||
validate: samePassword,
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ export default function TeamSettings({ teamId }) {
|
|||
title={
|
||||
<Breadcrumbs>
|
||||
<Item>
|
||||
<Link href="/settings/teams">Teams</Link>
|
||||
<Link href="/settings/teams">{formatMessage(labels.teams)}</Link>
|
||||
</Item>
|
||||
<Item>{values?.name}</Item>
|
||||
</Breadcrumbs>
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ export default function UserEditForm({ userId, data, onSave }) {
|
|||
<FormInput
|
||||
name="newPassword"
|
||||
rules={{
|
||||
minLength: { value: 8, message: formatMessage(messages.minPasswordLength) },
|
||||
minLength: { value: 8, message: formatMessage(messages.minPasswordLength, { n: 8 }) },
|
||||
}}
|
||||
>
|
||||
<PasswordField autoComplete="new-password" />
|
||||
|
|
|
|||
|
|
@ -6,10 +6,12 @@ import UserDeleteForm from './UserDeleteForm';
|
|||
import { ROLES } from 'lib/constants';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import SettingsTable from 'components/common/SettingsTable';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
|
||||
export default function UsersTable({ data = [], onDelete }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { user } = useUser();
|
||||
const { dateLocale } = useLocale();
|
||||
|
||||
const columns = [
|
||||
{ name: 'username', label: formatMessage(labels.username), style: { flex: 1.5 } },
|
||||
|
|
@ -22,6 +24,7 @@ export default function UsersTable({ data = [], onDelete }) {
|
|||
if (key === 'created') {
|
||||
return formatDistance(new Date(row.createdAt), new Date(), {
|
||||
addSuffix: true,
|
||||
locale: dateLocale,
|
||||
});
|
||||
}
|
||||
if (key === 'role') {
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export default function WebsiteData({ websiteId, onSave }) {
|
|||
description={formatMessage(messages.deleteWebsiteWarning)}
|
||||
>
|
||||
<ModalTrigger>
|
||||
<Button variant="danger">Delete</Button>
|
||||
<Button variant="danger">{formatMessage(labels.delete)}</Button>
|
||||
<Modal title={formatMessage(labels.deleteWebsite)}>
|
||||
{close => (
|
||||
<WebsiteDeleteForm websiteId={websiteId} onSave={handleDelete} onClose={close} />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue