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 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} />}
</> </>
)} )}
</> </>

View File

@ -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>

View File

@ -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,
]; ];
} }

View File

@ -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:

View File

@ -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;
} }

View File

@ -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 }) => {

View File

@ -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%;
}
} }

View File

@ -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">

View File

@ -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"
/> />
); );
} }

View File

@ -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>
</>
); );
} }

View File

@ -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>
</>
); );
} }