choose correct Metricsbar based on component type, fix metric types list based on component type, fix chart previews reanimating. fix hostname broken query on channel query

This commit is contained in:
Francis Cao
2026-03-12 16:18:52 -07:00
parent 7e54e7de31
commit d2d14f6de0
9 changed files with 97 additions and 42 deletions
@@ -6,9 +6,11 @@ import { getComponentDefinition } from '../boardComponentRegistry';
function BoardComponentRendererComponent({
config,
websiteId,
entityType,
}: {
config: BoardComponentConfig;
websiteId?: string;
entityType?: string;
}) {
const definition = getComponentDefinition(config.type);
@@ -20,7 +22,8 @@ function BoardComponentRendererComponent({
);
}
const Component = definition.component;
const Component =
(entityType && definition.componentByEntityType?.[entityType]) || definition.component;
if (!websiteId && definition.requiresWebsite !== false) {
return (
@@ -36,7 +39,9 @@ function BoardComponentRendererComponent({
export const BoardComponentRenderer = memo(
BoardComponentRendererComponent,
(prevProps, nextProps) =>
prevProps.websiteId === nextProps.websiteId && prevProps.config === nextProps.config,
prevProps.websiteId === nextProps.websiteId &&
prevProps.entityType === nextProps.entityType &&
prevProps.config === nextProps.config,
);
BoardComponentRenderer.displayName = 'BoardComponentRenderer';
@@ -177,14 +177,16 @@ export function BoardComponentSelect({
onSelect(config);
};
const previewConfig: BoardComponentConfig | null = selectedDef
? {
type: selectedDef.type,
title,
description,
props: { ...selectedDef.defaultProps, ...configValues },
}
: null;
const previewConfig: BoardComponentConfig | null = useMemo(
() =>
selectedDef
? {
type: selectedDef.type,
props: { ...selectedDef.defaultProps, ...configValues },
}
: null,
[selectedDef, configValues],
);
const canSave = !!selectedDef && isSelectedDefSupported && (!needsWebsite || !!resolvedEntityId);
const availableDefinitions = useMemo(
@@ -274,12 +276,16 @@ export function BoardComponentSelect({
<Select
value={String(configValues[field.name] ?? field.defaultValue ?? '')}
onChange={(value: string) => handleConfigChange(field.name, value)}
maxHeight={300}
popoverProps={{ style: { width: 220 } }}
>
{field.options?.map(option => (
<ListItem key={option.value} id={option.value}>
{option.label}
</ListItem>
))}
{(field.optionsByEntityType?.[activeEntityType] ?? field.options)?.map(
option => (
<ListItem key={option.value} id={option.value}>
{option.label}
</ListItem>
),
)}
</Select>
)}
@@ -364,7 +370,7 @@ export function BoardComponentSelect({
<Text weight="bold">Preview</Text>
<Column border="left" paddingLeft="4" height="100%" style={{ minWidth: 0 }}>
{hasSelectedEntity && previewConfig && (!needsWebsite || resolvedEntityId) ? (
<BoardComponentRenderer config={previewConfig} websiteId={resolvedEntityId} />
<BoardComponentRenderer config={previewConfig} websiteId={resolvedEntityId} entityType={resolvedEntityType} />
) : (
<Column alignItems="center" justifyContent="center" height="100%">
<Text color="muted">
@@ -42,14 +42,14 @@ export function BoardEditColumn({
const boardType = getBoardType(board);
const { entityType: boardEntityType, entityId: boardEntityId } = getBoardEntity(board);
const definition = component ? getComponentDefinition(component.type) : undefined;
const { entityId } = getResolvedComponentEntity(board, component);
const { entityType, entityId } = getResolvedComponentEntity(board, component);
const renderedComponent = useMemo(() => {
if (!component || (!entityId && definition?.requiresWebsite !== false)) {
return null;
}
return <BoardComponentRenderer config={component} websiteId={entityId} />;
}, [component, definition?.requiresWebsite, entityId]);
return <BoardComponentRenderer config={component} websiteId={entityId} entityType={entityType} />;
}, [component, definition?.requiresWebsite, entityId, entityType]);
const handleSelect = (config: BoardComponentConfig) => {
onSetComponent(id, config);
@@ -43,7 +43,7 @@ export function BoardViewColumn({
{description && <Text color="muted">{description}</Text>}
<Column width="100%" height="100%" style={{ minHeight: 0 }}>
<Column width="100%" flexGrow={1} style={{ minHeight: 0 }}>
<BoardComponentRenderer config={component} websiteId={entityId} />
<BoardComponentRenderer config={component} websiteId={entityId} entityType={entityType} />
</Column>
</Column>
</Panel>
@@ -1,5 +1,6 @@
import type { ComponentType } from 'react';
import { TextBlock } from '@/app/(main)/boards/TextBlock';
import { LinkMetricsBar } from '@/app/(main)/links/[linkId]/LinkMetricsBar';
import { PixelMetricsBar } from '@/app/(main)/pixels/[pixelId]/PixelMetricsBar';
import { WebsiteChart } from '@/app/(main)/websites/[websiteId]/WebsiteChart';
import { WebsiteMetricsBar } from '@/app/(main)/websites/[websiteId]/WebsiteMetricsBar';
import {
@@ -15,12 +16,14 @@ import { EventsChart } from '@/components/metrics/EventsChart';
import { MetricsTable } from '@/components/metrics/MetricsTable';
import { WeeklyTraffic } from '@/components/metrics/WeeklyTraffic';
import { WorldMap } from '@/components/metrics/WorldMap';
import type { ComponentType } from 'react';
export interface ConfigField {
name: string;
label: string;
type: 'select' | 'number' | 'text' | 'textarea';
options?: { label: string; value: string }[];
optionsByEntityType?: Record<string, { label: string; value: string }[]>;
defaultValue?: any;
}
@@ -31,6 +34,7 @@ export interface ComponentDefinition {
category: string;
icon: ComponentType<any>;
component: ComponentType<any>;
componentByEntityType?: Record<string, ComponentType<any>>;
defaultProps?: Record<string, any>;
configFields?: ConfigField[];
requiresWebsite?: boolean;
@@ -44,31 +48,60 @@ export const CATEGORIES = [
] as const;
const METRIC_TYPES = [
{ label: 'Pages', value: 'path' },
{ label: 'Entry pages', value: 'entry' },
{ label: 'Exit pages', value: 'exit' },
{ label: 'Referrers', value: 'referrer' },
{ label: 'Channels', value: 'channel' },
{ label: 'Browsers', value: 'browser' },
{ label: 'Path', value: 'path' },
{ label: 'Entry page', value: 'entry' },
{ label: 'Exit page', value: 'exit' },
{ label: 'Title', value: 'title' },
{ label: 'Query', value: 'query' },
{ label: 'Referrer', value: 'referrer' },
{ label: 'Channel', value: 'channel' },
{ label: 'Country', value: 'country' },
{ label: 'Region', value: 'region' },
{ label: 'City', value: 'city' },
{ label: 'Browser', value: 'browser' },
{ label: 'OS', value: 'os' },
{ label: 'Devices', value: 'device' },
{ label: 'Countries', value: 'country' },
{ label: 'Regions', value: 'region' },
{ label: 'Cities', value: 'city' },
{ label: 'Languages', value: 'language' },
{ label: 'Screens', value: 'screen' },
{ label: 'Query parameters', value: 'query' },
{ label: 'Page titles', value: 'title' },
{ label: 'Hosts', value: 'host' },
{ label: 'Events', value: 'event' },
{ label: 'Device', value: 'device' },
{ label: 'Language', value: 'language' },
{ label: 'Screen', value: 'screen' },
{ label: 'UTM Source', value: 'utmSource' },
{ label: 'UTM Medium', value: 'utmMedium' },
{ label: 'UTM Campaign', value: 'utmCampaign' },
{ label: 'UTM Content', value: 'utmContent' },
{ label: 'UTM Term', value: 'utmTerm' },
{ label: 'Event', value: 'event' },
{ label: 'Hostname', value: 'hostname' },
];
const PIXEL_LINK_METRIC_TYPES = METRIC_TYPES.filter(({ value }) =>
[
'referrer',
'country',
'region',
'city',
'browser',
'os',
'device',
'query',
'utmSource',
'utmMedium',
'utmCampaign',
'utmContent',
'utmTerm',
].includes(value),
);
const LIMIT_OPTIONS = [
{ label: '5', value: '5' },
{ label: '10', value: '10' },
{ label: '20', value: '20' },
];
const PixelMetricsBarAdapter = ({ websiteId }: { websiteId?: string }) =>
websiteId ? <PixelMetricsBar pixelId={websiteId} /> : null;
const LinkMetricsBarAdapter = ({ websiteId }: { websiteId?: string }) =>
websiteId ? <LinkMetricsBar linkId={websiteId} /> : null;
const componentDefinitions: ComponentDefinition[] = [
// Overview
{
@@ -78,6 +111,10 @@ const componentDefinitions: ComponentDefinition[] = [
category: 'overview',
icon: PanelTop,
component: WebsiteMetricsBar,
componentByEntityType: {
pixel: PixelMetricsBarAdapter,
link: LinkMetricsBarAdapter,
},
},
{
type: 'WebsiteChart',
@@ -103,6 +140,10 @@ const componentDefinitions: ComponentDefinition[] = [
label: 'Metric type',
type: 'select',
options: METRIC_TYPES,
optionsByEntityType: {
pixel: PIXEL_LINK_METRIC_TYPES,
link: PIXEL_LINK_METRIC_TYPES,
},
defaultValue: 'path',
},
{
@@ -14,7 +14,7 @@ export function LinkMetricsBar({
}) {
const { isAllTime } = useDateRange();
const { t, labels } = useMessages();
const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(linkId);
const { data, isLoading, isFetching, error } = useWebsiteStatsQuery({ websiteId: linkId });
const { pageviews, visitors, visits, comparison } = data || {};
@@ -14,7 +14,7 @@ export function PixelMetricsBar({
}) {
const { isAllTime } = useDateRange();
const { t, labels } = useMessages();
const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(pixelId);
const { data, isLoading, isFetching, error } = useWebsiteStatsQuery({ websiteId: pixelId });
const { pageviews, visitors, visits, comparison } = data || {};
+3 -1
View File
@@ -60,6 +60,7 @@ async function relationalQuery(
website_event.utm_source,
website_event.session_id,
website_event.visit_id,
website_event.hostname,
count(*) c,
min(website_event.created_at) min_time,
max(website_event.created_at) max_time
@@ -77,7 +78,8 @@ async function relationalQuery(
website_event.utm_medium,
website_event.utm_source,
website_event.session_id,
website_event.visit_id),
website_event.visit_id,
website_event.hostname),
channels as (
select case
+3 -2
View File
@@ -39,7 +39,8 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
website_event.url_query,
website_event.utm_medium,
website_event.utm_source,
website_event.session_id
website_event.session_id,
website_event.hostname
from website_event
${cohortQuery}
${excludeBounceQuery}
@@ -61,7 +62,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
when ${toPostgresLikeClause('referrer_domain', EMAIL_DOMAINS)} or utm_medium ilike '%mail%' then 'email'
when ${toPostgresLikeClause('referrer_domain', SHOPPING_DOMAINS)} or utm_medium ilike '%shop%' then concat(prefix, 'Shopping')
when ${toPostgresLikeClause('referrer_domain', VIDEO_DOMAINS)} or utm_medium ilike '%video%' then concat(prefix, 'Video')
wwhen referrer_domain != regexp_replace(hostname, '^www.', '') and referrer_domain != '' then 'referral'
when referrer_domain != regexp_replace(hostname, '^www.', '') and referrer_domain != '' then 'referral'
else '' end AS x,
count(distinct session_id) y
from prefix