mirror of
https://github.com/umami-software/umami.git
synced 2026-02-07 14:17:13 +01:00
Feat/um 49 query builder api (#1573)
* add uuid to event. add indexes * eventdata api * add event data * remove test data * update list
This commit is contained in:
parent
9c36dc485e
commit
ba31f48f1a
32 changed files with 690 additions and 64 deletions
48
components/common/EventDataButton.js
Normal file
48
components/common/EventDataButton.js
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import List from 'assets/list-ul.svg';
|
||||
import Modal from 'components/common/Modal';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import Button from './Button';
|
||||
import EventDataForm from 'components/forms/EventDataForm';
|
||||
import styles from './EventDataButton.module.css';
|
||||
|
||||
function EventDataButton({ websiteId }) {
|
||||
const [showEventData, setShowEventData] = useState(false);
|
||||
|
||||
function handleClick() {
|
||||
if (!showEventData) {
|
||||
setShowEventData(true);
|
||||
}
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
setShowEventData(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
icon={<List />}
|
||||
tooltip={<FormattedMessage id="label.event-data" defaultMessage="Event" />}
|
||||
tooltipId="button-event"
|
||||
size="small"
|
||||
onClick={handleClick}
|
||||
className={styles.button}
|
||||
>
|
||||
Event Data
|
||||
</Button>
|
||||
{showEventData && (
|
||||
<Modal title={<FormattedMessage id="label.event-data" defaultMessage="Query Event Data" />}>
|
||||
<EventDataForm websiteId={websiteId} onClose={handleClose} />
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
EventDataButton.propTypes = {
|
||||
websiteId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
};
|
||||
|
||||
export default EventDataButton;
|
||||
3
components/common/EventDataButton.module.css
Normal file
3
components/common/EventDataButton.module.css
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
.button {
|
||||
width: fit-content;
|
||||
}
|
||||
262
components/forms/EventDataForm.js
Normal file
262
components/forms/EventDataForm.js
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
import classNames from 'classnames';
|
||||
import Button from 'components/common/Button';
|
||||
import DateFilter from 'components/common/DateFilter';
|
||||
import DropDown from 'components/common/DropDown';
|
||||
import FormLayout, {
|
||||
FormButtons,
|
||||
FormError,
|
||||
FormMessage,
|
||||
FormRow,
|
||||
} from 'components/layout/FormLayout';
|
||||
import DataTable from 'components/metrics/DataTable';
|
||||
import FilterTags from 'components/metrics/FilterTags';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import useApi from 'hooks/useApi';
|
||||
import useDateRange from 'hooks/useDateRange';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import styles from './EventDataForm.module.css';
|
||||
import useTimezone from 'hooks/useTimezone';
|
||||
|
||||
export const filterOptions = [
|
||||
{ label: 'Count', value: 'count' },
|
||||
{ label: 'Average', value: 'avg' },
|
||||
{ label: 'Minimum', value: 'min' },
|
||||
{ label: 'Maxmimum', value: 'max' },
|
||||
{ label: 'Sum', value: 'sum' },
|
||||
];
|
||||
|
||||
export const dateOptions = [
|
||||
{ label: <FormattedMessage id="label.today" defaultMessage="Today" />, value: '1day' },
|
||||
{
|
||||
label: (
|
||||
<FormattedMessage id="label.last-hours" defaultMessage="Last {x} hours" values={{ x: 24 }} />
|
||||
),
|
||||
value: '24hour',
|
||||
},
|
||||
{
|
||||
label: <FormattedMessage id="label.yesterday" defaultMessage="Yesterday" />,
|
||||
value: '-1day',
|
||||
},
|
||||
{
|
||||
label: <FormattedMessage id="label.this-week" defaultMessage="This week" />,
|
||||
value: '1week',
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<FormattedMessage id="label.last-days" defaultMessage="Last {x} days" values={{ x: 7 }} />
|
||||
),
|
||||
value: '7day',
|
||||
},
|
||||
{
|
||||
label: <FormattedMessage id="label.this-month" defaultMessage="This month" />,
|
||||
value: '1month',
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<FormattedMessage id="label.last-days" defaultMessage="Last {x} days" values={{ x: 30 }} />
|
||||
),
|
||||
value: '30day',
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<FormattedMessage id="label.last-days" defaultMessage="Last {x} days" values={{ x: 90 }} />
|
||||
),
|
||||
value: '90day',
|
||||
},
|
||||
{ label: <FormattedMessage id="label.this-year" defaultMessage="This year" />, value: '1year' },
|
||||
{
|
||||
label: <FormattedMessage id="label.custom-range" defaultMessage="Custom range" />,
|
||||
value: 'custom',
|
||||
divider: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default function EventDataForm({ websiteId, onClose, className }) {
|
||||
const { post } = useApi();
|
||||
const [message, setMessage] = useState();
|
||||
const [columns, setColumns] = useState();
|
||||
const [filters, setFilters] = useState();
|
||||
const [data, setData] = useState([]);
|
||||
const [dateRange, setDateRange] = useDateRange('report');
|
||||
const { startDate, endDate, value } = dateRange;
|
||||
const [timezone] = useTimezone();
|
||||
const [isValid, setIsValid] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (Object.keys(columns).length > 0) {
|
||||
setIsValid(true);
|
||||
} else {
|
||||
setIsValid(false);
|
||||
}
|
||||
}, [columns]);
|
||||
|
||||
const handleAddTag = (value, list, setState, resetForm) => {
|
||||
setState({ ...list, [`${value.field}`]: value.value });
|
||||
resetForm();
|
||||
};
|
||||
|
||||
const handleRemoveTag = (value, list, setState) => {
|
||||
const next = { ...list };
|
||||
|
||||
delete next[`${value}`];
|
||||
|
||||
setState(next);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const params = {
|
||||
website_id: websiteId,
|
||||
start_at: +startDate,
|
||||
end_at: +endDate,
|
||||
timezone,
|
||||
columns,
|
||||
filters,
|
||||
};
|
||||
|
||||
const { ok, data } = await post(`/websites/${websiteId}/eventdata`, params);
|
||||
|
||||
if (!ok) {
|
||||
setMessage(<FormattedMessage id="message.failure" defaultMessage="Something went wrong." />);
|
||||
setData([]);
|
||||
} else {
|
||||
setData(data);
|
||||
setMessage(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormMessage>{message}</FormMessage>
|
||||
<div className={classNames(styles.container, className)}>
|
||||
<div className={styles.form}>
|
||||
<FormLayout>
|
||||
<div className={styles.filters}>
|
||||
<FormRow>
|
||||
<label htmlFor="date-range">
|
||||
<FormattedMessage id="label.date-range" defaultMessage="Date Range" />
|
||||
</label>
|
||||
<DateFilter
|
||||
value={value}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onChange={setDateRange}
|
||||
options={dateOptions}
|
||||
/>
|
||||
</FormRow>
|
||||
</div>
|
||||
<div className={styles.filters}>
|
||||
<Formik
|
||||
initialValues={{ field: '', value: '' }}
|
||||
onSubmit={(value, { resetForm }) =>
|
||||
handleAddTag(value, columns, setColumns, resetForm)
|
||||
}
|
||||
>
|
||||
{({ values, setFieldValue }) => (
|
||||
<Form>
|
||||
<FormRow>
|
||||
<label htmlFor="field">
|
||||
<FormattedMessage id="label.field-name" defaultMessage="Field Name" />
|
||||
</label>
|
||||
<div>
|
||||
<Field name="field" type="text" />
|
||||
<FormError name="field" />
|
||||
</div>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<label htmlFor="value">
|
||||
<FormattedMessage id="label.type" defaultMessage="Type" />
|
||||
</label>
|
||||
<div>
|
||||
<DropDown
|
||||
value={values.value}
|
||||
onChange={value => setFieldValue('value', value)}
|
||||
className={styles.dropdown}
|
||||
name="value"
|
||||
options={filterOptions}
|
||||
/>
|
||||
<FormError name="value" />
|
||||
</div>
|
||||
</FormRow>
|
||||
<FormButtons className={styles.formButtons}>
|
||||
<Button
|
||||
variant="action"
|
||||
type="submit"
|
||||
disabled={!values.field || !values.value}
|
||||
>
|
||||
<FormattedMessage id="label.add-column" defaultMessage="Add Column" />
|
||||
</Button>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
<FilterTags
|
||||
className={styles.filterTag}
|
||||
params={columns}
|
||||
onClick={value => handleRemoveTag(value, columns, setColumns)}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.filters}>
|
||||
<Formik
|
||||
initialValues={{ field: '', value: '' }}
|
||||
onSubmit={(value, { resetForm }) =>
|
||||
handleAddTag(value, filters, setFilters, resetForm)
|
||||
}
|
||||
>
|
||||
{({ values }) => (
|
||||
<Form>
|
||||
<FormRow>
|
||||
<label htmlFor="field">
|
||||
<FormattedMessage id="label.field-name" defaultMessage="Field Name" />
|
||||
</label>
|
||||
<div>
|
||||
<Field name="field" type="text" />
|
||||
<FormError name="field" />
|
||||
</div>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<label htmlFor="value">
|
||||
<FormattedMessage id="label.value" defaultMessage="Value" />
|
||||
</label>
|
||||
<div>
|
||||
<Field name="value" type="text" />
|
||||
<FormError name="value" />
|
||||
</div>
|
||||
</FormRow>
|
||||
<FormButtons className={styles.formButtons}>
|
||||
<Button
|
||||
variant="action"
|
||||
type="submit"
|
||||
disabled={!values.field || !values.value}
|
||||
>
|
||||
<FormattedMessage id="label.add-filter" defaultMessage="Add Filter" />
|
||||
</Button>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
<FilterTags
|
||||
className={styles.filterTag}
|
||||
params={filters}
|
||||
onClick={value => handleRemoveTag(value, filters, setFilters)}
|
||||
/>
|
||||
</div>
|
||||
</FormLayout>
|
||||
</div>
|
||||
<div>
|
||||
<DataTable className={styles.table} data={data} title="Results" showPercentage={false} />
|
||||
</div>
|
||||
</div>
|
||||
<FormButtons>
|
||||
<Button variant="action" onClick={handleSubmit} disabled={!isValid}>
|
||||
<FormattedMessage id="label.search" defaultMessage="Search" />
|
||||
</Button>
|
||||
<Button onClick={onClose}>
|
||||
<FormattedMessage id="label.cancel" defaultMessage="Cancel" />
|
||||
</Button>
|
||||
</FormButtons>
|
||||
</>
|
||||
);
|
||||
}
|
||||
38
components/forms/EventDataForm.module.css
Normal file
38
components/forms/EventDataForm.module.css
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
.container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.form {
|
||||
border-right: 1px solid var(--gray300);
|
||||
width: 420px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
padding: 10px 5px;
|
||||
}
|
||||
|
||||
.filters + .filters {
|
||||
border-top: 1px solid var(--gray300);
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
.table {
|
||||
padding: 10px;
|
||||
min-height: 430px;
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
.formButtons {
|
||||
justify-content: flex-start;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
min-height: 39px;
|
||||
min-width: 240px;
|
||||
}
|
||||
|
||||
.filterTag {
|
||||
flex-wrap: wrap;
|
||||
margin: 10px 5px 5px 5px;
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ export default function DataTable({
|
|||
height,
|
||||
animate = true,
|
||||
virtualize = false,
|
||||
showPercentage = true,
|
||||
}) {
|
||||
const [format, setFormat] = useState(true);
|
||||
const formatFunc = format ? formatLongNumber : formatNumber;
|
||||
|
|
@ -38,6 +39,7 @@ export default function DataTable({
|
|||
animate={animate && !virtualize}
|
||||
format={formatFunc}
|
||||
onClick={handleSetFormat}
|
||||
showPercentage={showPercentage}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -68,7 +70,15 @@ export default function DataTable({
|
|||
);
|
||||
}
|
||||
|
||||
const AnimatedRow = ({ label, value = 0, percent, animate, format, onClick }) => {
|
||||
const AnimatedRow = ({
|
||||
label,
|
||||
value = 0,
|
||||
percent,
|
||||
animate,
|
||||
format,
|
||||
onClick,
|
||||
showPercentage = true,
|
||||
}) => {
|
||||
const props = useSpring({
|
||||
width: percent,
|
||||
y: value,
|
||||
|
|
@ -82,15 +92,17 @@ const AnimatedRow = ({ label, value = 0, percent, animate, format, onClick }) =>
|
|||
<div className={styles.value} onClick={onClick}>
|
||||
<animated.div className={styles.value}>{props.y?.interpolate(format)}</animated.div>
|
||||
</div>
|
||||
<div className={styles.percent}>
|
||||
<animated.div
|
||||
className={styles.bar}
|
||||
style={{ width: props.width.interpolate(n => `${n}%`) }}
|
||||
/>
|
||||
<animated.span className={styles.percentValue}>
|
||||
{props.width.interpolate(n => `${n.toFixed(0)}%`)}
|
||||
</animated.span>
|
||||
</div>
|
||||
{showPercentage && (
|
||||
<div className={styles.percent}>
|
||||
<animated.div
|
||||
className={styles.bar}
|
||||
style={{ width: props.width.interpolate(n => `${n}%`) }}
|
||||
/>
|
||||
<animated.span className={styles.percentValue}>
|
||||
{props.width.interpolate(n => `${n.toFixed(0)}%`)}
|
||||
</animated.span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,12 +5,12 @@ import Button from 'components/common/Button';
|
|||
import Times from 'assets/times.svg';
|
||||
import styles from './FilterTags.module.css';
|
||||
|
||||
export default function FilterTags({ params, onClick }) {
|
||||
export default function FilterTags({ className, params, onClick }) {
|
||||
if (Object.keys(params).filter(key => params[key]).length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className={classNames(styles.filters, 'col-12')}>
|
||||
<div className={classNames(styles.filters, 'col-12', className)}>
|
||||
{Object.keys(params).map(key => {
|
||||
if (!params[key]) {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -7,8 +7,5 @@
|
|||
.tag {
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.tag + .tag {
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
import React from 'react';
|
||||
import Arrow from 'assets/arrow-right.svg';
|
||||
import classNames from 'classnames';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import Favicon from 'components/common/Favicon';
|
||||
import Link from 'components/common/Link';
|
||||
import OverflowText from 'components/common/OverflowText';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import RefreshButton from 'components/common/RefreshButton';
|
||||
import ButtonLayout from 'components/layout/ButtonLayout';
|
||||
import Favicon from 'components/common/Favicon';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import ActiveUsers from './ActiveUsers';
|
||||
import Arrow from 'assets/arrow-right.svg';
|
||||
import styles from './WebsiteHeader.module.css';
|
||||
|
||||
export default function WebsiteHeader({ websiteId, title, domain, showLink = false }) {
|
||||
|
|
|
|||
|
|
@ -24,9 +24,9 @@ export default function TestConsole() {
|
|||
return null;
|
||||
}
|
||||
|
||||
const options = data.map(({ name, websiteId }) => ({ label: name, value: websiteId }));
|
||||
const website = data.find(({ websiteId }) => websiteId === +websiteId);
|
||||
const selectedValue = options.find(({ value }) => value === website?.websiteId)?.value;
|
||||
const options = data.map(({ name, websiteUuid }) => ({ label: name, value: websiteUuid }));
|
||||
const website = data.find(({ websiteUuid }) => websiteId === websiteUuid);
|
||||
const selectedValue = options.find(({ value }) => value === website?.websiteUuid)?.value;
|
||||
|
||||
function handleSelect(value) {
|
||||
router.push(`/console/${value}`);
|
||||
|
|
@ -104,13 +104,13 @@ export default function TestConsole() {
|
|||
<div className="row">
|
||||
<div className="col-12">
|
||||
<WebsiteChart
|
||||
websiteId={website.websiteId}
|
||||
websiteId={website.websiteUuid}
|
||||
title={website.name}
|
||||
domain={website.domain}
|
||||
showLink
|
||||
/>
|
||||
<PageHeader>Events</PageHeader>
|
||||
<EventsChart websiteId={website.websiteId} />
|
||||
<EventsChart websiteId={website.websiteUuid} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import useFetch from 'hooks/useFetch';
|
|||
import usePageQuery from 'hooks/usePageQuery';
|
||||
import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
|
||||
import styles from './WebsiteDetails.module.css';
|
||||
import EventDataButton from 'components/common/EventDataButton';
|
||||
|
||||
const messages = defineMessages({
|
||||
pages: { id: 'metrics.pages', defaultMessage: 'Pages' },
|
||||
|
|
@ -183,6 +184,7 @@ export default function WebsiteDetails({ websiteId }) {
|
|||
<EventsTable {...tableProps} onDataLoad={setEventsData} />
|
||||
</GridColumn>
|
||||
<GridColumn xs={12} md={12} lg={8}>
|
||||
<EventDataButton websiteId={websiteId} />
|
||||
<EventsChart className={styles.eventschart} websiteId={websiteId} />
|
||||
</GridColumn>
|
||||
</GridRow>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue