mirror of
https://github.com/umami-software/umami.git
synced 2026-02-09 07:07:17 +01:00
Merge branch 'dev' of https://github.com/umami-software/umami into dev
This commit is contained in:
commit
4680c89e28
52 changed files with 498 additions and 118 deletions
|
|
@ -19,7 +19,7 @@ export function useWebsiteExpandedMetricsQuery(
|
|||
options?: ReactQueryOptions<WebsiteExpandedMetricsData>,
|
||||
) {
|
||||
const { get, useQuery } = useApi();
|
||||
const { startAt, endAt, unit, timezone } = useDateParameters();
|
||||
const { startAt, endAt } = useDateParameters();
|
||||
const filters = useFilterParameters();
|
||||
|
||||
return useQuery<WebsiteExpandedMetricsData>({
|
||||
|
|
@ -29,8 +29,6 @@ export function useWebsiteExpandedMetricsQuery(
|
|||
websiteId,
|
||||
startAt,
|
||||
endAt,
|
||||
unit,
|
||||
timezone,
|
||||
...filters,
|
||||
...params,
|
||||
},
|
||||
|
|
@ -39,8 +37,6 @@ export function useWebsiteExpandedMetricsQuery(
|
|||
get(`/websites/${websiteId}/metrics/expanded`, {
|
||||
startAt,
|
||||
endAt,
|
||||
unit,
|
||||
timezone,
|
||||
...filters,
|
||||
...params,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export function useWebsiteMetricsQuery(
|
|||
options?: ReactQueryOptions<WebsiteMetricsData>,
|
||||
) {
|
||||
const { get, useQuery } = useApi();
|
||||
const { startAt, endAt, unit, timezone } = useDateParameters();
|
||||
const { startAt, endAt } = useDateParameters();
|
||||
const filters = useFilterParameters();
|
||||
|
||||
return useQuery<WebsiteMetricsData>({
|
||||
|
|
@ -25,8 +25,6 @@ export function useWebsiteMetricsQuery(
|
|||
websiteId,
|
||||
startAt,
|
||||
endAt,
|
||||
unit,
|
||||
timezone,
|
||||
...filters,
|
||||
...params,
|
||||
},
|
||||
|
|
@ -35,8 +33,6 @@ export function useWebsiteMetricsQuery(
|
|||
get(`/websites/${websiteId}/metrics`, {
|
||||
startAt,
|
||||
endAt,
|
||||
unit,
|
||||
timezone,
|
||||
...filters,
|
||||
...params,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import type { UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useDateParameters } from '@/components/hooks/useDateParameters';
|
||||
import { useDateRange } from '@/components/hooks/useDateRange';
|
||||
import { useApi } from '../useApi';
|
||||
import { useFilterParameters } from '../useFilterParameters';
|
||||
|
||||
|
|
@ -20,21 +19,16 @@ export interface WebsiteStatsData {
|
|||
}
|
||||
|
||||
export function useWebsiteStatsQuery(
|
||||
websiteId: string,
|
||||
{ websiteId, compare }: { websiteId: string; compare?: string },
|
||||
options?: UseQueryOptions<WebsiteStatsData, Error, WebsiteStatsData>,
|
||||
) {
|
||||
const { get, useQuery } = useApi();
|
||||
const { startAt, endAt, unit, timezone } = useDateParameters();
|
||||
const { compare } = useDateRange();
|
||||
const { startAt, endAt } = useDateParameters();
|
||||
const filters = useFilterParameters();
|
||||
|
||||
return useQuery<WebsiteStatsData>({
|
||||
queryKey: [
|
||||
'websites:stats',
|
||||
{ websiteId, startAt, endAt, unit, timezone, compare, ...filters },
|
||||
],
|
||||
queryFn: () =>
|
||||
get(`/websites/${websiteId}/stats`, { startAt, endAt, unit, timezone, compare, ...filters }),
|
||||
queryKey: ['websites:stats', { websiteId, compare, startAt, endAt, ...filters }],
|
||||
queryFn: () => get(`/websites/${websiteId}/stats`, { compare, startAt, endAt, ...filters }),
|
||||
enabled: !!websiteId,
|
||||
...options,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,13 +12,12 @@ export function useWeeklyTrafficQuery(websiteId: string, params?: Record<string,
|
|||
return useQuery({
|
||||
queryKey: [
|
||||
'sessions',
|
||||
{ websiteId, modified, startAt, endAt, unit, timezone, ...params, ...filters },
|
||||
{ websiteId, modified, startAt, endAt, timezone, ...params, ...filters },
|
||||
],
|
||||
queryFn: () => {
|
||||
return get(`/websites/${websiteId}/sessions/weekly`, {
|
||||
startAt,
|
||||
endAt,
|
||||
unit,
|
||||
timezone,
|
||||
...params,
|
||||
...filters,
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@ import { getItem } from '@/lib/storage';
|
|||
|
||||
export function useDateRange(options: { ignoreOffset?: boolean; timezone?: string } = {}) {
|
||||
const {
|
||||
query: { date = '', offset = 0, compare = 'prev' },
|
||||
query: { date = '', unit = '', offset = 0, compare = 'prev' },
|
||||
} = useNavigation();
|
||||
const { locale } = useLocale();
|
||||
|
||||
const dateRange = useMemo(() => {
|
||||
const dateRangeObject = parseDateRange(
|
||||
date || getItem(DATE_RANGE_CONFIG) || DEFAULT_DATE_RANGE_VALUE,
|
||||
unit,
|
||||
locale,
|
||||
options.timezone,
|
||||
);
|
||||
|
|
@ -21,12 +21,13 @@ export function useDateRange(options: { ignoreOffset?: boolean; timezone?: strin
|
|||
return !options.ignoreOffset && offset
|
||||
? getOffsetDateRange(dateRangeObject, +offset)
|
||||
: dateRangeObject;
|
||||
}, [date, offset, options]);
|
||||
}, [date, unit, offset, options]);
|
||||
|
||||
const dateCompare = getCompareDate(compare, dateRange.startDate, dateRange.endDate);
|
||||
|
||||
return {
|
||||
date,
|
||||
unit,
|
||||
offset,
|
||||
compare,
|
||||
isAllTime: date.endsWith(`:all`),
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export function useFields() {
|
|||
{ name: 'region', type: 'string', label: formatMessage(labels.region) },
|
||||
{ name: 'city', type: 'string', label: formatMessage(labels.city) },
|
||||
{ name: 'hostname', type: 'string', label: formatMessage(labels.hostname) },
|
||||
{ name: 'distinctId', type: 'string', label: formatMessage(labels.distinctId) },
|
||||
{ name: 'tag', type: 'string', label: formatMessage(labels.tag) },
|
||||
{ name: 'event', type: 'string', label: formatMessage(labels.event) },
|
||||
];
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export function useFilterParameters() {
|
|||
event,
|
||||
tag,
|
||||
hostname,
|
||||
distinctId,
|
||||
page,
|
||||
pageSize,
|
||||
search,
|
||||
|
|
@ -42,6 +43,7 @@ export function useFilterParameters() {
|
|||
event,
|
||||
tag,
|
||||
hostname,
|
||||
distinctId,
|
||||
search,
|
||||
segment,
|
||||
cohort,
|
||||
|
|
@ -61,6 +63,7 @@ export function useFilterParameters() {
|
|||
event,
|
||||
tag,
|
||||
hostname,
|
||||
distinctId,
|
||||
page,
|
||||
pageSize,
|
||||
search,
|
||||
|
|
|
|||
|
|
@ -61,7 +61,9 @@ export function FilterEditForm({ websiteId, onChange, onClose }: FilterEditFormP
|
|||
websiteId={websiteId}
|
||||
value={currentFilters}
|
||||
onChange={setCurrentFilters}
|
||||
exclude={excludeFilters ? ['path', 'title', 'hostname', 'tag', 'event'] : []}
|
||||
exclude={
|
||||
excludeFilters ? ['path', 'title', 'hostname', 'distinctId', 'tag', 'event'] : []
|
||||
}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel id="segments">
|
||||
|
|
|
|||
71
src/components/input/UnitFilter.tsx
Normal file
71
src/components/input/UnitFilter.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { ListItem, Row, Select } from '@umami/react-zen';
|
||||
import { useMessages, useNavigation } from '@/components/hooks';
|
||||
import { DATE_RANGE_CONFIG, DEFAULT_DATE_RANGE_VALUE } from '@/lib/constants';
|
||||
import { getItem } from '@/lib/storage';
|
||||
|
||||
export function UnitFilter() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { router, query, updateParams } = useNavigation();
|
||||
|
||||
const DATE_RANGE_UNIT_CONFIG = {
|
||||
'0week': {
|
||||
defaultUnit: 'day',
|
||||
availableUnits: ['day', 'hour'],
|
||||
},
|
||||
'7day': {
|
||||
defaultUnit: 'day',
|
||||
availableUnits: ['day', 'hour'],
|
||||
},
|
||||
'0month': {
|
||||
defaultUnit: 'day',
|
||||
availableUnits: ['day', 'hour'],
|
||||
},
|
||||
'30day': {
|
||||
defaultUnit: 'day',
|
||||
availableUnits: ['day', 'hour'],
|
||||
},
|
||||
'90day': {
|
||||
defaultUnit: 'day',
|
||||
availableUnits: ['day', 'month'],
|
||||
},
|
||||
'6month': {
|
||||
defaultUnit: 'month',
|
||||
availableUnits: ['month', 'day'],
|
||||
},
|
||||
};
|
||||
|
||||
const unitConfig =
|
||||
DATE_RANGE_UNIT_CONFIG[query.date || getItem(DATE_RANGE_CONFIG) || DEFAULT_DATE_RANGE_VALUE];
|
||||
|
||||
if (!unitConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
router.push(updateParams({ unit: value }));
|
||||
};
|
||||
|
||||
const options = unitConfig.availableUnits.map(unit => ({
|
||||
id: unit,
|
||||
label: formatMessage(labels[unit]),
|
||||
}));
|
||||
|
||||
const selectedUnit = query.unit ?? unitConfig.defaultUnit;
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<Select
|
||||
value={selectedUnit}
|
||||
onChange={handleChange}
|
||||
popoverProps={{ placement: 'bottom right' }}
|
||||
style={{ width: 100 }}
|
||||
>
|
||||
{options.map(({ id, label }) => (
|
||||
<ListItem key={id} id={id}>
|
||||
{label}
|
||||
</ListItem>
|
||||
))}
|
||||
</Select>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
71
src/components/input/UserSelect.tsx
Normal file
71
src/components/input/UserSelect.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { ListItem, Row, Select, type SelectProps, Text } from '@umami/react-zen';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Empty } from '@/components/common/Empty';
|
||||
import { useMessages, useTeamMembersQuery, useUsersQuery } from '@/components/hooks';
|
||||
|
||||
export function UserSelect({
|
||||
teamId,
|
||||
onChange,
|
||||
...props
|
||||
}: {
|
||||
teamId?: string;
|
||||
} & SelectProps) {
|
||||
const { formatMessage, messages } = useMessages();
|
||||
const { data: users, isLoading: usersLoading } = useUsersQuery();
|
||||
const { data: teamMembers, isLoading: teamMembersLoading } = useTeamMembersQuery(teamId);
|
||||
const [username, setUsername] = useState<string>();
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const listItems = useMemo(() => {
|
||||
if (!users) {
|
||||
return [];
|
||||
}
|
||||
if (!teamId || !teamMembers) {
|
||||
return users.data;
|
||||
}
|
||||
const teamMemberIds = teamMembers.data.map(({ userId }) => userId);
|
||||
return users.data.filter(({ id }) => !teamMemberIds.includes(id));
|
||||
}, [users, teamMembers, teamId]);
|
||||
|
||||
const handleSearch = (value: string) => {
|
||||
setSearch(value);
|
||||
};
|
||||
|
||||
const handleOpenChange = () => {
|
||||
setSearch('');
|
||||
};
|
||||
|
||||
const handleChange = (id: string) => {
|
||||
setUsername(listItems.find(item => item.id === id)?.username);
|
||||
onChange(id);
|
||||
};
|
||||
|
||||
const renderValue = () => {
|
||||
return (
|
||||
<Row maxWidth="160px">
|
||||
<Text truncate>{username}</Text>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Select
|
||||
{...props}
|
||||
items={listItems}
|
||||
value={username}
|
||||
isLoading={usersLoading || (teamId && teamMembersLoading)}
|
||||
allowSearch={true}
|
||||
searchValue={search}
|
||||
onSearch={handleSearch}
|
||||
onChange={handleChange}
|
||||
onOpenChange={handleOpenChange}
|
||||
renderValue={renderValue}
|
||||
listProps={{
|
||||
renderEmptyState: () => <Empty message={formatMessage(messages.noResultsFound)} />,
|
||||
style: { maxHeight: 'calc(42vh - 65px)' },
|
||||
}}
|
||||
>
|
||||
{({ id, username }: any) => <ListItem key={id}>{username}</ListItem>}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
|
@ -41,7 +41,7 @@ export function WebsiteDateFilter({
|
|||
}),
|
||||
);
|
||||
} else {
|
||||
router.push(updateParams({ date, offset: undefined }));
|
||||
router.push(updateParams({ date, offset: undefined, unit: undefined }));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -245,7 +245,10 @@ export const labels = defineMessages({
|
|||
tag: { id: 'label.tag', defaultMessage: 'Tag' },
|
||||
segment: { id: 'label.segment', defaultMessage: 'Segment' },
|
||||
cohort: { id: 'label.cohort', defaultMessage: 'Cohort' },
|
||||
minute: { id: 'label.minute', defaultMessage: 'Minute' },
|
||||
hour: { id: 'label.hour', defaultMessage: 'Hour' },
|
||||
day: { id: 'label.day', defaultMessage: 'Day' },
|
||||
month: { id: 'label.month', defaultMessage: 'Month' },
|
||||
date: { id: 'label.date', defaultMessage: 'Date' },
|
||||
pageOf: { id: 'label.page-of', defaultMessage: 'Page {current} of {total}' },
|
||||
create: { id: 'label.create', defaultMessage: 'Create' },
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export const MetricCard = ({
|
|||
showChange = false,
|
||||
}: MetricCardProps) => {
|
||||
const diff = value - change;
|
||||
const pct = ((value - diff) / diff) * 100;
|
||||
const pct = diff !== 0 ? ((value - diff) / diff) * 100 : value !== 0 ? 100 : 0;
|
||||
const props = useSpring({ x: Number(value) || 0, from: { x: 0 } });
|
||||
const changeProps = useSpring({ x: Number(pct) || 0, from: { x: 0 } });
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue