mirror of
https://github.com/kremalicious/umami.git
synced 2024-11-22 09:57:00 +01:00
Added search to metrics table.
This commit is contained in:
parent
3a28fea8ac
commit
cad719fd23
@ -6,7 +6,7 @@ import FilterTags from 'components/metrics/FilterTags';
|
|||||||
import useNavigation from 'components/hooks/useNavigation';
|
import useNavigation from 'components/hooks/useNavigation';
|
||||||
import { useWebsite } from 'components/hooks';
|
import { useWebsite } from 'components/hooks';
|
||||||
import WebsiteChart from './WebsiteChart';
|
import WebsiteChart from './WebsiteChart';
|
||||||
import WebsiteMenuView from './WebsiteMenuView';
|
import WebsiteExpandedView from './WebsiteExpandedView';
|
||||||
import WebsiteHeader from './WebsiteHeader';
|
import WebsiteHeader from './WebsiteHeader';
|
||||||
import WebsiteMetricsBar from './WebsiteMetricsBar';
|
import WebsiteMetricsBar from './WebsiteMetricsBar';
|
||||||
import WebsiteTableView from './WebsiteTableView';
|
import WebsiteTableView from './WebsiteTableView';
|
||||||
@ -34,7 +34,7 @@ export default function WebsiteDetails({ websiteId }: { websiteId: string }) {
|
|||||||
{website && (
|
{website && (
|
||||||
<>
|
<>
|
||||||
{!view && <WebsiteTableView websiteId={websiteId} />}
|
{!view && <WebsiteTableView websiteId={websiteId} />}
|
||||||
{view && <WebsiteMenuView websiteId={websiteId} />}
|
{view && <WebsiteExpandedView websiteId={websiteId} />}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -15,7 +15,7 @@ import SideNav from 'components/layout/SideNav';
|
|||||||
import useNavigation from 'components/hooks/useNavigation';
|
import useNavigation from 'components/hooks/useNavigation';
|
||||||
import useMessages from 'components/hooks/useMessages';
|
import useMessages from 'components/hooks/useMessages';
|
||||||
import LinkButton from 'components/common/LinkButton';
|
import LinkButton from 'components/common/LinkButton';
|
||||||
import styles from './WebsiteMenuView.module.css';
|
import styles from './WebsiteExpandedView.module.css';
|
||||||
|
|
||||||
const views = {
|
const views = {
|
||||||
url: PagesTable,
|
url: PagesTable,
|
||||||
@ -33,7 +33,7 @@ const views = {
|
|||||||
query: QueryParametersTable,
|
query: QueryParametersTable,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function WebsiteMenuView({
|
export default function WebsiteExpandedView({
|
||||||
websiteId,
|
websiteId,
|
||||||
websiteDomain,
|
websiteDomain,
|
||||||
}: {
|
}: {
|
||||||
@ -113,11 +113,11 @@ export default function WebsiteMenuView({
|
|||||||
|
|
||||||
const DetailsComponent = views[view] || (() => null);
|
const DetailsComponent = views[view] || (() => null);
|
||||||
|
|
||||||
const handleChange = view => {
|
const handleChange = (view: any) => {
|
||||||
router.push(makeUrl({ view }));
|
router.push(makeUrl({ view }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderValue = value => items.find(({ key }) => key === value)?.label;
|
const renderValue = (value: string) => items.find(({ key }) => key === value)?.label;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.layout}>
|
<div className={styles.layout}>
|
||||||
@ -146,9 +146,10 @@ export default function WebsiteMenuView({
|
|||||||
websiteDomain={websiteDomain}
|
websiteDomain={websiteDomain}
|
||||||
limit={false}
|
limit={false}
|
||||||
animate={false}
|
animate={false}
|
||||||
showFilters={true}
|
|
||||||
virtualize={true}
|
virtualize={true}
|
||||||
itemCount={25}
|
itemCount={25}
|
||||||
|
allowFilter={true}
|
||||||
|
allowSearch={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@ -46,7 +46,7 @@ export function useDateRange(websiteId?: string) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return [dateRange, saveDateRange] as [
|
return [dateRange, saveDateRange] as [
|
||||||
{ startDate: Date; endDate: Date },
|
{ startDate: Date; endDate: Date; modified?: number },
|
||||||
(value: string | DateRange) => void,
|
(value: string | DateRange) => void,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -18,14 +18,19 @@ export function useFormat() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const formatRegion = (value: string): string => {
|
const formatRegion = (value: string): string => {
|
||||||
return regions[value] ? regions[value] : value;
|
const [country] = value.split('-');
|
||||||
|
return regions[value] ? `${regions[value]}, ${countryNames[country]}` : value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatCity = (value: string, country?: string): string => {
|
||||||
|
return `${value}, ${countryNames[country]}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatDevice = (value: string): string => {
|
const formatDevice = (value: string): string => {
|
||||||
return formatMessage(labels[value] || labels.unknown);
|
return formatMessage(labels[value] || labels.unknown);
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatValue = (value: string, type: string): string => {
|
const formatValue = (value: string, type: string, data?: { [key: string]: any }): string => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'browser':
|
case 'browser':
|
||||||
return formatBrowser(value);
|
return formatBrowser(value);
|
||||||
@ -33,6 +38,8 @@ export function useFormat() {
|
|||||||
return formatCountry(value);
|
return formatCountry(value);
|
||||||
case 'region':
|
case 'region':
|
||||||
return formatRegion(value);
|
return formatRegion(value);
|
||||||
|
case 'city':
|
||||||
|
return formatCity(value, data?.country);
|
||||||
case 'device':
|
case 'device':
|
||||||
return formatDevice(value);
|
return formatDevice(value);
|
||||||
default:
|
default:
|
||||||
|
@ -9,7 +9,7 @@ export interface SideNavProps {
|
|||||||
items: any[];
|
items: any[];
|
||||||
shallow?: boolean;
|
shallow?: boolean;
|
||||||
scroll?: boolean;
|
scroll?: boolean;
|
||||||
className?: boolean;
|
className?: string;
|
||||||
onSelect?: () => void;
|
onSelect?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,8 +11,8 @@ export function CitiesTable(props: MetricsTableProps) {
|
|||||||
const countryNames = useCountryNames(locale);
|
const countryNames = useCountryNames(locale);
|
||||||
|
|
||||||
const renderLabel = (city: string, country: string) => {
|
const renderLabel = (city: string, country: string) => {
|
||||||
const name = countryNames[country];
|
const countryName = countryNames[country];
|
||||||
return name ? `${city}, ${name}` : city;
|
return countryName ? `${city}, ${countryName}` : city;
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderLink = ({ x: city, country }) => {
|
const renderLink = ({ x: city, country }) => {
|
||||||
|
@ -6,13 +6,33 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search {
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 992px) {
|
@media only screen and (max-width: 992px) {
|
||||||
.container {
|
.container {
|
||||||
min-height: auto;
|
min-height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useMemo } from 'react';
|
import { ReactNode, useMemo, useState } from 'react';
|
||||||
import { Loading, Icon, Text } from 'react-basics';
|
import { Loading, Icon, Text, SearchField } from 'react-basics';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import useApi from 'components/hooks/useApi';
|
import useApi from 'components/hooks/useApi';
|
||||||
import { percentFilter } from 'lib/filters';
|
import { percentFilter } from 'lib/filters';
|
||||||
@ -12,6 +12,7 @@ import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
|
|||||||
import Icons from 'components/icons';
|
import Icons from 'components/icons';
|
||||||
import useMessages from 'components/hooks/useMessages';
|
import useMessages from 'components/hooks/useMessages';
|
||||||
import useLocale from 'components/hooks/useLocale';
|
import useLocale from 'components/hooks/useLocale';
|
||||||
|
import useFormat from 'components//hooks/useFormat';
|
||||||
import styles from './MetricsTable.module.css';
|
import styles from './MetricsTable.module.css';
|
||||||
|
|
||||||
export interface MetricsTableProps extends ListTableProps {
|
export interface MetricsTableProps extends ListTableProps {
|
||||||
@ -22,6 +23,9 @@ export interface MetricsTableProps extends ListTableProps {
|
|||||||
limit?: number;
|
limit?: number;
|
||||||
delay?: number;
|
delay?: number;
|
||||||
onDataLoad?: (data: any) => void;
|
onDataLoad?: (data: any) => void;
|
||||||
|
onSearch?: (search: string) => void;
|
||||||
|
allowSearch?: boolean;
|
||||||
|
children?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MetricsTable({
|
export function MetricsTable({
|
||||||
@ -32,8 +36,12 @@ export function MetricsTable({
|
|||||||
limit,
|
limit,
|
||||||
onDataLoad,
|
onDataLoad,
|
||||||
delay = null,
|
delay = null,
|
||||||
|
allowSearch = false,
|
||||||
|
children,
|
||||||
...props
|
...props
|
||||||
}: MetricsTableProps) {
|
}: MetricsTableProps) {
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const { formatValue } = useFormat();
|
||||||
const [{ startDate, endDate, modified }] = useDateRange(websiteId);
|
const [{ startDate, endDate, modified }] = useDateRange(websiteId);
|
||||||
const {
|
const {
|
||||||
makeUrl,
|
makeUrl,
|
||||||
@ -42,7 +50,6 @@ export function MetricsTable({
|
|||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { get, useQuery } = useApi();
|
const { get, useQuery } = useApi();
|
||||||
const { dir } = useLocale();
|
const { dir } = useLocale();
|
||||||
|
|
||||||
const { data, isLoading, isFetched, error } = useQuery({
|
const { data, isLoading, isFetched, error } = useQuery({
|
||||||
queryKey: [
|
queryKey: [
|
||||||
'websites:metrics',
|
'websites:metrics',
|
||||||
@ -94,24 +101,43 @@ export function MetricsTable({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
items = items.filter(({ x, ...data }) => {
|
||||||
|
const value = formatValue(x, type, data);
|
||||||
|
|
||||||
|
return value.toLowerCase().includes(search.toLowerCase());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
items = percentFilter(items);
|
items = percentFilter(items);
|
||||||
|
|
||||||
if (limit) {
|
if (limit) {
|
||||||
items = items.filter((e, i) => i < limit);
|
items = items.slice(0, limit - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}, [data, error, dataFilter, limit]);
|
}, [data, dataFilter, search, limit, formatValue, type]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames(styles.container, className)}>
|
<div className={classNames(styles.container, className)}>
|
||||||
{!data && isLoading && !isFetched && <Loading icon="dots" />}
|
|
||||||
{error && <ErrorMessage />}
|
{error && <ErrorMessage />}
|
||||||
|
<div className={styles.actions}>
|
||||||
|
{allowSearch && (
|
||||||
|
<SearchField
|
||||||
|
className={styles.search}
|
||||||
|
value={search}
|
||||||
|
onSearch={setSearch}
|
||||||
|
autoFocus={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
{data && !error && (
|
{data && !error && (
|
||||||
<ListTable {...(props as ListTableProps)} data={filteredData} className={className} />
|
<ListTable {...(props as ListTableProps)} data={filteredData} className={className} />
|
||||||
)}
|
)}
|
||||||
|
{!data && isLoading && !isFetched && <Loading icon="dots" />}
|
||||||
<div className={styles.footer}>
|
<div className={styles.footer}>
|
||||||
{data && !error && limit && (
|
{data && !error && limit && (
|
||||||
<LinkButton href={makeUrl({ view: type })} variant="quiet">
|
<LinkButton href={makeUrl({ view: type })} variant="quiet">
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import MetricsTable from './MetricsTable';
|
import MetricsTable, { MetricsTableProps } from './MetricsTable';
|
||||||
import FilterLink from 'components/common/FilterLink';
|
import FilterLink from 'components/common/FilterLink';
|
||||||
import useMessages from 'components/hooks/useMessages';
|
import useMessages from 'components/hooks/useMessages';
|
||||||
|
|
||||||
@ -8,7 +8,7 @@ const names = {
|
|||||||
'Sun OS': 'SunOS',
|
'Sun OS': 'SunOS',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function OSTable({ websiteId, limit }: { websiteId: string; limit?: number }) {
|
export function OSTable(props: MetricsTableProps) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
||||||
function renderLink({ x: os }) {
|
function renderLink({ x: os }) {
|
||||||
@ -28,12 +28,11 @@ export function OSTable({ websiteId, limit }: { websiteId: string; limit?: numbe
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<MetricsTable
|
<MetricsTable
|
||||||
websiteId={websiteId}
|
{...props}
|
||||||
limit={limit}
|
type="os"
|
||||||
title={formatMessage(labels.os)}
|
title={formatMessage(labels.os)}
|
||||||
metric={formatMessage(labels.visitors)}
|
metric={formatMessage(labels.visitors)}
|
||||||
renderLabel={renderLink}
|
renderLabel={renderLink}
|
||||||
type="os"
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -6,10 +6,10 @@ import useNavigation from 'components/hooks/useNavigation';
|
|||||||
import { emptyFilter } from 'lib/filters';
|
import { emptyFilter } from 'lib/filters';
|
||||||
|
|
||||||
export interface PagesTableProps extends MetricsTableProps {
|
export interface PagesTableProps extends MetricsTableProps {
|
||||||
showFilters?: boolean;
|
allowFilter?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PagesTable({ showFilters, ...props }: PagesTableProps) {
|
export function PagesTable({ allowFilter, ...props }: PagesTableProps) {
|
||||||
const {
|
const {
|
||||||
router,
|
router,
|
||||||
makeUrl,
|
makeUrl,
|
||||||
@ -37,17 +37,16 @@ export function PagesTable({ showFilters, ...props }: PagesTableProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<MetricsTable
|
||||||
{showFilters && <FilterButtons items={buttons} selectedKey={view} onSelect={handleSelect} />}
|
{...props}
|
||||||
<MetricsTable
|
title={formatMessage(labels.pages)}
|
||||||
{...props}
|
type={view}
|
||||||
title={formatMessage(labels.pages)}
|
metric={formatMessage(labels.views)}
|
||||||
type={view}
|
dataFilter={emptyFilter}
|
||||||
metric={formatMessage(labels.views)}
|
renderLabel={renderLink}
|
||||||
dataFilter={emptyFilter}
|
>
|
||||||
renderLabel={renderLink}
|
{allowFilter && <FilterButtons items={buttons} selectedKey={view} onSelect={handleSelect} />}
|
||||||
/>
|
</MetricsTable>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,9 +13,9 @@ const filters = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function QueryParametersTable({
|
export function QueryParametersTable({
|
||||||
showFilters,
|
allowFilter,
|
||||||
...props
|
...props
|
||||||
}: { showFilters: boolean } & MetricsTableProps) {
|
}: { allowFilter: boolean } & MetricsTableProps) {
|
||||||
const [filter, setFilter] = useState(FILTER_COMBINED);
|
const [filter, setFilter] = useState(FILTER_COMBINED);
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
||||||
@ -28,27 +28,26 @@ export function QueryParametersTable({
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<MetricsTable
|
||||||
{showFilters && <FilterButtons items={buttons} selectedKey={filter} onSelect={setFilter} />}
|
{...props}
|
||||||
<MetricsTable
|
title={formatMessage(labels.query)}
|
||||||
{...props}
|
type="query"
|
||||||
title={formatMessage(labels.query)}
|
metric={formatMessage(labels.views)}
|
||||||
type="query"
|
dataFilter={filters[filter]}
|
||||||
metric={formatMessage(labels.views)}
|
renderLabel={({ x, p, v }) =>
|
||||||
dataFilter={filters[filter]}
|
filter === FILTER_RAW ? (
|
||||||
renderLabel={({ x, p, v }) =>
|
x
|
||||||
filter === FILTER_RAW ? (
|
) : (
|
||||||
x
|
<div className={styles.item}>
|
||||||
) : (
|
<div className={styles.param}>{safeDecodeURI(p)}</div>
|
||||||
<div className={styles.item}>
|
<div className={styles.value}>{safeDecodeURI(v)}</div>
|
||||||
<div className={styles.param}>{safeDecodeURI(p)}</div>
|
</div>
|
||||||
<div className={styles.value}>{safeDecodeURI(v)}</div>
|
)
|
||||||
</div>
|
}
|
||||||
)
|
delay={0}
|
||||||
}
|
>
|
||||||
delay={0}
|
{allowFilter && <FilterButtons items={buttons} selectedKey={filter} onSelect={setFilter} />}
|
||||||
/>
|
</MetricsTable>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user