mirror of
https://github.com/kremalicious/umami.git
synced 2024-11-15 17:55:08 +01:00
Added report context. Removed report store.
This commit is contained in:
parent
bc37f5124e
commit
bfb52eb678
@ -4,7 +4,7 @@ import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simp
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { colord } from 'colord';
|
import { colord } from 'colord';
|
||||||
import HoverTooltip from 'components/common/HoverTooltip';
|
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 useTheme from 'hooks/useTheme';
|
||||||
import useCountryNames from 'hooks/useCountryNames';
|
import useCountryNames from 'hooks/useCountryNames';
|
||||||
import useLocale from 'hooks/useLocale';
|
import useLocale from 'hooks/useLocale';
|
||||||
@ -15,16 +15,7 @@ import styles from './WorldMap.module.css';
|
|||||||
export function WorldMap({ data, className }) {
|
export function WorldMap({ data, className }) {
|
||||||
const { basePath } = useRouter();
|
const { basePath } = useRouter();
|
||||||
const [tooltip, setTooltip] = useState();
|
const [tooltip, setTooltip] = useState();
|
||||||
const { theme } = useTheme();
|
const { theme, colors } = 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 { locale } = useLocale();
|
const { locale } = useLocale();
|
||||||
const countryNames = useCountryNames(locale);
|
const countryNames = useCountryNames(locale);
|
||||||
const metrics = useMemo(() => (data ? percentFilter(data) : []), [data]);
|
const metrics = useMemo(() => (data ? percentFilter(data) : []), [data]);
|
||||||
@ -34,10 +25,10 @@ export function WorldMap({ data, className }) {
|
|||||||
const country = metrics?.find(({ x }) => x === code);
|
const country = metrics?.find(({ x }) => x === code);
|
||||||
|
|
||||||
if (!country) {
|
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))
|
[theme === 'light' ? 'lighten' : 'darken'](0.4 * (1.0 - country.z / 100))
|
||||||
.toHex();
|
.toHex();
|
||||||
}
|
}
|
||||||
@ -70,11 +61,11 @@ export function WorldMap({ data, className }) {
|
|||||||
key={geo.rsmKey}
|
key={geo.rsmKey}
|
||||||
geography={geo}
|
geography={geo}
|
||||||
fill={getFillColor(code)}
|
fill={getFillColor(code)}
|
||||||
stroke={colors.strokeColor}
|
stroke={colors.map.strokeColor}
|
||||||
opacity={getOpacity(code)}
|
opacity={getOpacity(code)}
|
||||||
style={{
|
style={{
|
||||||
default: { outline: 'none' },
|
default: { outline: 'none' },
|
||||||
hover: { outline: 'none', fill: colors.hoverColor },
|
hover: { outline: 'none', fill: colors.map.hoverColor },
|
||||||
pressed: { outline: 'none' },
|
pressed: { outline: 'none' },
|
||||||
}}
|
}}
|
||||||
onMouseOver={() => handleHover(code)}
|
onMouseOver={() => handleHover(code)}
|
||||||
|
@ -128,6 +128,7 @@ export const labels = defineMessages({
|
|||||||
add: { id: 'label.add', defaultMessage: 'Add' },
|
add: { id: 'label.add', defaultMessage: 'Add' },
|
||||||
window: { id: 'label.window', defaultMessage: 'Window' },
|
window: { id: 'label.window', defaultMessage: 'Window' },
|
||||||
addUrl: { id: 'label.add-url', defaultMessage: 'Add URL' },
|
addUrl: { id: 'label.add-url', defaultMessage: 'Add URL' },
|
||||||
|
runQuery: { id: 'label.run-query', defaultMessage: 'Run query' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const messages = defineMessages({
|
export const messages = defineMessages({
|
||||||
|
@ -1,11 +1,19 @@
|
|||||||
|
import { createContext } from 'react';
|
||||||
import Page from 'components/layout/Page';
|
import Page from 'components/layout/Page';
|
||||||
import styles from './reports.module.css';
|
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 (
|
return (
|
||||||
|
<ReportContext.Provider value={{ ...report }}>
|
||||||
<Page {...props} className={styles.container}>
|
<Page {...props} className={styles.container}>
|
||||||
{children}
|
{children}
|
||||||
</Page>
|
</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 WebsiteSelect from 'components/input/WebsiteSelect';
|
||||||
import PageHeader from 'components/layout/PageHeader';
|
import PageHeader from 'components/layout/PageHeader';
|
||||||
import DateFilter from 'components/input/DateFilter';
|
import DateFilter from 'components/input/DateFilter';
|
||||||
import { parseDateRange } from 'lib/date';
|
import { parseDateRange } from 'lib/date';
|
||||||
import { updateReport } from 'store/reports';
|
|
||||||
import { useMessages, useApi } from 'hooks';
|
import { useMessages, useApi } from 'hooks';
|
||||||
|
import { ReportContext } from './Report';
|
||||||
import styles from './reports.module.css';
|
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 { formatMessage, labels, messages } = useMessages();
|
||||||
const { toast, showToast } = useToast();
|
const { toast, showToast } = useToast();
|
||||||
const { post, useMutation } = useApi();
|
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 { websiteId, name, dateRange } = report || {};
|
||||||
const { value, startDate, endDate } = parameters?.dateRange || {};
|
const { value, startDate, endDate } = dateRange || {};
|
||||||
|
|
||||||
const handleSelect = websiteId => {
|
const handleSelect = websiteId => {
|
||||||
updateReport(id, { websiteId });
|
updateReport({ websiteId });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDateChange = value => {
|
const handleDateChange = value => {
|
||||||
updateReport(id, { parameters: { dateRange: { ...parseDateRange(value) } } });
|
updateReport({ dateRange: { ...parseDateRange(value) } });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
mutate(report, {
|
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 () => {
|
onSuccess: async () => {
|
||||||
showToast({ message: formatMessage(messages.saved), variant: 'success' });
|
showToast({ message: formatMessage(messages.saved), variant: 'success' });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNameChange = name => {
|
||||||
|
updateReport({ name });
|
||||||
};
|
};
|
||||||
|
|
||||||
const Title = () => {
|
const Title = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Icon size="lg">{icon}</Icon>
|
<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} />
|
<WebsiteSelect websiteId={websiteId} onSelect={handleSelect} />
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
variant="primary"
|
variant="primary"
|
||||||
loading={isLoading}
|
loading={isCreating || isUpdating}
|
||||||
disabled={!websiteId || !value}
|
disabled={!websiteId || !value}
|
||||||
onClick={handleSave}
|
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 { Loading } from 'react-basics';
|
||||||
import useMessages from 'hooks/useMessages';
|
import useMessages from 'hooks/useMessages';
|
||||||
import useTheme from 'hooks/useTheme';
|
import useTheme from 'hooks/useTheme';
|
||||||
import BarChart from 'components/metrics/BarChart';
|
import BarChart from 'components/metrics/BarChart';
|
||||||
import { formatLongNumber } from 'lib/format';
|
import { formatLongNumber } from 'lib/format';
|
||||||
import styles from './FunnelChart.module.css';
|
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 { formatMessage, labels } = useMessages();
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
|
|
||||||
const { parameters } = report || {};
|
const { parameters, data } = report || {};
|
||||||
|
|
||||||
const renderXLabel = useCallback(
|
const renderXLabel = useCallback(
|
||||||
(label, index) => {
|
(label, index) => {
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
|
import { useContext, useRef, useState } from 'react';
|
||||||
import { useMessages } from 'hooks';
|
import { useMessages } from 'hooks';
|
||||||
import {
|
import {
|
||||||
Button,
|
|
||||||
Icon,
|
Icon,
|
||||||
Form,
|
Form,
|
||||||
FormButtons,
|
FormButtons,
|
||||||
FormInput,
|
FormInput,
|
||||||
FormRow,
|
FormRow,
|
||||||
ModalTrigger,
|
|
||||||
Modal,
|
Modal,
|
||||||
SubmitButton,
|
SubmitButton,
|
||||||
Text,
|
Text,
|
||||||
@ -14,30 +13,36 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
} from 'react-basics';
|
} from 'react-basics';
|
||||||
import Icons from 'components/icons';
|
import Icons from 'components/icons';
|
||||||
import { updateReport } from 'store/reports';
|
import AddUrlForm from './AddUrlForm';
|
||||||
import { useRef, useState } from 'react';
|
import { ReportContext } from 'components/pages/reports/Report';
|
||||||
import styles from './FunnelParameters.module.css';
|
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 { formatMessage, labels } = useMessages();
|
||||||
|
const [show, setShow] = useState(false);
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
const { id, websiteId, parameters, isLoading } = report || {};
|
const { websiteId, parameters } = report || {};
|
||||||
const queryDisabled = !websiteId || parameters?.urls?.length < 2;
|
const queryDisabled = !websiteId || parameters?.urls?.length < 2;
|
||||||
|
|
||||||
const handleSubmit = values => {
|
const handleSubmit = values => {
|
||||||
updateReport(id, { parameters: values, isLoading: false, update: Date.now() });
|
runReport(values);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAdd = url => {
|
const handleAddUrl = url => {
|
||||||
updateReport(id, { parameters: { ...parameters, urls: parameters.urls.concat(url) } });
|
updateReport({ parameters: { ...parameters, urls: parameters.urls.concat(url) } });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemove = index => {
|
const handleRemoveUrl = (index, e) => {
|
||||||
|
e.stopPropagation();
|
||||||
const urls = [...parameters.urls];
|
const urls = [...parameters.urls];
|
||||||
urls.splice(index, 1);
|
urls.splice(index, 1);
|
||||||
updateReport(id, { parameters: { ...parameters, urls } });
|
updateReport({ parameters: { ...parameters, urls } });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const showAddForm = () => setShow(true);
|
||||||
|
const hideAddForm = () => setShow(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Form ref={ref} values={parameters} onSubmit={handleSubmit}>
|
<Form ref={ref} values={parameters} onSubmit={handleSubmit}>
|
||||||
@ -49,72 +54,49 @@ export function FunnelParameters({ report }) {
|
|||||||
<TextField autoComplete="off" />
|
<TextField autoComplete="off" />
|
||||||
</FormInput>
|
</FormInput>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormRow label={formatMessage(labels.urls)} action={<AddURLButton onAdd={handleAdd} />}>
|
<FormRow label={formatMessage(labels.urls)} action={<AddUrlButton onClick={showAddForm} />}>
|
||||||
<div className={styles.urls}>
|
<div className={styles.urls}>
|
||||||
{parameters?.urls.map((url, index) => {
|
{parameters?.urls?.map((url, index) => {
|
||||||
return (
|
return (
|
||||||
<div key={index} className={styles.url}>
|
<div key={index} className={styles.url}>
|
||||||
<Text>{url}</Text>
|
<Text>{url}</Text>
|
||||||
<Icon onClick={() => handleRemove(index)}>
|
<Tooltip
|
||||||
|
className={styles.icon}
|
||||||
|
label={formatMessage(labels.remove)}
|
||||||
|
position="right"
|
||||||
|
>
|
||||||
|
<Icon onClick={handleRemoveUrl.bind(null, index)}>
|
||||||
<Icons.Close />
|
<Icons.Close />
|
||||||
</Icon>
|
</Icon>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormButtons>
|
<FormButtons>
|
||||||
<SubmitButton variant="primary" disabled={queryDisabled} loading={isLoading}>
|
<SubmitButton variant="primary" disabled={queryDisabled} loading={isRunning}>
|
||||||
{formatMessage(labels.query)}
|
{formatMessage(labels.runQuery)}
|
||||||
</SubmitButton>
|
</SubmitButton>
|
||||||
</FormButtons>
|
</FormButtons>
|
||||||
</Form>
|
</Form>
|
||||||
|
{show && (
|
||||||
|
<Modal onClose={hideAddForm}>
|
||||||
|
<AddUrlForm onSave={handleAddUrl} onClose={hideAddForm} />
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AddURLButton({ onAdd }) {
|
function AddUrlButton({ onClick }) {
|
||||||
const [url, setUrl] = useState('');
|
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
||||||
const handleAdd = close => {
|
|
||||||
onAdd?.(url);
|
|
||||||
setUrl('');
|
|
||||||
close();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChange = e => {
|
|
||||||
setUrl(e.target.value);
|
|
||||||
};
|
|
||||||
const handleClose = close => {
|
|
||||||
setUrl('');
|
|
||||||
close();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip label={formatMessage(labels.addUrl)}>
|
<Tooltip label={formatMessage(labels.addUrl)}>
|
||||||
<ModalTrigger>
|
<Icon onClick={onClick}>
|
||||||
<Icon>
|
|
||||||
<Icons.Plus />
|
<Icons.Plus />
|
||||||
</Icon>
|
</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>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
.urls {
|
.urls {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.url {
|
.url {
|
||||||
@ -14,3 +14,7 @@
|
|||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
box-shadow: 1px 1px 1px var(--base400);
|
box-shadow: 1px 1px 1px var(--base400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
@ -1,41 +1,29 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
import FunnelChart from './FunnelChart';
|
import FunnelChart from './FunnelChart';
|
||||||
import FunnelTable from './FunnelTable';
|
import FunnelTable from './FunnelTable';
|
||||||
import FunnelParameters from './FunnelParameters';
|
import FunnelParameters from './FunnelParameters';
|
||||||
import Report from '../Report';
|
import Report, { ReportContext } from '../Report';
|
||||||
import ReportHeader from '../ReportHeader';
|
import ReportHeader from '../ReportHeader';
|
||||||
import ReportMenu from '../ReportMenu';
|
import ReportMenu from '../ReportMenu';
|
||||||
import ReportBody from '../ReportBody';
|
import ReportBody from '../ReportBody';
|
||||||
import Funnel from 'assets/funnel.svg';
|
import Funnel from 'assets/funnel.svg';
|
||||||
import { useReport } from 'hooks';
|
import { useReport } from 'hooks';
|
||||||
import useApi from 'hooks/useApi';
|
|
||||||
|
const defaultParameters = {
|
||||||
|
type: 'funnel',
|
||||||
|
parameters: { window: 60, urls: ['/', '/docs'] },
|
||||||
|
};
|
||||||
|
|
||||||
export default function FunnelReport({ reportId }) {
|
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 (
|
return (
|
||||||
<Report error={error} loading={data && isLoading}>
|
<Report reportId={reportId} defaultParameters={defaultParameters}>
|
||||||
<ReportHeader icon={<Funnel />} report={report} />
|
<ReportHeader icon={<Funnel />} />
|
||||||
<ReportMenu>
|
<ReportMenu>
|
||||||
<FunnelParameters report={report} />
|
<FunnelParameters />
|
||||||
</ReportMenu>
|
</ReportMenu>
|
||||||
<ReportBody>
|
<ReportBody>
|
||||||
<FunnelChart report={report} data={data} />
|
<FunnelChart />
|
||||||
<FunnelTable data={data} />
|
<FunnelTable />
|
||||||
</ReportBody>
|
</ReportBody>
|
||||||
</Report>
|
</Report>
|
||||||
);
|
);
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
import DataTable from 'components/metrics/DataTable';
|
import DataTable from 'components/metrics/DataTable';
|
||||||
import { useMessages } from 'hooks';
|
import { useMessages } from 'hooks';
|
||||||
|
import { ReportContext } from '../Report';
|
||||||
|
|
||||||
export function FunnelTable({ data }) {
|
export function FunnelTable() {
|
||||||
|
const { report } = useContext(ReportContext);
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataTable
|
<DataTable
|
||||||
data={data}
|
data={report?.data}
|
||||||
title={formatMessage(labels.url)}
|
title={formatMessage(labels.url)}
|
||||||
metric={formatMessage(labels.visitors)}
|
metric={formatMessage(labels.visitors)}
|
||||||
showPercentage={false}
|
showPercentage={false}
|
||||||
|
@ -1,20 +1,80 @@
|
|||||||
import useStore, { createReport } from 'store/reports';
|
import { produce } from 'immer';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import useApi from './useApi';
|
||||||
|
|
||||||
|
const baseParameters = {
|
||||||
|
name: 'Untitled',
|
||||||
|
description: '',
|
||||||
|
parameters: {},
|
||||||
|
};
|
||||||
|
|
||||||
export function useReport(reportId, defaultParameters) {
|
export function useReport(reportId, defaultParameters) {
|
||||||
const [id, setId] = useState(reportId);
|
const [report, setReport] = useState(null);
|
||||||
|
const [isRunning, setIsRunning] = useState(false);
|
||||||
|
const { get, post } = useApi();
|
||||||
|
|
||||||
const selector = useCallback(state => state[id], [id]);
|
const loadReport = async id => {
|
||||||
const report = useStore(selector);
|
const data = await get(`/reports/${id}`);
|
||||||
|
|
||||||
|
setReport(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const runReport = useCallback(
|
||||||
|
async parameters => {
|
||||||
|
const { websiteId, type, dateRange } = report;
|
||||||
|
setIsRunning(true);
|
||||||
|
|
||||||
|
const data = await post(`/reports/${type}`, {
|
||||||
|
websiteId: websiteId,
|
||||||
|
...parameters,
|
||||||
|
startAt: +dateRange?.startDate,
|
||||||
|
endAt: +dateRange?.endDate,
|
||||||
|
});
|
||||||
|
|
||||||
|
setReport(
|
||||||
|
produce(state => {
|
||||||
|
state.parameters = parameters;
|
||||||
|
state.data = data;
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
setIsRunning(false);
|
||||||
|
},
|
||||||
|
[report],
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateReport = useCallback(
|
||||||
|
async data => {
|
||||||
|
setReport(
|
||||||
|
produce(state => {
|
||||||
|
const { parameters, ...rest } = data;
|
||||||
|
|
||||||
|
if (parameters) {
|
||||||
|
state.parameters = { ...state.parameters, ...parameters };
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key in rest) {
|
||||||
|
state[key] = rest[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[report],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!report) {
|
if (!reportId) {
|
||||||
const newReport = createReport(defaultParameters);
|
setReport({ ...baseParameters, ...defaultParameters });
|
||||||
setId(newReport.id);
|
} else {
|
||||||
|
loadReport(reportId);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return report;
|
return { report, runReport, updateReport, isRunning };
|
||||||
}
|
}
|
||||||
|
|
||||||
export default useReport;
|
export default useReport;
|
||||||
|
@ -17,6 +17,9 @@ export function useTheme() {
|
|||||||
const primaryColor = colord(THEME_COLORS[theme].primary);
|
const primaryColor = colord(THEME_COLORS[theme].primary);
|
||||||
|
|
||||||
const colors = {
|
const colors = {
|
||||||
|
theme: {
|
||||||
|
...THEME_COLORS[theme],
|
||||||
|
},
|
||||||
chart: {
|
chart: {
|
||||||
text: THEME_COLORS[theme].gray700,
|
text: THEME_COLORS[theme].gray700,
|
||||||
line: THEME_COLORS[theme].gray200,
|
line: THEME_COLORS[theme].gray200,
|
||||||
@ -33,6 +36,12 @@ export function useTheme() {
|
|||||||
hoverBorderColor: primaryColor.toRgbString(),
|
hoverBorderColor: primaryColor.toRgbString(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
map: {
|
||||||
|
baseColor: THEME_COLORS[theme].primary,
|
||||||
|
fillColor: THEME_COLORS[theme].gray100,
|
||||||
|
strokeColor: THEME_COLORS[theme].primary,
|
||||||
|
hoverColor: THEME_COLORS[theme].primary,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function saveTheme(value) {
|
function saveTheme(value) {
|
||||||
|
12
lib/auth.ts
12
lib/auth.ts
@ -1,4 +1,4 @@
|
|||||||
import { UserReport } from '@prisma/client';
|
import { Report } from '@prisma/client';
|
||||||
import redis from '@umami/redis-client';
|
import redis from '@umami/redis-client';
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
import { PERMISSIONS, ROLE_PERMISSIONS, SHARE_TOKEN_HEADER } from 'lib/constants';
|
import { PERMISSIONS, ROLE_PERMISSIONS, SHARE_TOKEN_HEADER } from 'lib/constants';
|
||||||
@ -135,28 +135,28 @@ export async function canDeleteWebsite({ user }: Auth, websiteId: string) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canViewUserReport(auth: Auth, userReport: UserReport) {
|
export async function canViewReport(auth: Auth, report: Report) {
|
||||||
if (auth.user.isAdmin) {
|
if (auth.user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((auth.user.id = userReport.userId)) {
|
if ((auth.user.id = report.userId)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await canViewWebsite(auth, userReport.websiteId)) {
|
if (await canViewWebsite(auth, report.websiteId)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function canUpdateUserReport(auth: Auth, userReport: UserReport) {
|
export async function canUpdateReport(auth: Auth, report: Report) {
|
||||||
if (auth.user.isAdmin) {
|
if (auth.user.isAdmin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((auth.user.id = userReport.userId)) {
|
if ((auth.user.id = report.userId)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,7 +91,7 @@
|
|||||||
"maxmind": "^4.3.6",
|
"maxmind": "^4.3.6",
|
||||||
"moment-timezone": "^0.5.35",
|
"moment-timezone": "^0.5.35",
|
||||||
"next": "13.3.1",
|
"next": "13.3.1",
|
||||||
"next-basics": "^0.27.0",
|
"next-basics": "^0.30.0",
|
||||||
"node-fetch": "^3.2.8",
|
"node-fetch": "^3.2.8",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
@ -1,59 +1,68 @@
|
|||||||
import { canUpdateUserReport, canViewUserReport } from 'lib/auth';
|
import { canUpdateReport, canViewReport } from 'lib/auth';
|
||||||
import { useAuth, useCors } from 'lib/middleware';
|
import { useAuth, useCors } from 'lib/middleware';
|
||||||
import { NextApiRequestQueryBody } from 'lib/types';
|
import { NextApiRequestQueryBody } from 'lib/types';
|
||||||
import { NextApiResponse } from 'next';
|
import { NextApiResponse } from 'next';
|
||||||
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
|
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
|
||||||
import { getUserReportById, updateUserReport } from 'queries';
|
import { getReportById, updateReport } from 'queries';
|
||||||
|
|
||||||
export interface UserReportRequestQuery {
|
export interface ReportRequestQuery {
|
||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserReportRequestBody {
|
export interface ReportRequestBody {
|
||||||
websiteId: string;
|
websiteId: string;
|
||||||
reportName: string;
|
type: string;
|
||||||
templateName: string;
|
name: string;
|
||||||
|
description: string;
|
||||||
parameters: string;
|
parameters: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async (
|
export default async (
|
||||||
req: NextApiRequestQueryBody<UserReportRequestQuery, UserReportRequestBody>,
|
req: NextApiRequestQueryBody<ReportRequestQuery, ReportRequestBody>,
|
||||||
res: NextApiResponse,
|
res: NextApiResponse,
|
||||||
) => {
|
) => {
|
||||||
await useCors(req, res);
|
await useCors(req, res);
|
||||||
await useAuth(req, res);
|
await useAuth(req, res);
|
||||||
|
|
||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
const { id: userReportId } = req.query;
|
const { id: reportId } = req.query;
|
||||||
|
|
||||||
const data = await getUserReportById(userReportId);
|
const data = await getReportById(reportId);
|
||||||
|
|
||||||
if (!(await canViewUserReport(req.auth, data))) {
|
if (!(await canViewReport(req.auth, data))) {
|
||||||
return unauthorized(res);
|
return unauthorized(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data.parameters = JSON.parse(data.parameters);
|
||||||
|
|
||||||
return ok(res, data);
|
return ok(res, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.method === 'POST') {
|
if (req.method === 'POST') {
|
||||||
const { id: userReportId } = req.query;
|
const { id: reportId } = req.query;
|
||||||
|
|
||||||
const data = await getUserReportById(userReportId);
|
const { websiteId, type, name, description, parameters } = req.body;
|
||||||
|
|
||||||
if (!(await canUpdateUserReport(req.auth, data))) {
|
const data = await getReportById(reportId);
|
||||||
|
|
||||||
|
if (!(await canUpdateReport(req.auth, data))) {
|
||||||
return unauthorized(res);
|
return unauthorized(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
const updated = await updateUserReport(
|
const result = await updateReport(
|
||||||
{
|
{
|
||||||
...req.body,
|
websiteId,
|
||||||
},
|
type,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
parameters: JSON.stringify(parameters),
|
||||||
|
} as any,
|
||||||
{
|
{
|
||||||
id: userReportId,
|
id: reportId,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return ok(res, updated);
|
return ok(res, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
return methodNotAllowed(res);
|
return methodNotAllowed(res);
|
||||||
|
@ -3,17 +3,21 @@ import { useAuth, useCors } from 'lib/middleware';
|
|||||||
import { NextApiRequestQueryBody } from 'lib/types';
|
import { NextApiRequestQueryBody } from 'lib/types';
|
||||||
import { NextApiResponse } from 'next';
|
import { NextApiResponse } from 'next';
|
||||||
import { methodNotAllowed, ok } from 'next-basics';
|
import { methodNotAllowed, ok } from 'next-basics';
|
||||||
import { createUserReport, getUserReports } from 'queries';
|
import { createReport, getReports } from 'queries';
|
||||||
|
|
||||||
export interface UserReportRequestBody {
|
export interface ReportRequestBody {
|
||||||
websiteId: string;
|
websiteId: string;
|
||||||
reportName: string;
|
name: string;
|
||||||
templateName: string;
|
type: string;
|
||||||
parameters: string;
|
description: string;
|
||||||
|
parameters: {
|
||||||
|
window: string;
|
||||||
|
urls: string[];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async (
|
export default async (
|
||||||
req: NextApiRequestQueryBody<any, UserReportRequestBody>,
|
req: NextApiRequestQueryBody<any, ReportRequestBody>,
|
||||||
res: NextApiResponse,
|
res: NextApiResponse,
|
||||||
) => {
|
) => {
|
||||||
await useCors(req, res);
|
await useCors(req, res);
|
||||||
@ -24,19 +28,25 @@ export default async (
|
|||||||
} = req.auth;
|
} = req.auth;
|
||||||
|
|
||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
const data = await getUserReports(userId);
|
const data = await getReports(userId);
|
||||||
|
|
||||||
return ok(res, data);
|
return ok(res, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.method === 'POST') {
|
if (req.method === 'POST') {
|
||||||
const data = await createUserReport({
|
const { websiteId, type, name, description, parameters } = req.body;
|
||||||
|
|
||||||
|
const result = await createReport({
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
userId,
|
userId,
|
||||||
...req.body,
|
websiteId,
|
||||||
});
|
type,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
parameters: JSON.stringify(parameters),
|
||||||
|
} as any);
|
||||||
|
|
||||||
return ok(res, data);
|
return ok(res, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
return methodNotAllowed(res);
|
return methodNotAllowed(res);
|
||||||
|
20
pages/reports/[id].js
Normal file
20
pages/reports/[id].js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import AppLayout from 'components/layout/AppLayout';
|
||||||
|
import FunnelReport from 'components/pages/reports/funnel/FunnelReport';
|
||||||
|
import useMessages from 'hooks/useMessages';
|
||||||
|
|
||||||
|
export default function ReportsPage() {
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const router = useRouter();
|
||||||
|
const { id } = router.query;
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppLayout title={formatMessage(labels.websites)}>
|
||||||
|
<FunnelReport reportId={id} />
|
||||||
|
</AppLayout>
|
||||||
|
);
|
||||||
|
}
|
@ -1,13 +1,21 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Item, Tabs } from 'react-basics';
|
||||||
import AppLayout from 'components/layout/AppLayout';
|
import AppLayout from 'components/layout/AppLayout';
|
||||||
import ReportList from 'components/pages/reports/ReportList';
|
import ReportList from 'components/pages/reports/ReportList';
|
||||||
import useMessages from 'hooks/useMessages';
|
import useMessages from 'hooks/useMessages';
|
||||||
|
|
||||||
export default function ReportsPage() {
|
export default function ReportsPage() {
|
||||||
|
const [tab, setTab] = useState('create');
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppLayout title={formatMessage(labels.reports)}>
|
<AppLayout title={formatMessage(labels.reports)}>
|
||||||
<ReportList />
|
<Tabs selectedKey={tab} onSelect={setTab} style={{ marginBottom: 30, fontSize: 14 }}>
|
||||||
|
<Item key="create">{formatMessage(labels.reports)}</Item>
|
||||||
|
<Item key="saved">{formatMessage(labels.save)}</Item>
|
||||||
|
</Tabs>
|
||||||
|
{tab === 'create' && <ReportList />}
|
||||||
|
{tab === 'saved' && <h1>My reports</h1>}
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
33
queries/admin/report.ts
Normal file
33
queries/admin/report.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { Prisma, Report } from '@prisma/client';
|
||||||
|
import prisma from 'lib/prisma';
|
||||||
|
|
||||||
|
export async function createReport(data: Prisma.ReportUncheckedCreateInput): Promise<Report> {
|
||||||
|
return prisma.client.report.create({ data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getReportById(reportId: string): Promise<Report> {
|
||||||
|
return prisma.client.report.findUnique({
|
||||||
|
where: {
|
||||||
|
id: reportId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getReports(userId: string): Promise<Report[]> {
|
||||||
|
return prisma.client.report.findMany({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateReport(
|
||||||
|
data: Prisma.ReportUpdateInput,
|
||||||
|
where: Prisma.ReportWhereUniqueInput,
|
||||||
|
): Promise<Report> {
|
||||||
|
return prisma.client.report.update({ data, where });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteReport(where: Prisma.ReportWhereUniqueInput): Promise<Report> {
|
||||||
|
return prisma.client.report.delete({ where });
|
||||||
|
}
|
@ -210,7 +210,7 @@ export async function deleteUser(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
client.userReport.deleteMany({
|
client.report.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
OR: [
|
OR: [
|
||||||
{
|
{
|
||||||
|
@ -1,37 +0,0 @@
|
|||||||
import { Prisma, UserReport } from '@prisma/client';
|
|
||||||
import prisma from 'lib/prisma';
|
|
||||||
|
|
||||||
export async function createUserReport(
|
|
||||||
data: Prisma.UserReportUncheckedCreateInput,
|
|
||||||
): Promise<UserReport> {
|
|
||||||
return prisma.client.userReport.create({ data });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getUserReportById(userReportId: string): Promise<UserReport> {
|
|
||||||
return prisma.client.userReport.findUnique({
|
|
||||||
where: {
|
|
||||||
id: userReportId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getUserReports(userId: string): Promise<UserReport[]> {
|
|
||||||
return prisma.client.userReport.findMany({
|
|
||||||
where: {
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateUserReport(
|
|
||||||
data: Prisma.UserReportUpdateInput,
|
|
||||||
where: Prisma.UserReportWhereUniqueInput,
|
|
||||||
): Promise<UserReport> {
|
|
||||||
return prisma.client.userReport.update({ data, where });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteUserReport(
|
|
||||||
where: Prisma.UserReportWhereUniqueInput,
|
|
||||||
): Promise<UserReport> {
|
|
||||||
return prisma.client.userReport.delete({ where });
|
|
||||||
}
|
|
@ -92,7 +92,7 @@ export async function deleteWebsite(
|
|||||||
websiteId,
|
websiteId,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
client.userReport.deleteMany({
|
client.report.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
websiteId,
|
websiteId,
|
||||||
},
|
},
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
export * from './admin/team';
|
export * from './admin/team';
|
||||||
export * from './admin/teamUser';
|
export * from './admin/teamUser';
|
||||||
export * from './admin/user';
|
export * from './admin/user';
|
||||||
export * from './admin/userReport';
|
export * from './admin/report';
|
||||||
export * from './admin/website';
|
export * from './admin/website';
|
||||||
export * from './analytics/event/getEventMetrics';
|
export * from './analytics/event/getEventMetrics';
|
||||||
export * from './analytics/event/getEventUsage';
|
export * from './analytics/event/getEventUsage';
|
||||||
|
@ -4,7 +4,7 @@ import replace from '@rollup/plugin-replace';
|
|||||||
import { terser } from 'rollup-plugin-terser';
|
import { terser } from 'rollup-plugin-terser';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
input: 'tracker/index.js',
|
input: 'tracker/[id].js',
|
||||||
output: {
|
output: {
|
||||||
file: 'public/script.js',
|
file: 'public/script.js',
|
||||||
format: 'iife',
|
format: 'iife',
|
||||||
|
@ -1,57 +0,0 @@
|
|||||||
import { create } from 'zustand';
|
|
||||||
import produce from 'immer';
|
|
||||||
import { getRandomChars } from 'next-basics';
|
|
||||||
|
|
||||||
const emptyReport = {
|
|
||||||
name: 'Untitled',
|
|
||||||
description: '',
|
|
||||||
parameters: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
const initialState = {};
|
|
||||||
|
|
||||||
const store = create(() => ({ ...initialState }));
|
|
||||||
|
|
||||||
export function updateReport(id, data) {
|
|
||||||
const report = store.getState()[id];
|
|
||||||
|
|
||||||
if (report) {
|
|
||||||
store.setState(
|
|
||||||
produce(state => {
|
|
||||||
const item = state[id];
|
|
||||||
const { parameters, ...rest } = data;
|
|
||||||
|
|
||||||
if (parameters) {
|
|
||||||
item.parameters = { ...item.parameters, ...parameters };
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const key in rest) {
|
|
||||||
item[key] = rest[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
return state;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createReport(parameters) {
|
|
||||||
const id = `new_${getRandomChars(16)}`;
|
|
||||||
const report = { ...emptyReport, id, parameters };
|
|
||||||
|
|
||||||
store.setState(
|
|
||||||
produce(state => {
|
|
||||||
state[id] = report;
|
|
||||||
|
|
||||||
return state;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return report;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default store;
|
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
window.__STORE__ = store;
|
|
||||||
}
|
|
20
yarn.lock
20
yarn.lock
@ -3715,11 +3715,6 @@ balanced-match@^2.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-2.0.0.tgz#dc70f920d78db8b858535795867bf48f820633d9"
|
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-2.0.0.tgz#dc70f920d78db8b858535795867bf48f820633d9"
|
||||||
integrity sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==
|
integrity sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==
|
||||||
|
|
||||||
base-x@^4.0.0:
|
|
||||||
version "4.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/base-x/-/base-x-4.0.0.tgz#d0e3b7753450c73f8ad2389b5c018a4af7b2224a"
|
|
||||||
integrity sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==
|
|
||||||
|
|
||||||
base64-js@^1.3.1:
|
base64-js@^1.3.1:
|
||||||
version "1.5.1"
|
version "1.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
||||||
@ -7019,14 +7014,14 @@ natural-compare@^1.4.0:
|
|||||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||||
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
|
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
|
||||||
|
|
||||||
next-basics@^0.27.0:
|
next-basics@^0.30.0:
|
||||||
version "0.27.0"
|
version "0.30.0"
|
||||||
resolved "https://registry.yarnpkg.com/next-basics/-/next-basics-0.27.0.tgz#7d46c88de4b53cadfef86230f8cbe7dba6f10dc6"
|
resolved "https://registry.yarnpkg.com/next-basics/-/next-basics-0.30.0.tgz#1d5f1b3bd3cd66218f00ab0390659a819ed274fa"
|
||||||
integrity sha512-ZviF4O4/14eBjGG7fK83oswuM/rur37TRxcjXCKPJN4kOTUgrzn9Sz+vpzkB8PZ2WaHv5ONQ7TkBEwnhMCEyMQ==
|
integrity sha512-IWNrBsxM8watkvWHWkUp5PC0WypqeHMEaj5rCufjEfwuIe++ePtDrYgUKY6Se6y6QJWbyHliozCpa1KpVyZHnQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
base-x "^4.0.0"
|
|
||||||
bcryptjs "^2.4.3"
|
bcryptjs "^2.4.3"
|
||||||
jsonwebtoken "^9.0.0"
|
jsonwebtoken "^9.0.0"
|
||||||
|
pure-rand "^6.0.2"
|
||||||
|
|
||||||
next@13.3.1:
|
next@13.3.1:
|
||||||
version "13.3.1"
|
version "13.3.1"
|
||||||
@ -8144,6 +8139,11 @@ punycode@^2.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f"
|
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f"
|
||||||
integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==
|
integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==
|
||||||
|
|
||||||
|
pure-rand@^6.0.2:
|
||||||
|
version "6.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.0.2.tgz#a9c2ddcae9b68d736a8163036f088a2781c8b306"
|
||||||
|
integrity sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ==
|
||||||
|
|
||||||
qs@~6.5.2:
|
qs@~6.5.2:
|
||||||
version "6.5.3"
|
version "6.5.3"
|
||||||
resolved "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz"
|
resolved "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz"
|
||||||
|
Loading…
Reference in New Issue
Block a user