Updated hooks. Changed url, host to path, hostname.

This commit is contained in:
Mike Cao 2025-06-20 22:35:02 -07:00
parent 25a9c011b3
commit 543674c7f2
146 changed files with 23348 additions and 2533 deletions

View file

@ -31,7 +31,7 @@ export function DataGrid({
const { page, pageSize, count, data } = result || {};
const { search } = params || {};
const hasData = Boolean(!isLoading && data?.length);
const { router, renderUrl } = useNavigation();
const { router, updateParams } = useNavigation();
const handleSearch = (search: string) => {
setParams({ ...params, search });
@ -39,7 +39,7 @@ export function DataGrid({
const handlePageChange = (page: number) => {
setParams({ ...params, page });
router.push(renderUrl({ page }));
router.push(updateParams({ page }));
};
return (

View file

@ -47,7 +47,7 @@ export function FilterEditForm({ websiteId, data = [], onChange, onClose }: Filt
};
return (
<Grid columns="160px 1fr" width="760px" gapY="6">
<Grid columns="160px 1fr" width="760px" overflow="hidden" gapY="6">
<Row gridColumn="span 2">
<Heading>{formatMessage(labels.filters)}</Heading>
</Row>
@ -64,7 +64,13 @@ export function FilterEditForm({ websiteId, data = [], onChange, onClose }: Filt
})}
</List>
</Column>
<Column paddingLeft="6" overflow="auto" gapY="4">
<Column
paddingLeft="6"
overflow="auto"
gapY="4"
maxHeight="600px"
style={{ contain: 'layout' }}
>
{filters.map(filter => {
return (
<FilterRecord

View file

@ -24,7 +24,7 @@ export function FilterLink({
className,
}: FilterLinkProps) {
const { formatMessage, labels } = useMessages();
const { renderUrl, query } = useNavigation();
const { updateParams, query } = useNavigation();
const active = query[id] !== undefined;
const selected = query[id] === value;
@ -38,7 +38,7 @@ export function FilterLink({
{children}
{!value && `(${label || formatMessage(labels.unknown)})`}
{value && (
<Link href={renderUrl({ [id]: `eq.${value}` })} className={styles.label} replace>
<Link href={updateParams({ [id]: `eq.${value}` })} className={styles.label} replace>
{label || value}
</Link>
)}

View file

@ -1,19 +1,25 @@
import { getMinimumUnit, parseDateRange } from '@/lib/date';
import { setItem } from '@/lib/storage';
import { DATE_RANGE_CONFIG, DEFAULT_DATE_COMPARE, DEFAULT_DATE_RANGE } from '@/lib/constants';
import { useWebsites, setWebsiteDateRange, setWebsiteDateCompare } from '@/store/websites';
import { useApp, setDateRange } from '@/store/app';
import { setWebsiteDateCompare, setWebsiteDateRange, useWebsites } from '@/store/websites';
import { setDateRange, useApp } from '@/store/app';
import { DateRange } from '@/lib/types';
import { useLocale } from './useLocale';
import { useApi } from './useApi';
import { useNavigation } from './useNavigation';
export function useDateRange(websiteId?: string) {
const { get } = useApi();
const { locale } = useLocale();
const {
query: { date },
} = useNavigation();
const websiteConfig = useWebsites(state => state[websiteId]?.dateRange);
const defaultConfig = DEFAULT_DATE_RANGE;
const globalConfig = useApp(state => state.dateRange);
const dateRange = parseDateRange(websiteConfig || globalConfig || defaultConfig, locale);
const dateRange = parseDateRange(
date || websiteConfig || globalConfig || DEFAULT_DATE_RANGE,
locale,
);
const dateCompare = useWebsites(state => state[websiteId]?.dateCompare || DEFAULT_DATE_COMPARE);
const saveDateRange = async (value: DateRange | string) => {

View file

@ -4,17 +4,17 @@ export function useFields() {
const { formatMessage, labels } = useMessages();
const fields = [
{ name: 'url', type: 'string', label: formatMessage(labels.url) },
{ name: 'path', type: 'string', label: formatMessage(labels.path) },
{ name: 'title', type: 'string', label: formatMessage(labels.pageTitle) },
{ name: 'referrer', type: 'string', label: formatMessage(labels.referrer) },
{ name: 'query', type: 'string', label: formatMessage(labels.query) },
//{ name: 'query', type: 'string', label: formatMessage(labels.query) },
{ name: 'browser', type: 'string', label: formatMessage(labels.browser) },
{ name: 'os', type: 'string', label: formatMessage(labels.os) },
{ name: 'device', type: 'string', label: formatMessage(labels.device) },
{ name: 'country', type: 'string', label: formatMessage(labels.country) },
{ name: 'region', type: 'string', label: formatMessage(labels.region) },
{ name: 'city', type: 'string', label: formatMessage(labels.city) },
{ name: 'host', type: 'string', label: formatMessage(labels.host) },
{ name: 'hostname', type: 'string', label: formatMessage(labels.hostname) },
{ name: 'tag', type: 'string', label: formatMessage(labels.tag) },
];

View file

@ -1,45 +1,32 @@
import { useMessages } from './useMessages';
import { useNavigation } from '@/components/hooks/useNavigation';
import { FILTER_COLUMNS, OPERATORS } from '@/lib/constants';
import { safeDecodeURIComponent } from '@/lib/url';
import { useMessages } from './useMessages';
import { useNavigation } from './useNavigation';
import { useFields } from './useFields';
export function useFilters() {
const { formatMessage, labels } = useMessages();
const { query } = useNavigation();
const fields = [
{ name: 'url', type: 'string', label: formatMessage(labels.url) },
{ name: 'title', type: 'string', label: formatMessage(labels.pageTitle) },
{ name: 'referrer', type: 'string', label: formatMessage(labels.referrer) },
{ name: 'query', type: 'string', label: formatMessage(labels.query) },
{ name: 'browser', type: 'string', label: formatMessage(labels.browser) },
{ name: 'os', type: 'string', label: formatMessage(labels.os) },
{ name: 'device', type: 'string', label: formatMessage(labels.device) },
{ name: 'country', type: 'string', label: formatMessage(labels.country) },
{ name: 'region', type: 'string', label: formatMessage(labels.region) },
{ name: 'city', type: 'string', label: formatMessage(labels.city) },
{ name: 'host', type: 'string', label: formatMessage(labels.host) },
{ name: 'tag', type: 'string', label: formatMessage(labels.tag) },
];
const { fields } = useFields();
const operators = [
{ name: 'eq', type: 'string', label: 'Is' },
{ name: 'neq', type: 'string', label: 'Is not' },
{ name: 'c', type: 'string', label: 'Contains' },
{ name: 'dnc', type: 'string', label: 'Does not contain' },
{ name: 'c', type: 'array', label: 'Contains' },
{ name: 'dnc', type: 'array', label: 'Does not contain' },
{ name: 't', type: 'boolean', label: 'True' },
{ name: 'f', type: 'boolean', label: 'False' },
{ name: 'eq', type: 'number', label: 'Is' },
{ name: 'neq', type: 'number', label: 'Is not' },
{ name: 'gt', type: 'number', label: 'Greater than' },
{ name: 'lt', type: 'number', label: 'Less than' },
{ name: 'gte', type: 'number', label: 'Greater than or equals' },
{ name: 'lte', type: 'number', label: 'Less than or equals' },
{ name: 'bf', type: 'date', label: 'Before' },
{ name: 'af', type: 'date', label: 'After' },
{ name: 'eq', type: 'uuid', label: 'Is' },
{ name: 'eq', type: 'string', label: formatMessage(labels.is) },
{ name: 'neq', type: 'string', label: formatMessage(labels.isNot) },
{ name: 'c', type: 'string', label: formatMessage(labels.contains) },
{ name: 'dnc', type: 'string', label: formatMessage(labels.doesNotContain) },
{ name: 'i', type: 'array', label: formatMessage(labels.includes) },
{ name: 'dni', type: 'array', label: formatMessage(labels.doesNotInclude) },
{ name: 't', type: 'boolean', label: formatMessage(labels.isTrue) },
{ name: 'f', type: 'boolean', label: formatMessage(labels.isFalse) },
{ name: 'eq', type: 'number', label: formatMessage(labels.is) },
{ name: 'neq', type: 'number', label: formatMessage(labels.isNot) },
{ name: 'gt', type: 'number', label: formatMessage(labels.greaterThan) },
{ name: 'lt', type: 'number', label: formatMessage(labels.lessThan) },
{ name: 'gte', type: 'number', label: formatMessage(labels.greaterThanEquals) },
{ name: 'lte', type: 'number', label: formatMessage(labels.lessThanEquals) },
{ name: 'bf', type: 'date', label: formatMessage(labels.before) },
{ name: 'af', type: 'date', label: formatMessage(labels.after) },
{ name: 'eq', type: 'uuid', label: formatMessage(labels.is) },
];
const operatorLabels = {

View file

@ -1,31 +1,24 @@
import { useMemo } from 'react';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { buildUrl } from '@/lib/url';
export function useNavigation() {
const router = useRouter();
const pathname = usePathname();
const params = useSearchParams();
const searchParams = useSearchParams();
const [, teamId] = pathname.match(/\/teams\/([a-f0-9-]+)/) || [];
const [, websiteId] = pathname.match(/\/websites\/([a-f0-9-]+)/) || [];
const query = Object.fromEntries(searchParams);
const query = useMemo<{ [key: string]: any }>(() => {
const obj = {};
for (const [key, value] of params.entries()) {
obj[key] = value;
}
return obj;
}, [params]);
function renderUrl(params: any) {
const updateParams = (params?: { [key: string]: string | number }) => {
return !params ? pathname : buildUrl(pathname, { ...query, ...params });
}
};
function renderTeamUrl(url: string) {
return teamId ? `/teams/${teamId}${url}` : url;
}
const renderUrl = (path: string, params?: { [key: string]: string | number } | false) => {
return buildUrl(
teamId ? `/teams/${teamId}${path}` : path,
params === false ? {} : { ...query, ...params },
);
};
return { pathname, query, router, renderUrl, renderTeamUrl, teamId, websiteId };
return { router, pathname, searchParams, query, teamId, websiteId, updateParams, renderUrl };
}

View file

@ -7,16 +7,16 @@ import { isSearchOperator } from '@/lib/params';
export function FilterBar() {
const { formatMessage, labels } = useMessages();
const { formatValue } = useFormat();
const { router, renderUrl } = useNavigation();
const { router, updateParams } = useNavigation();
const { filters, operatorLabels } = useFilters();
const handleCloseFilter = (param: string, e: MouseEvent) => {
e.stopPropagation();
router.push(renderUrl({ [param]: undefined }));
router.push(updateParams({ [param]: undefined }));
};
const handleResetFilter = () => {
router.push(renderUrl(false));
router.push(updateParams());
};
if (!filters.length) {
@ -24,7 +24,7 @@ export function FilterBar() {
}
return (
<Row gap alignItems="center" justifyContent="space-between" padding backgroundColor="3">
<Row gap alignItems="center" justifyContent="space-between" padding="2" backgroundColor="3">
<Row alignItems="center" gap="2" wrap="wrap">
{Object.keys(filters).map(key => {
const filter = filters[key];

View file

@ -12,7 +12,6 @@ import { isAfter } from 'date-fns';
import { Chevron, Close, Compare } from '@/components/icons';
import { useDateRange, useMessages, useNavigation } from '@/components/hooks';
import { getOffsetDateRange } from '@/lib/date';
import { DateRange } from '@/lib/types';
import { DateFilter } from './DateFilter';
export function WebsiteDateFilter({
@ -32,7 +31,7 @@ export function WebsiteDateFilter({
const { formatMessage, labels } = useMessages();
const {
router,
renderUrl,
updateParams,
query: { compare },
} = useNavigation();
const isAllTime = value === 'all';
@ -40,22 +39,22 @@ export function WebsiteDateFilter({
const disableForward = value === 'all' || isAfter(endDate, new Date());
const handleChange = (date: string | DateRange) => {
router.push(renderUrl({ date }));
const handleChange = (date: string) => {
router.push(updateParams({ date }));
saveDateRange(date);
};
const handleIncrement = (increment: number) => {
router.push(renderUrl({ offset: offset + increment }));
router.push(updateParams({ offset: offset + increment }));
saveDateRange(getOffsetDateRange(dateRange, increment));
};
const handleSelect = (compare: any) => {
router.push(renderUrl({ compare }));
router.push(updateParams({ compare }));
};
const handleCompare = () => {
router.push(renderUrl({ compare: compare ? undefined : 'prev' }));
router.push(updateParams({ compare: compare ? undefined : 'prev' }));
};
return (

View file

@ -94,7 +94,6 @@ export const labels = defineMessages({
entry: { id: 'label.entry', defaultMessage: 'Entry path' },
exit: { id: 'label.exit', defaultMessage: 'Exit path' },
referrers: { id: 'label.referrers', defaultMessage: 'Referrers' },
hosts: { id: 'label.hosts', defaultMessage: 'Hosts' },
screens: { id: 'label.screens', defaultMessage: 'Screens' },
browsers: { id: 'label.browsers', defaultMessage: 'Browsers' },
os: { id: 'label.os', defaultMessage: 'OS' },
@ -177,8 +176,6 @@ export const labels = defineMessages({
},
currency: { id: 'label.currency', defaultMessage: 'Currency' },
model: { id: 'label.model', defaultMessage: 'Model' },
url: { id: 'label.url', defaultMessage: 'URL' },
urls: { id: 'label.urls', defaultMessage: 'URLs' },
path: { id: 'label.path', defaultMessage: 'Path' },
paths: { id: 'label.paths', defaultMessage: 'Paths' },
add: { id: 'label.add', defaultMessage: 'Add' },
@ -206,8 +203,14 @@ export const labels = defineMessages({
lessThanEquals: { id: 'label.less-than-equals', defaultMessage: 'Less than or equals' },
contains: { id: 'label.contains', defaultMessage: 'Contains' },
doesNotContain: { id: 'label.does-not-contain', defaultMessage: 'Does not contain' },
includes: { id: 'label.includes', defaultMessage: 'Includes' },
doesNotInclude: { id: 'label.does-not-include', defaultMessage: 'Does not include' },
before: { id: 'label.before', defaultMessage: 'Before' },
after: { id: 'label.after', defaultMessage: 'After' },
isTrue: { id: 'label.is-true', defaultMessage: 'Is true' },
isFalse: { id: 'label.is-false', defaultMessage: 'Is false' },
exists: { id: 'label.exists', defaultMessage: 'Exists' },
doesNotExist: { id: 'label.doest-not-exist', defaultMessage: 'Does not exist' },
total: { id: 'label.total', defaultMessage: 'Total' },
min: { id: 'label.min', defaultMessage: 'Min' },
max: { id: 'label.max', defaultMessage: 'Max' },
@ -228,7 +231,7 @@ export const labels = defineMessages({
},
dropoff: { id: 'label.dropoff', defaultMessage: 'Dropoff' },
referrer: { id: 'label.referrer', defaultMessage: 'Referrer' },
host: { id: 'label.host', defaultMessage: 'Host' },
hostname: { id: 'label.hostname', defaultMessage: 'Hostname' },
country: { id: 'label.country', defaultMessage: 'Country' },
region: { id: 'label.region', defaultMessage: 'Region' },
city: { id: 'label.city', defaultMessage: 'City' },

View file

@ -1,9 +1,11 @@
import { useState } from 'react';
import { Button, Row, Column, Calendar, ToggleGroup, ToggleGroupItem } from '@umami/react-zen';
import { isAfter, isBefore, isSameDay, startOfDay, endOfDay } from 'date-fns';
import { FILTER_DAY, FILTER_RANGE } from '@/lib/constants';
import { useMessages } from '@/components/hooks';
const FILTER_DAY = 'filter-day';
const FILTER_RANGE = 'filter-range';
export function DatePickerForm({
startDate: defaultStartDate,
endDate: defaultEndDate,

View file

@ -3,17 +3,17 @@ import { FilterLink } from '@/components/common/FilterLink';
import { useMessages } from '@/components/hooks';
import { Flexbox } from '@umami/react-zen';
export function HostsTable(props: MetricsTableProps) {
export function HostnamesTable(props: MetricsTableProps) {
const { formatMessage, labels } = useMessages();
const renderLink = ({ x: host }) => {
const renderLink = ({ x: hostname }) => {
return (
<Flexbox alignItems="center">
<FilterLink
id="host"
value={host}
externalUrl={`https://${host}`}
label={!host && formatMessage(labels.none)}
id="hostname"
value={hostname}
externalUrl={`https://${hostname}`}
label={!hostname && formatMessage(labels.none)}
/>
</Flexbox>
);
@ -23,8 +23,8 @@ export function HostsTable(props: MetricsTableProps) {
<>
<MetricsTable
{...props}
title={formatMessage(labels.hosts)}
type="host"
title={formatMessage(labels.hostname)}
type="hostname"
metric={formatMessage(labels.visitors)}
renderLabel={renderLink}
/>

View file

@ -41,7 +41,7 @@ export function MetricsTable({
}: MetricsTableProps) {
const [search, setSearch] = useState('');
const { formatValue } = useFormat();
const { renderUrl } = useNavigation();
const { updateParams } = useNavigation();
const { formatMessage, labels } = useMessages();
const { data, isLoading, isFetching, error } = useWebsiteMetricsQuery(
@ -99,7 +99,7 @@ export function MetricsTable({
)}
<Row justifyContent="center">
{showMore && data && !error && limit && (
<LinkButton href={renderUrl({ view: type })} variant="quiet">
<LinkButton href={updateParams({ view: type })} variant="quiet">
<Text>{formatMessage(labels.more)}</Text>
<Icon size="sm">
<Arrow />

View file

@ -13,19 +13,19 @@ export interface PagesTableProps extends MetricsTableProps {
export function PagesTable({ allowFilter, ...props }: PagesTableProps) {
const {
router,
renderUrl,
query: { view = 'url' },
updateParams,
query: { view = 'path' },
} = useNavigation();
const { formatMessage, labels } = useMessages();
const { domain } = useContext(WebsiteContext);
const handleChange = (id: any) => {
router.push(renderUrl({ view: id }));
router.push(updateParams({ view: id }));
};
const buttons = [
{
id: 'url',
id: 'path',
label: formatMessage(labels.path),
},
{
@ -45,7 +45,7 @@ export function PagesTable({ allowFilter, ...props }: PagesTableProps) {
const renderLink = ({ x }) => {
return (
<FilterLink
id={view === 'entry' || view === 'exit' ? 'url' : view}
id={view === 'entry' || view === 'exit' ? 'path' : view}
value={x}
label={!x && formatMessage(labels.none)}
externalUrl={

View file

@ -1,10 +1,12 @@
import { useState } from 'react';
import { Row, Text } from '@umami/react-zen';
import { FilterButtons } from '@/components/common/FilterButtons';
import { emptyFilter, paramFilter } from '@/lib/filters';
import { FILTER_RAW, FILTER_COMBINED } from '@/lib/constants';
import { MetricsTable, MetricsTableProps } from './MetricsTable';
import { useMessages } from '@/components/hooks';
import styles from './QueryParametersTable.module.css';
const FILTER_COMBINED = 'filter-combined';
const FILTER_RAW = 'filter-raw';
const filters = {
[FILTER_RAW]: emptyFilter,
@ -26,6 +28,27 @@ export function QueryParametersTable({
{ id: FILTER_RAW, label: formatMessage(labels.filterRaw) },
];
const renderLabel = ({ x, p, v }) => {
return (
<Row alignItems="center" maxWidth="600px" gap>
{filter === FILTER_RAW ? (
<Text truncate title={x}>
{x}
</Text>
) : (
<>
<Text color="primary" weight="bold">
{p}
</Text>
<Text truncate title={v}>
{v}
</Text>
</>
)}
</Row>
);
};
return (
<MetricsTable
{...props}
@ -33,16 +56,7 @@ export function QueryParametersTable({
type="query"
metric={formatMessage(labels.views)}
dataFilter={filters[filter]}
renderLabel={({ x, p, v }) =>
filter === FILTER_RAW ? (
x
) : (
<div className={styles.item}>
<div className={styles.param}>{p}</div>
<div className={styles.value}>{v}</div>
</div>
)
}
renderLabel={renderLabel}
delay={0}
>
{allowFilter && <FilterButtons items={buttons} value={filter} onChange={setFilter} />}

View file

@ -14,13 +14,13 @@ export interface ReferrersTableProps extends MetricsTableProps {
export function ReferrersTable({ allowFilter, ...props }: ReferrersTableProps) {
const {
router,
renderUrl,
updateParams,
query: { view = 'referrer' },
} = useNavigation();
const { formatMessage, labels } = useMessages();
const handleSelect = (key: any) => {
router.push(renderUrl({ view: key }));
router.push(updateParams({ view: key }));
};
const buttons = [