mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 04:37:11 +01:00
feat: Add time-of-day filtering with quick hour filters and time range picker
This commit is contained in:
parent
860e6390f1
commit
c5cb032e05
3 changed files with 152 additions and 0 deletions
|
|
@ -4,6 +4,7 @@ import { Fragment, type Key, useState } from 'react';
|
|||
import { DateDisplay } from '@/components/common/DateDisplay';
|
||||
import { useMessages, useMobile } from '@/components/hooks';
|
||||
import { DatePickerForm } from '@/components/metrics/DatePickerForm';
|
||||
import { TimeRangePickerForm } from '@/components/metrics/TimeRangePickerForm';
|
||||
import { parseDateRange } from '@/lib/date';
|
||||
|
||||
export interface DateFilterProps extends SelectProps {
|
||||
|
|
@ -24,6 +25,7 @@ export function DateFilter({
|
|||
}: DateFilterProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const [showPicker, setShowPicker] = useState(false);
|
||||
const [showTimePicker, setShowTimePicker] = useState(false);
|
||||
const { startDate, endDate } = parseDateRange(value) || {};
|
||||
const { isMobile } = useMobile();
|
||||
|
||||
|
|
@ -33,6 +35,27 @@ export function DateFilter({
|
|||
label: formatMessage(labels.lastHours, { x: '24' }),
|
||||
value: '24hour',
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.lastHours, { x: '12' }),
|
||||
value: '12hour',
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.lastHours, { x: '6' }),
|
||||
value: '6hour',
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.lastHours, { x: '4' }),
|
||||
value: '4hour',
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.lastHours, { x: '2' }),
|
||||
value: '2hour',
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.lastHours, { x: '1' }),
|
||||
value: '1hour',
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.thisWeek),
|
||||
value: '0week',
|
||||
|
|
@ -75,6 +98,10 @@ export function DateFilter({
|
|||
value: 'custom',
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.timeRange),
|
||||
value: 'timeRange',
|
||||
},
|
||||
]
|
||||
.filter(n => n)
|
||||
.map((a, id) => ({ ...a, id }));
|
||||
|
|
@ -84,6 +111,10 @@ export function DateFilter({
|
|||
setShowPicker(true);
|
||||
return;
|
||||
}
|
||||
if (value === 'timeRange') {
|
||||
setShowTimePicker(true);
|
||||
return;
|
||||
}
|
||||
onChange(value.toString());
|
||||
};
|
||||
|
||||
|
|
@ -92,6 +123,11 @@ export function DateFilter({
|
|||
onChange(value.toString());
|
||||
};
|
||||
|
||||
const handleTimePickerChange = (value: string) => {
|
||||
setShowTimePicker(false);
|
||||
onChange(value.toString());
|
||||
};
|
||||
|
||||
const renderValue = ({ defaultChildren }) => {
|
||||
return value?.startsWith('range') || renderDate ? (
|
||||
<DateDisplay startDate={startDate} endDate={endDate} />
|
||||
|
|
@ -136,6 +172,20 @@ export function DateFilter({
|
|||
</Dialog>
|
||||
</Modal>
|
||||
)}
|
||||
{showTimePicker && (
|
||||
<Modal isOpen={true}>
|
||||
<Dialog>
|
||||
<TimeRangePickerForm
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
minDate={new Date(2000, 0, 1)}
|
||||
maxDate={endOfYear(new Date())}
|
||||
onChange={handleTimePickerChange}
|
||||
onClose={() => setShowTimePicker(false)}
|
||||
/>
|
||||
</Dialog>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -132,6 +132,9 @@ export const labels = defineMessages({
|
|||
thisYear: { id: 'label.this-year', defaultMessage: 'This year' },
|
||||
allTime: { id: 'label.all-time', defaultMessage: 'All time' },
|
||||
customRange: { id: 'label.custom-range', defaultMessage: 'Custom range' },
|
||||
timeRange: { id: 'label.time-range', defaultMessage: 'Time range' },
|
||||
startTime: { id: 'label.start-time', defaultMessage: 'Start time' },
|
||||
endTime: { id: 'label.end-time', defaultMessage: 'End time' },
|
||||
selectWebsite: { id: 'label.select-website', defaultMessage: 'Select website' },
|
||||
selectRole: { id: 'label.select-role', defaultMessage: 'Select role' },
|
||||
selectDate: { id: 'label.select-date', defaultMessage: 'Select date' },
|
||||
|
|
|
|||
99
src/components/metrics/TimeRangePickerForm.tsx
Normal file
99
src/components/metrics/TimeRangePickerForm.tsx
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import { Button, Calendar, Column, ListItem, Row, Select } from '@umami/react-zen';
|
||||
import { endOfDay, isAfter, setHours, setMinutes, startOfDay } from 'date-fns';
|
||||
import { type Key, useState } from 'react';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
|
||||
// Generate hour options (00:00 to 23:00)
|
||||
const HOUR_OPTIONS = Array.from({ length: 24 }, (_, i) => ({
|
||||
id: i.toString(),
|
||||
label: `${i.toString().padStart(2, '0')}:00`,
|
||||
value: i,
|
||||
}));
|
||||
|
||||
interface TimeRangePickerFormProps {
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
minDate?: Date;
|
||||
maxDate?: Date;
|
||||
onChange: (value: string) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function TimeRangePickerForm({
|
||||
startDate: defaultStartDate,
|
||||
endDate: defaultEndDate,
|
||||
minDate,
|
||||
maxDate,
|
||||
onChange,
|
||||
onClose,
|
||||
}: TimeRangePickerFormProps) {
|
||||
const [date, setDate] = useState(defaultStartDate || new Date());
|
||||
const [startHour, setStartHour] = useState(0);
|
||||
const [endHour, setEndHour] = useState(23);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const disabled = startHour > endHour;
|
||||
|
||||
const handleSave = () => {
|
||||
const start = setMinutes(setHours(startOfDay(date), startHour), 0);
|
||||
const end = setMinutes(setHours(startOfDay(date), endHour), 59);
|
||||
onChange(`range:${start.getTime()}:${end.getTime()}`);
|
||||
};
|
||||
|
||||
const handleStartHourChange = (value: Key) => {
|
||||
setStartHour(Number(value));
|
||||
};
|
||||
|
||||
const handleEndHourChange = (value: Key) => {
|
||||
setEndHour(Number(value));
|
||||
};
|
||||
|
||||
return (
|
||||
<Column gap>
|
||||
<Row justifyContent="center">
|
||||
<Calendar value={date} minValue={minDate} maxValue={maxDate} onChange={setDate} />
|
||||
</Row>
|
||||
<Row gap justifyContent="center" alignItems="center">
|
||||
<Column>
|
||||
<label style={{ fontSize: '0.875rem', marginBottom: '0.25rem' }}>
|
||||
{formatMessage(labels.startTime)}
|
||||
</label>
|
||||
<Select
|
||||
value={startHour.toString()}
|
||||
onChange={handleStartHourChange}
|
||||
style={{ minWidth: '100px' }}
|
||||
>
|
||||
{HOUR_OPTIONS.map(({ id, label }) => (
|
||||
<ListItem key={id} id={id}>
|
||||
{label}
|
||||
</ListItem>
|
||||
))}
|
||||
</Select>
|
||||
</Column>
|
||||
<span style={{ marginTop: '1.5rem' }}>—</span>
|
||||
<Column>
|
||||
<label style={{ fontSize: '0.875rem', marginBottom: '0.25rem' }}>
|
||||
{formatMessage(labels.endTime)}
|
||||
</label>
|
||||
<Select
|
||||
value={endHour.toString()}
|
||||
onChange={handleEndHourChange}
|
||||
style={{ minWidth: '100px' }}
|
||||
>
|
||||
{HOUR_OPTIONS.map(({ id, label }) => (
|
||||
<ListItem key={id} id={id}>
|
||||
{label}
|
||||
</ListItem>
|
||||
))}
|
||||
</Select>
|
||||
</Column>
|
||||
</Row>
|
||||
<Row justifyContent="end" gap>
|
||||
<Button onPress={onClose}>{formatMessage(labels.cancel)}</Button>
|
||||
<Button variant="primary" onPress={handleSave} isDisabled={disabled}>
|
||||
{formatMessage(labels.apply)}
|
||||
</Button>
|
||||
</Row>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue