Redesigned filter bar.

This commit is contained in:
Mike Cao 2024-05-21 21:15:31 -07:00
parent 76cab03bb2
commit 6589bc6ecb
18 changed files with 86 additions and 38 deletions

View File

@ -9,6 +9,7 @@ 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';
import { FILTER_COLUMNS } from 'lib/constants';
export default function WebsiteDetails({ websiteId }: { websiteId: string }) { export default function WebsiteDetails({ websiteId }: { websiteId: string }) {
const { data: website, isLoading, error } = useWebsite(websiteId); const { data: website, isLoading, error } = useWebsite(websiteId);
@ -20,13 +21,20 @@ export default function WebsiteDetails({ websiteId }: { websiteId: string }) {
} }
const showLinks = !pathname.includes('/share/'); const showLinks = !pathname.includes('/share/');
const { view, ...params } = query; const { view } = query;
const params = Object.keys(query).reduce((obj, key) => {
if (FILTER_COLUMNS[key]) {
obj[key] = query[key];
}
return obj;
}, {});
return ( return (
<> <>
<WebsiteHeader websiteId={websiteId} showLinks={showLinks} /> <WebsiteHeader websiteId={websiteId} showLinks={showLinks} />
<FilterTags websiteId={websiteId} params={params} /> <FilterTags websiteId={websiteId} params={params} />
<WebsiteMetricsBar websiteId={websiteId} sticky={true} /> <WebsiteMetricsBar websiteId={websiteId} />
<WebsiteChart websiteId={websiteId} /> <WebsiteChart websiteId={websiteId} />
{!website && <Loading icon="dots" style={{ minHeight: 300 }} />} {!website && <Loading icon="dots" style={{ minHeight: 300 }} />}
{website && ( {website && (

View File

@ -9,9 +9,15 @@ import styles from './WebsiteFilterButton.module.css';
export function WebsiteFilterButton({ export function WebsiteFilterButton({
websiteId, websiteId,
className, className,
position = 'bottom',
alignment = 'end',
showText = true,
}: { }: {
websiteId: string; websiteId: string;
className?: string; className?: string;
position?: 'bottom' | 'top' | 'left' | 'right';
alignment?: 'end' | 'center' | 'start';
showText?: boolean;
}) { }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { renderUrl, router } = useNavigation(); const { renderUrl, router } = useNavigation();
@ -30,9 +36,9 @@ export function WebsiteFilterButton({
<Icon> <Icon>
<Icons.Plus /> <Icons.Plus />
</Icon> </Icon>
<Text>{formatMessage(labels.filter)}</Text> {showText && <Text>{formatMessage(labels.filter)}</Text>}
</Button> </Button>
<Popup position="bottom" alignment="end"> <Popup position={position} alignment={alignment}>
{(close: () => void) => { {(close: () => void) => {
return ( return (
<PopupForm> <PopupForm>

View File

@ -30,6 +30,11 @@ export function WebsiteHeader({
icon: <Icons.Overview />, icon: <Icons.Overview />,
path: '', path: '',
}, },
{
label: formatMessage(labels.compare),
icon: <Icons.Compare />,
path: '/compare',
},
{ {
label: formatMessage(labels.realtime), label: formatMessage(labels.realtime),
icon: <Icons.Clock />, icon: <Icons.Clock />,

View File

@ -38,9 +38,3 @@
border-bottom: 1px solid var(--base300); border-bottom: 1px solid var(--base300);
} }
} }
@media screen and (max-width: 768px) {
.button {
display: none;
}
}

View File

@ -5,18 +5,10 @@ import MetricCard from 'components/metrics/MetricCard';
import MetricsBar from 'components/metrics/MetricsBar'; import MetricsBar from 'components/metrics/MetricsBar';
import { formatShortTime } from 'lib/format'; import { formatShortTime } from 'lib/format';
import WebsiteFilterButton from './WebsiteFilterButton'; import WebsiteFilterButton from './WebsiteFilterButton';
import styles from './WebsiteMetricsBar.module.css';
import useWebsiteStats from 'components/hooks/queries/useWebsiteStats'; import useWebsiteStats from 'components/hooks/queries/useWebsiteStats';
import styles from './WebsiteMetricsBar.module.css';
export function WebsiteMetricsBar({ export function WebsiteMetricsBar({ websiteId, sticky }: { websiteId: string; sticky?: boolean }) {
websiteId,
showFilter = true,
sticky,
}: {
websiteId: string;
showFilter?: boolean;
sticky?: boolean;
}) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { ref, isSticky } = useSticky({ enabled: sticky }); const { ref, isSticky } = useSticky({ enabled: sticky });
const { data, isLoading, isFetched, error } = useWebsiteStats(websiteId); const { data, isLoading, isFetched, error } = useWebsiteStats(websiteId);
@ -89,7 +81,7 @@ export function WebsiteMetricsBar({
)} )}
</MetricsBar> </MetricsBar>
<div className={styles.actions}> <div className={styles.actions}>
{showFilter && <WebsiteFilterButton websiteId={websiteId} className={styles.button} />} <WebsiteFilterButton websiteId={websiteId} />
<WebsiteDateFilter websiteId={websiteId} /> <WebsiteDateFilter websiteId={websiteId} />
</div> </div>
</div> </div>

View File

@ -0,0 +1,13 @@
import WebsiteHeader from '../WebsiteHeader';
import WebsiteMetricsBar from '../WebsiteMetricsBar';
export function WebsiteComparePage({ websiteId }) {
return (
<>
<WebsiteHeader websiteId={websiteId} />
<WebsiteMetricsBar websiteId={websiteId} />
</>
);
}
export default WebsiteComparePage;

View File

@ -0,0 +1,10 @@
import WebsiteComparePage from './WebsiteComparePage';
import { Metadata } from 'next';
export default function ({ params: { websiteId } }) {
return <WebsiteComparePage websiteId={websiteId} />;
}
export const metadata: Metadata = {
title: 'Website Comparison',
};

1
src/assets/compare.svg Normal file
View File

@ -0,0 +1 @@
<svg height="512" viewBox="0 0 24 24" width="512" xmlns="http://www.w3.org/2000/svg"><path d="M6 22a1 1 0 0 1-.71-.29l-4-4a1 1 0 0 1 0-1.42l4-4a1 1 0 0 1 1.42 1.42L4.41 16H22a1 1 0 0 1 0 2H4.41l2.3 2.29a1 1 0 0 1 0 1.42A1 1 0 0 1 6 22zm12-10a1 1 0 0 1-.71-.29 1 1 0 0 1 0-1.42L19.59 8H2a1 1 0 0 1 0-2h17.59l-2.3-2.29a1 1 0 0 1 1.42-1.42l4 4a1 1 0 0 1 0 1.42l-4 4A1 1 0 0 1 18 12z"/></svg>

After

Width:  |  Height:  |  Size: 388 B

View File

@ -1 +1 @@
<svg clip-rule="evenodd" fill-rule="evenodd" height="512" stroke-linejoin="round" stroke-miterlimit="2" viewBox="0 0 24 24" width="512" xmlns="http://www.w3.org/2000/svg"><g id="Icon"><path d="m19.393 10.825c-.097-.403.151-.808.553-.905.402-.098.808.15.905.553.181.75.277 1.533.277 2.338 0 5.485-4.453 9.939-9.939 9.939-5.485 0-9.939-4.454-9.939-9.939 0-5.486 4.454-9.939 9.939-9.939.805 0 1.588.096 2.338.277.403.097.651.503.553.905-.097.402-.502.65-.905.553-.637-.154-1.302-.235-1.986-.235-4.658 0-8.439 3.781-8.439 8.439s3.781 8.439 8.439 8.439 8.439-3.781 8.439-8.439c0-.684-.081-1.349-.235-1.986z"/><path d="m14.764 12.811c0-.414.336-.75.75-.75.413 0 .75.336.75.75 0 2.8-2.274 5.074-5.075 5.074-2.8 0-5.074-2.274-5.074-5.074 0-2.801 2.274-5.075 5.074-5.075.414 0 .75.337.75.75 0 .414-.336.75-.75.75-1.973 0-3.574 1.602-3.574 3.575s1.601 3.574 3.574 3.574 3.575-1.601 3.575-3.574z"/><path d="m22.53 5.588-3.057 3.058c-.141.141-.332.22-.531.22h-3.058c-.414 0-.75-.336-.75-.75v-3.058c0-.199.079-.39.22-.531l3.058-3.057c.184-.184.45-.26.703-.2s.457.246.539.493l.646 1.937 1.937.646c.247.082.433.286.493.539s-.016.519-.2.703zm-1.918-.202-1.142-.381c-.224-.075-.4-.251-.475-.475l-.381-1.142-1.98 1.98v1.998h1.998z"/><path d="m15.354 7.585c.293-.293.768-.293 1.061 0s.293.768 0 1.061l-4.587 4.586c-.293.293-.768.293-1.06 0-.293-.292-.293-.767 0-1.06z"/></g></svg> <svg clip-rule="evenodd" fill-rule="evenodd" height="512" stroke-linejoin="round" stroke-miterlimit="2" viewBox="0 0 24 24" width="512" xmlns="http://www.w3.org/2000/svg"><path d="M19.393 10.825a.75.75 0 0 1 1.458-.352c.181.75.277 1.533.277 2.338 0 5.485-4.453 9.939-9.939 9.939-5.485 0-9.939-4.454-9.939-9.939 0-5.486 4.454-9.939 9.939-9.939.805 0 1.588.096 2.338.277a.75.75 0 1 1-.352 1.458A8.442 8.442 0 0 0 2.75 12.811a8.442 8.442 0 0 0 8.439 8.439 8.442 8.442 0 0 0 8.204-10.425z"/><path d="M14.764 12.811a.75.75 0 0 1 1.5 0c0 2.8-2.274 5.074-5.075 5.074a5.077 5.077 0 0 1-5.074-5.074 5.077 5.077 0 0 1 5.074-5.075.75.75 0 0 1 0 1.5 3.575 3.575 0 1 0 3.575 3.575zm7.766-7.223-3.057 3.058a.75.75 0 0 1-.531.22h-3.058a.75.75 0 0 1-.75-.75V5.058a.75.75 0 0 1 .22-.531l3.058-3.057a.75.75 0 0 1 1.242.293L20.3 3.7l1.937.646a.75.75 0 0 1 .293 1.242zm-1.918-.202-1.142-.381a.753.753 0 0 1-.475-.475l-.381-1.142-1.98 1.98v1.998h1.998z"/><path d="M15.354 7.585a.75.75 0 1 1 1.061 1.061l-4.587 4.586a.749.749 0 1 1-1.06-1.06z"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -6,6 +6,7 @@ import Bolt from 'assets/bolt.svg';
import Calendar from 'assets/calendar.svg'; import Calendar from 'assets/calendar.svg';
import Change from 'assets/change.svg'; import Change from 'assets/change.svg';
import Clock from 'assets/clock.svg'; import Clock from 'assets/clock.svg';
import Compare from 'assets/compare.svg';
import Dashboard from 'assets/dashboard.svg'; import Dashboard from 'assets/dashboard.svg';
import Eye from 'assets/eye.svg'; import Eye from 'assets/eye.svg';
import Gear from 'assets/gear.svg'; import Gear from 'assets/gear.svg';
@ -32,6 +33,7 @@ const icons = {
Calendar, Calendar,
Change, Change,
Clock, Clock,
Compare,
Dashboard, Dashboard,
Eye, Eye,
Gear, Gear,

View File

@ -247,6 +247,7 @@ export const labels = defineMessages({
id: 'label.journey-description', id: 'label.journey-description',
defaultMessage: 'Understand how users nagivate through your website.', defaultMessage: 'Understand how users nagivate through your website.',
}, },
compare: { id: 'label.compare', defaultMessage: 'Compare' },
}); });
export const messages = defineMessages({ export const messages = defineMessages({

View File

@ -2,6 +2,12 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
background: var(--base75);
padding: 10px 20px;
border: 1px solid var(--base400);
border-radius: 8px;
margin-bottom: 20px;
flex-wrap: wrap;
} }
.label { .label {
@ -12,12 +18,13 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
gap: 10px; gap: 4px;
background: var(--base75); font-size: 12px;
background: var(--base50);
border: 1px solid var(--base400); border: 1px solid var(--base400);
border-radius: var(--border-radius); border-radius: var(--border-radius);
box-shadow: 1px 1px 1px var(--base500); box-shadow: 1px 1px 1px var(--base500);
padding: 8px 16px; padding: 6px 14px;
cursor: pointer; cursor: pointer;
} }
@ -27,6 +34,8 @@
.close { .close {
font-weight: 700; font-weight: 700;
align-self: center;
margin-left: auto;
} }
.name, .name,

View File

@ -13,6 +13,7 @@ import FieldFilterEditForm from 'app/(main)/reports/[reportId]/FieldFilterEditFo
import { OPERATOR_PREFIXES } from 'lib/constants'; import { OPERATOR_PREFIXES } from 'lib/constants';
import { isSearchOperator, parseParameterValue } from 'lib/params'; import { isSearchOperator, parseParameterValue } from 'lib/params';
import styles from './FilterTags.module.css'; import styles from './FilterTags.module.css';
import WebsiteFilterButton from 'app/(main)/websites/[websiteId]/WebsiteFilterButton';
export function FilterTags({ export function FilterTags({
websiteId, websiteId,
@ -100,6 +101,7 @@ export function FilterTags({
</PopupTrigger> </PopupTrigger>
); );
})} })}
<WebsiteFilterButton websiteId={websiteId} alignment="center" showText={false} />
<Button className={styles.close} variant="quiet" onClick={handleResetFilter}> <Button className={styles.close} variant="quiet" onClick={handleResetFilter}>
<Icon> <Icon>
<Icons.Close /> <Icons.Close />

View File

@ -2,10 +2,17 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
min-height: 90px;
min-width: 140px; min-width: 140px;
} }
.card:first-child {
padding-left: 0;
}
.card:last-child {
border: 0;
}
.value { .value {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -27,9 +27,6 @@ export const MetricCard = ({
return ( return (
<div className={classNames(styles.card, className)}> <div className={classNames(styles.card, className)}>
<animated.div className={styles.value} title={props?.x as any}>
{props?.x?.to(x => format(x))}
</animated.div>
<div className={styles.label}> <div className={styles.label}>
{label} {label}
{~~change !== 0 && !hideComparison && ( {~~change !== 0 && !hideComparison && (
@ -45,6 +42,9 @@ export const MetricCard = ({
</animated.span> </animated.span>
)} )}
</div> </div>
<animated.div className={styles.value} title={props?.x as any}>
{props?.x?.to(x => format(x))}
</animated.div>
</div> </div>
); );
}; };

View File

@ -1,4 +1,5 @@
declare module 'cors'; declare module 'cors';
declare module 'dateformat';
declare module 'debug'; declare module 'debug';
declare module 'chartjs-adapter-date-fns'; declare module 'chartjs-adapter-date-fns';
declare module 'md5'; declare module 'md5';

View File

@ -119,10 +119,7 @@ async function parseFilters(websiteId: string, filters: QueryFilters = {}, optio
}; };
} }
async function rawQuery<T = unknown>( async function rawQuery(query: string, params: Record<string, unknown> = {}): Promise<unknown[]> {
query: string,
params: Record<string, unknown> = {},
): Promise<T> {
if (process.env.LOG_QUERY) { if (process.env.LOG_QUERY) {
log('QUERY:\n', query); log('QUERY:\n', query);
log('PARAMETERS:\n', params); log('PARAMETERS:\n', params);

View File

@ -33,8 +33,8 @@ async function relationalQuery(
let excludeDomain = ''; let excludeDomain = '';
if (column === 'referrer_domain') { if (column === 'referrer_domain') {
excludeDomain = excludeDomain = `and website_event.referrer_domain != {{websiteDomain}}
'and (website_event.referrer_domain != {{websiteDomain}} or website_event.referrer_domain is null)'; and website_event.referrer_domain is not null`;
} }
return rawQuery( return rawQuery(
@ -72,7 +72,7 @@ async function clickhouseQuery(
let excludeDomain = ''; let excludeDomain = '';
if (column === 'referrer_domain') { if (column === 'referrer_domain') {
excludeDomain = 'and referrer_domain != {websiteDomain:String}'; excludeDomain = `and referrer_domain != {websiteDomain:String} and referrer_domain != ''`;
} }
return rawQuery( return rawQuery(
@ -90,8 +90,8 @@ async function clickhouseQuery(
offset ${offset} offset ${offset}
`, `,
params, params,
).then(a => { ).then((result: any) => {
return Object.values(a).map(a => { return Object.values(result).map((a: any) => {
return { x: a.x, y: Number(a.y) }; return { x: a.x, y: Number(a.y) };
}); });
}); });