Added search to metrics table.

This commit is contained in:
Mike Cao 2023-12-10 02:02:24 -08:00
parent 3a28fea8ac
commit cad719fd23
12 changed files with 111 additions and 60 deletions

View File

@ -6,7 +6,7 @@ import FilterTags from 'components/metrics/FilterTags';
import useNavigation from 'components/hooks/useNavigation';
import { useWebsite } from 'components/hooks';
import WebsiteChart from './WebsiteChart';
import WebsiteMenuView from './WebsiteMenuView';
import WebsiteExpandedView from './WebsiteExpandedView';
import WebsiteHeader from './WebsiteHeader';
import WebsiteMetricsBar from './WebsiteMetricsBar';
import WebsiteTableView from './WebsiteTableView';
@ -34,7 +34,7 @@ export default function WebsiteDetails({ websiteId }: { websiteId: string }) {
{website && (
<>
{!view && <WebsiteTableView websiteId={websiteId} />}
{view && <WebsiteMenuView websiteId={websiteId} />}
{view && <WebsiteExpandedView websiteId={websiteId} />}
</>
)}
</>

View File

@ -15,7 +15,7 @@ import SideNav from 'components/layout/SideNav';
import useNavigation from 'components/hooks/useNavigation';
import useMessages from 'components/hooks/useMessages';
import LinkButton from 'components/common/LinkButton';
import styles from './WebsiteMenuView.module.css';
import styles from './WebsiteExpandedView.module.css';
const views = {
url: PagesTable,
@ -33,7 +33,7 @@ const views = {
query: QueryParametersTable,
};
export default function WebsiteMenuView({
export default function WebsiteExpandedView({
websiteId,
websiteDomain,
}: {
@ -113,11 +113,11 @@ export default function WebsiteMenuView({
const DetailsComponent = views[view] || (() => null);
const handleChange = view => {
const handleChange = (view: any) => {
router.push(makeUrl({ view }));
};
const renderValue = value => items.find(({ key }) => key === value)?.label;
const renderValue = (value: string) => items.find(({ key }) => key === value)?.label;
return (
<div className={styles.layout}>
@ -146,9 +146,10 @@ export default function WebsiteMenuView({
websiteDomain={websiteDomain}
limit={false}
animate={false}
showFilters={true}
virtualize={true}
itemCount={25}
allowFilter={true}
allowSearch={true}
/>
</div>
</div>

View File

@ -46,7 +46,7 @@ export function useDateRange(websiteId?: string) {
};
return [dateRange, saveDateRange] as [
{ startDate: Date; endDate: Date },
{ startDate: Date; endDate: Date; modified?: number },
(value: string | DateRange) => void,
];
}

View File

@ -18,14 +18,19 @@ export function useFormat() {
};
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 => {
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) {
case 'browser':
return formatBrowser(value);
@ -33,6 +38,8 @@ export function useFormat() {
return formatCountry(value);
case 'region':
return formatRegion(value);
case 'city':
return formatCity(value, data?.country);
case 'device':
return formatDevice(value);
default:

View File

@ -9,7 +9,7 @@ export interface SideNavProps {
items: any[];
shallow?: boolean;
scroll?: boolean;
className?: boolean;
className?: string;
onSelect?: () => void;
}

View File

@ -11,8 +11,8 @@ export function CitiesTable(props: MetricsTableProps) {
const countryNames = useCountryNames(locale);
const renderLabel = (city: string, country: string) => {
const name = countryNames[country];
return name ? `${city}, ${name}` : city;
const countryName = countryNames[country];
return countryName ? `${city}, ${countryName}` : city;
};
const renderLink = ({ x: city, country }) => {

View File

@ -6,13 +6,33 @@
flex: 1;
}
.actions {
display: flex;
gap: 20px;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.footer {
display: flex;
justify-content: center;
}
.search {
max-width: 300px;
}
@media only screen and (max-width: 992px) {
.container {
min-height: auto;
}
.actions {
flex-direction: column;
}
.search {
max-width: 100%;
}
}

View File

@ -1,5 +1,5 @@
import { useMemo } from 'react';
import { Loading, Icon, Text } from 'react-basics';
import { ReactNode, useMemo, useState } from 'react';
import { Loading, Icon, Text, SearchField } from 'react-basics';
import classNames from 'classnames';
import useApi from 'components/hooks/useApi';
import { percentFilter } from 'lib/filters';
@ -12,6 +12,7 @@ import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
import Icons from 'components/icons';
import useMessages from 'components/hooks/useMessages';
import useLocale from 'components/hooks/useLocale';
import useFormat from 'components//hooks/useFormat';
import styles from './MetricsTable.module.css';
export interface MetricsTableProps extends ListTableProps {
@ -22,6 +23,9 @@ export interface MetricsTableProps extends ListTableProps {
limit?: number;
delay?: number;
onDataLoad?: (data: any) => void;
onSearch?: (search: string) => void;
allowSearch?: boolean;
children?: ReactNode;
}
export function MetricsTable({
@ -32,8 +36,12 @@ export function MetricsTable({
limit,
onDataLoad,
delay = null,
allowSearch = false,
children,
...props
}: MetricsTableProps) {
const [search, setSearch] = useState('');
const { formatValue } = useFormat();
const [{ startDate, endDate, modified }] = useDateRange(websiteId);
const {
makeUrl,
@ -42,7 +50,6 @@ export function MetricsTable({
const { formatMessage, labels } = useMessages();
const { get, useQuery } = useApi();
const { dir } = useLocale();
const { data, isLoading, isFetched, error } = useQuery({
queryKey: [
'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);
if (limit) {
items = items.filter((e, i) => i < limit);
items = items.slice(0, limit - 1);
}
return items;
}
return [];
}, [data, error, dataFilter, limit]);
}, [data, dataFilter, search, limit, formatValue, type]);
return (
<div className={classNames(styles.container, className)}>
{!data && isLoading && !isFetched && <Loading icon="dots" />}
{error && <ErrorMessage />}
<div className={styles.actions}>
{allowSearch && (
<SearchField
className={styles.search}
value={search}
onSearch={setSearch}
autoFocus={true}
/>
)}
{children}
</div>
{data && !error && (
<ListTable {...(props as ListTableProps)} data={filteredData} className={className} />
)}
{!data && isLoading && !isFetched && <Loading icon="dots" />}
<div className={styles.footer}>
{data && !error && limit && (
<LinkButton href={makeUrl({ view: type })} variant="quiet">

View File

@ -1,4 +1,4 @@
import MetricsTable from './MetricsTable';
import MetricsTable, { MetricsTableProps } from './MetricsTable';
import FilterLink from 'components/common/FilterLink';
import useMessages from 'components/hooks/useMessages';
@ -8,7 +8,7 @@ const names = {
'Sun OS': 'SunOS',
};
export function OSTable({ websiteId, limit }: { websiteId: string; limit?: number }) {
export function OSTable(props: MetricsTableProps) {
const { formatMessage, labels } = useMessages();
function renderLink({ x: os }) {
@ -28,12 +28,11 @@ export function OSTable({ websiteId, limit }: { websiteId: string; limit?: numbe
return (
<MetricsTable
websiteId={websiteId}
limit={limit}
{...props}
type="os"
title={formatMessage(labels.os)}
metric={formatMessage(labels.visitors)}
renderLabel={renderLink}
type="os"
/>
);
}

View File

@ -6,10 +6,10 @@ import useNavigation from 'components/hooks/useNavigation';
import { emptyFilter } from 'lib/filters';
export interface PagesTableProps extends MetricsTableProps {
showFilters?: boolean;
allowFilter?: boolean;
}
export function PagesTable({ showFilters, ...props }: PagesTableProps) {
export function PagesTable({ allowFilter, ...props }: PagesTableProps) {
const {
router,
makeUrl,
@ -37,17 +37,16 @@ export function PagesTable({ showFilters, ...props }: PagesTableProps) {
};
return (
<>
{showFilters && <FilterButtons items={buttons} selectedKey={view} onSelect={handleSelect} />}
<MetricsTable
{...props}
title={formatMessage(labels.pages)}
type={view}
metric={formatMessage(labels.views)}
dataFilter={emptyFilter}
renderLabel={renderLink}
/>
</>
<MetricsTable
{...props}
title={formatMessage(labels.pages)}
type={view}
metric={formatMessage(labels.views)}
dataFilter={emptyFilter}
renderLabel={renderLink}
>
{allowFilter && <FilterButtons items={buttons} selectedKey={view} onSelect={handleSelect} />}
</MetricsTable>
);
}

View File

@ -13,9 +13,9 @@ const filters = {
};
export function QueryParametersTable({
showFilters,
allowFilter,
...props
}: { showFilters: boolean } & MetricsTableProps) {
}: { allowFilter: boolean } & MetricsTableProps) {
const [filter, setFilter] = useState(FILTER_COMBINED);
const { formatMessage, labels } = useMessages();
@ -28,27 +28,26 @@ export function QueryParametersTable({
];
return (
<>
{showFilters && <FilterButtons items={buttons} selectedKey={filter} onSelect={setFilter} />}
<MetricsTable
{...props}
title={formatMessage(labels.query)}
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}>{safeDecodeURI(p)}</div>
<div className={styles.value}>{safeDecodeURI(v)}</div>
</div>
)
}
delay={0}
/>
</>
<MetricsTable
{...props}
title={formatMessage(labels.query)}
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}>{safeDecodeURI(p)}</div>
<div className={styles.value}>{safeDecodeURI(v)}</div>
</div>
)
}
delay={0}
>
{allowFilter && <FilterButtons items={buttons} selectedKey={filter} onSelect={setFilter} />}
</MetricsTable>
);
}