mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 12:47:13 +01:00
Added report context. Removed report store.
This commit is contained in:
parent
bc37f5124e
commit
bfb52eb678
31 changed files with 372 additions and 273 deletions
|
|
@ -4,7 +4,7 @@ import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simp
|
|||
import classNames from 'classnames';
|
||||
import { colord } from 'colord';
|
||||
import HoverTooltip from 'components/common/HoverTooltip';
|
||||
import { ISO_COUNTRIES, THEME_COLORS, MAP_FILE } from 'lib/constants';
|
||||
import { ISO_COUNTRIES, MAP_FILE } from 'lib/constants';
|
||||
import useTheme from 'hooks/useTheme';
|
||||
import useCountryNames from 'hooks/useCountryNames';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
|
|
@ -15,16 +15,7 @@ import styles from './WorldMap.module.css';
|
|||
export function WorldMap({ data, className }) {
|
||||
const { basePath } = useRouter();
|
||||
const [tooltip, setTooltip] = useState();
|
||||
const { theme } = useTheme();
|
||||
const colors = useMemo(
|
||||
() => ({
|
||||
baseColor: THEME_COLORS[theme].primary,
|
||||
fillColor: THEME_COLORS[theme].gray100,
|
||||
strokeColor: THEME_COLORS[theme].primary,
|
||||
hoverColor: THEME_COLORS[theme].primary,
|
||||
}),
|
||||
[theme],
|
||||
);
|
||||
const { theme, colors } = useTheme();
|
||||
const { locale } = useLocale();
|
||||
const countryNames = useCountryNames(locale);
|
||||
const metrics = useMemo(() => (data ? percentFilter(data) : []), [data]);
|
||||
|
|
@ -34,10 +25,10 @@ export function WorldMap({ data, className }) {
|
|||
const country = metrics?.find(({ x }) => x === code);
|
||||
|
||||
if (!country) {
|
||||
return colors.fillColor;
|
||||
return colors.map.fillColor;
|
||||
}
|
||||
|
||||
return colord(colors.baseColor)
|
||||
return colord(colors.map.baseColor)
|
||||
[theme === 'light' ? 'lighten' : 'darken'](0.4 * (1.0 - country.z / 100))
|
||||
.toHex();
|
||||
}
|
||||
|
|
@ -70,11 +61,11 @@ export function WorldMap({ data, className }) {
|
|||
key={geo.rsmKey}
|
||||
geography={geo}
|
||||
fill={getFillColor(code)}
|
||||
stroke={colors.strokeColor}
|
||||
stroke={colors.map.strokeColor}
|
||||
opacity={getOpacity(code)}
|
||||
style={{
|
||||
default: { outline: 'none' },
|
||||
hover: { outline: 'none', fill: colors.hoverColor },
|
||||
hover: { outline: 'none', fill: colors.map.hoverColor },
|
||||
pressed: { outline: 'none' },
|
||||
}}
|
||||
onMouseOver={() => handleHover(code)}
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@ export const labels = defineMessages({
|
|||
add: { id: 'label.add', defaultMessage: 'Add' },
|
||||
window: { id: 'label.window', defaultMessage: 'Window' },
|
||||
addUrl: { id: 'label.add-url', defaultMessage: 'Add URL' },
|
||||
runQuery: { id: 'label.run-query', defaultMessage: 'Run query' },
|
||||
});
|
||||
|
||||
export const messages = defineMessages({
|
||||
|
|
|
|||
|
|
@ -1,11 +1,19 @@
|
|||
import { createContext } from 'react';
|
||||
import Page from 'components/layout/Page';
|
||||
import styles from './reports.module.css';
|
||||
import { useReport } from 'hooks';
|
||||
|
||||
export const ReportContext = createContext(null);
|
||||
|
||||
export function Report({ reportId, defaultParameters, children, ...props }) {
|
||||
const report = useReport(reportId, defaultParameters);
|
||||
|
||||
export function Report({ children, ...props }) {
|
||||
return (
|
||||
<Page {...props} className={styles.container}>
|
||||
{children}
|
||||
</Page>
|
||||
<ReportContext.Provider value={{ ...report }}>
|
||||
<Page {...props} className={styles.container}>
|
||||
{children}
|
||||
</Page>
|
||||
</ReportContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,42 +1,62 @@
|
|||
import { Flexbox, Icon, LoadingButton, Text, useToast } from 'react-basics';
|
||||
import { useContext } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { Flexbox, Icon, LoadingButton, InlineEditField, useToast } from 'react-basics';
|
||||
import WebsiteSelect from 'components/input/WebsiteSelect';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import DateFilter from 'components/input/DateFilter';
|
||||
import { parseDateRange } from 'lib/date';
|
||||
import { updateReport } from 'store/reports';
|
||||
import { useMessages, useApi } from 'hooks';
|
||||
import { ReportContext } from './Report';
|
||||
import styles from './reports.module.css';
|
||||
|
||||
export function ReportHeader({ report, icon }) {
|
||||
export function ReportHeader({ icon }) {
|
||||
const { report, updateReport } = useContext(ReportContext);
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { toast, showToast } = useToast();
|
||||
const { post, useMutation } = useApi();
|
||||
const { mutate, isLoading } = useMutation(data => post(`/reports`, data));
|
||||
const router = useRouter();
|
||||
const { mutate: create, isLoading: isCreating } = useMutation(data => post(`/reports`, data));
|
||||
const { mutate: update, isLoading: isUpdating } = useMutation(data =>
|
||||
post(`/reports/${data.id}`, data),
|
||||
);
|
||||
|
||||
const { id, websiteId, name, parameters } = report || {};
|
||||
const { value, startDate, endDate } = parameters?.dateRange || {};
|
||||
const { websiteId, name, dateRange } = report || {};
|
||||
const { value, startDate, endDate } = dateRange || {};
|
||||
|
||||
const handleSelect = websiteId => {
|
||||
updateReport(id, { websiteId });
|
||||
updateReport({ websiteId });
|
||||
};
|
||||
|
||||
const handleDateChange = value => {
|
||||
updateReport(id, { parameters: { dateRange: { ...parseDateRange(value) } } });
|
||||
updateReport({ dateRange: { ...parseDateRange(value) } });
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
mutate(report, {
|
||||
onSuccess: async () => {
|
||||
showToast({ message: formatMessage(messages.saved), variant: 'success' });
|
||||
},
|
||||
});
|
||||
if (!report.id) {
|
||||
create(report, {
|
||||
onSuccess: async ({ id }) => {
|
||||
router.push(`/reports/${id}`, null, { shallow: true });
|
||||
showToast({ message: formatMessage(messages.saved), variant: 'success' });
|
||||
},
|
||||
});
|
||||
} else {
|
||||
update(report, {
|
||||
onSuccess: async () => {
|
||||
showToast({ message: formatMessage(messages.saved), variant: 'success' });
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleNameChange = name => {
|
||||
updateReport({ name });
|
||||
};
|
||||
|
||||
const Title = () => {
|
||||
return (
|
||||
<>
|
||||
<Icon size="lg">{icon}</Icon>
|
||||
<Text>{name}</Text>
|
||||
<InlineEditField value={name} onCommit={handleNameChange} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -54,7 +74,7 @@ export function ReportHeader({ report, icon }) {
|
|||
<WebsiteSelect websiteId={websiteId} onSelect={handleSelect} />
|
||||
<LoadingButton
|
||||
variant="primary"
|
||||
loading={isLoading}
|
||||
loading={isCreating || isUpdating}
|
||||
disabled={!websiteId || !value}
|
||||
onClick={handleSave}
|
||||
>
|
||||
|
|
|
|||
45
components/pages/reports/funnel/AddUrlForm.js
Normal file
45
components/pages/reports/funnel/AddUrlForm.js
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { useState } from 'react';
|
||||
import { useMessages } from 'hooks';
|
||||
import { Button, Form, FormButtons, FormRow, TextField } from 'react-basics';
|
||||
|
||||
export function AddUrlForm({ defaultValue = '', onSave, onClose }) {
|
||||
const [url, setUrl] = useState(defaultValue);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const handleSave = () => {
|
||||
onSave?.(url);
|
||||
setUrl('');
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleChange = e => {
|
||||
setUrl(e.target.value);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setUrl('');
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<FormRow label={formatMessage(labels.url)}>
|
||||
<TextField
|
||||
name="url"
|
||||
value={url}
|
||||
onChange={handleChange}
|
||||
autoFocus={true}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</FormRow>
|
||||
<FormButtons align="center" flex>
|
||||
<Button variant="primary" onClick={handleSave}>
|
||||
{formatMessage(labels.save)}
|
||||
</Button>
|
||||
<Button onClick={handleClose}>{formatMessage(labels.cancel)}</Button>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddUrlForm;
|
||||
|
|
@ -1,16 +1,18 @@
|
|||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useContext, useMemo } from 'react';
|
||||
import { Loading } from 'react-basics';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import useTheme from 'hooks/useTheme';
|
||||
import BarChart from 'components/metrics/BarChart';
|
||||
import { formatLongNumber } from 'lib/format';
|
||||
import styles from './FunnelChart.module.css';
|
||||
import { ReportContext } from '../Report';
|
||||
|
||||
export function FunnelChart({ report, data, loading, className }) {
|
||||
export function FunnelChart({ className, loading }) {
|
||||
const { report } = useContext(ReportContext);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { colors } = useTheme();
|
||||
|
||||
const { parameters } = report || {};
|
||||
const { parameters, data } = report || {};
|
||||
|
||||
const renderXLabel = useCallback(
|
||||
(label, index) => {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import { useContext, useRef, useState } from 'react';
|
||||
import { useMessages } from 'hooks';
|
||||
import {
|
||||
Button,
|
||||
Icon,
|
||||
Form,
|
||||
FormButtons,
|
||||
FormInput,
|
||||
FormRow,
|
||||
ModalTrigger,
|
||||
Modal,
|
||||
SubmitButton,
|
||||
Text,
|
||||
|
|
@ -14,30 +13,36 @@ import {
|
|||
Tooltip,
|
||||
} from 'react-basics';
|
||||
import Icons from 'components/icons';
|
||||
import { updateReport } from 'store/reports';
|
||||
import { useRef, useState } from 'react';
|
||||
import AddUrlForm from './AddUrlForm';
|
||||
import { ReportContext } from 'components/pages/reports/Report';
|
||||
import styles from './FunnelParameters.module.css';
|
||||
|
||||
export function FunnelParameters({ report }) {
|
||||
export function FunnelParameters() {
|
||||
const { report, runReport, updateReport, isRunning } = useContext(ReportContext);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const [show, setShow] = useState(false);
|
||||
const ref = useRef(null);
|
||||
const { id, websiteId, parameters, isLoading } = report || {};
|
||||
const { websiteId, parameters } = report || {};
|
||||
const queryDisabled = !websiteId || parameters?.urls?.length < 2;
|
||||
|
||||
const handleSubmit = values => {
|
||||
updateReport(id, { parameters: values, isLoading: false, update: Date.now() });
|
||||
runReport(values);
|
||||
};
|
||||
|
||||
const handleAdd = url => {
|
||||
updateReport(id, { parameters: { ...parameters, urls: parameters.urls.concat(url) } });
|
||||
const handleAddUrl = url => {
|
||||
updateReport({ parameters: { ...parameters, urls: parameters.urls.concat(url) } });
|
||||
};
|
||||
|
||||
const handleRemove = index => {
|
||||
const handleRemoveUrl = (index, e) => {
|
||||
e.stopPropagation();
|
||||
const urls = [...parameters.urls];
|
||||
urls.splice(index, 1);
|
||||
updateReport(id, { parameters: { ...parameters, urls } });
|
||||
updateReport({ parameters: { ...parameters, urls } });
|
||||
};
|
||||
|
||||
const showAddForm = () => setShow(true);
|
||||
const hideAddForm = () => setShow(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form ref={ref} values={parameters} onSubmit={handleSubmit}>
|
||||
|
|
@ -49,72 +54,49 @@ export function FunnelParameters({ report }) {
|
|||
<TextField autoComplete="off" />
|
||||
</FormInput>
|
||||
</FormRow>
|
||||
<FormRow label={formatMessage(labels.urls)} action={<AddURLButton onAdd={handleAdd} />}>
|
||||
<FormRow label={formatMessage(labels.urls)} action={<AddUrlButton onClick={showAddForm} />}>
|
||||
<div className={styles.urls}>
|
||||
{parameters?.urls.map((url, index) => {
|
||||
{parameters?.urls?.map((url, index) => {
|
||||
return (
|
||||
<div key={index} className={styles.url}>
|
||||
<Text>{url}</Text>
|
||||
<Icon onClick={() => handleRemove(index)}>
|
||||
<Icons.Close />
|
||||
</Icon>
|
||||
<Tooltip
|
||||
className={styles.icon}
|
||||
label={formatMessage(labels.remove)}
|
||||
position="right"
|
||||
>
|
||||
<Icon onClick={handleRemoveUrl.bind(null, index)}>
|
||||
<Icons.Close />
|
||||
</Icon>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</FormRow>
|
||||
<FormButtons>
|
||||
<SubmitButton variant="primary" disabled={queryDisabled} loading={isLoading}>
|
||||
{formatMessage(labels.query)}
|
||||
<SubmitButton variant="primary" disabled={queryDisabled} loading={isRunning}>
|
||||
{formatMessage(labels.runQuery)}
|
||||
</SubmitButton>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
{show && (
|
||||
<Modal onClose={hideAddForm}>
|
||||
<AddUrlForm onSave={handleAddUrl} onClose={hideAddForm} />
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function AddURLButton({ onAdd }) {
|
||||
const [url, setUrl] = useState('');
|
||||
function AddUrlButton({ onClick }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const handleAdd = close => {
|
||||
onAdd?.(url);
|
||||
setUrl('');
|
||||
close();
|
||||
};
|
||||
|
||||
const handleChange = e => {
|
||||
setUrl(e.target.value);
|
||||
};
|
||||
const handleClose = close => {
|
||||
setUrl('');
|
||||
close();
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip label={formatMessage(labels.addUrl)}>
|
||||
<ModalTrigger>
|
||||
<Icon>
|
||||
<Icons.Plus />
|
||||
</Icon>
|
||||
<Modal>
|
||||
{close => {
|
||||
return (
|
||||
<Form>
|
||||
<FormRow label={formatMessage(labels.url)}>
|
||||
<TextField name="url" value={url} onChange={handleChange} autoComplete="off" />
|
||||
</FormRow>
|
||||
<FormButtons align="center" flex>
|
||||
<Button variant="primary" onClick={() => handleAdd(close)}>
|
||||
{formatMessage(labels.add)}
|
||||
</Button>
|
||||
<Button onClick={() => handleClose(close)}>{formatMessage(labels.cancel)}</Button>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
);
|
||||
}}
|
||||
</Modal>
|
||||
</ModalTrigger>
|
||||
<Icon onClick={onClick}>
|
||||
<Icons.Plus />
|
||||
</Icon>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
.urls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.url {
|
||||
|
|
@ -14,3 +14,7 @@
|
|||
border-radius: var(--border-radius);
|
||||
box-shadow: 1px 1px 1px var(--base400);
|
||||
}
|
||||
|
||||
.icon {
|
||||
align-self: center;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,41 +1,29 @@
|
|||
import { useContext } from 'react';
|
||||
import FunnelChart from './FunnelChart';
|
||||
import FunnelTable from './FunnelTable';
|
||||
import FunnelParameters from './FunnelParameters';
|
||||
import Report from '../Report';
|
||||
import Report, { ReportContext } from '../Report';
|
||||
import ReportHeader from '../ReportHeader';
|
||||
import ReportMenu from '../ReportMenu';
|
||||
import ReportBody from '../ReportBody';
|
||||
import Funnel from 'assets/funnel.svg';
|
||||
import { useReport } from 'hooks';
|
||||
import useApi from 'hooks/useApi';
|
||||
|
||||
const defaultParameters = {
|
||||
type: 'funnel',
|
||||
parameters: { window: 60, urls: ['/', '/docs'] },
|
||||
};
|
||||
|
||||
export default function FunnelReport({ reportId }) {
|
||||
const report = useReport(reportId, { window: 60, urls: ['/', '/docs'] });
|
||||
const { post, useQuery } = useApi();
|
||||
const { data, isLoading, error } = useQuery(
|
||||
['report:funnel', report?.update],
|
||||
() => {
|
||||
const { websiteId, parameters } = report || {};
|
||||
|
||||
return post(`/reports/funnel`, {
|
||||
websiteId: websiteId,
|
||||
...parameters,
|
||||
startAt: +parameters.dateRange.startDate,
|
||||
endAt: +parameters.dateRange.endDate,
|
||||
});
|
||||
},
|
||||
{ enabled: !!report?.update },
|
||||
);
|
||||
|
||||
return (
|
||||
<Report error={error} loading={data && isLoading}>
|
||||
<ReportHeader icon={<Funnel />} report={report} />
|
||||
<Report reportId={reportId} defaultParameters={defaultParameters}>
|
||||
<ReportHeader icon={<Funnel />} />
|
||||
<ReportMenu>
|
||||
<FunnelParameters report={report} />
|
||||
<FunnelParameters />
|
||||
</ReportMenu>
|
||||
<ReportBody>
|
||||
<FunnelChart report={report} data={data} />
|
||||
<FunnelTable data={data} />
|
||||
<FunnelChart />
|
||||
<FunnelTable />
|
||||
</ReportBody>
|
||||
</Report>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
import { useContext } from 'react';
|
||||
import DataTable from 'components/metrics/DataTable';
|
||||
import { useMessages } from 'hooks';
|
||||
import { ReportContext } from '../Report';
|
||||
|
||||
export function FunnelTable({ data }) {
|
||||
export function FunnelTable() {
|
||||
const { report } = useContext(ReportContext);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
data={data}
|
||||
data={report?.data}
|
||||
title={formatMessage(labels.url)}
|
||||
metric={formatMessage(labels.visitors)}
|
||||
showPercentage={false}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue