diff --git a/frontend/src/scenes/web-analytics/WebAnalyticsDataTable.tsx b/frontend/src/scenes/web-analytics/WebAnalyticsDataTable.tsx new file mode 100644 index 00000000000000..e306f78d8cab66 --- /dev/null +++ b/frontend/src/scenes/web-analytics/WebAnalyticsDataTable.tsx @@ -0,0 +1,152 @@ +import { QueryContext, QueryContextColumnComponent, QueryContextColumnTitleComponent } from '~/queries/types' +import { DataTableNode, NodeKind, WebStatsBreakdown } from '~/queries/schema' +import { UnexpectedNeverError } from 'lib/utils' +import { useActions } from 'kea' +import { webAnalyticsLogic } from 'scenes/web-analytics/webAnalyticsLogic' +import { useCallback } from 'react' +import { Query } from '~/queries/Query/Query' + +const PercentageCell: QueryContextColumnComponent = ({ value }) => { + if (typeof value === 'number') { + return {`${(value * 100).toFixed(1)}%`} + } else { + return null + } +} + +const NumericCell: QueryContextColumnComponent = ({ value }) => { + return {typeof value === 'number' ? value.toLocaleString() : String(value)} +} + +const BreakdownValueTitle: QueryContextColumnTitleComponent = (props) => { + const { query } = props + const { source } = query + if (source.kind !== NodeKind.WebStatsTableQuery) { + return null + } + const { breakdownBy } = source + switch (breakdownBy) { + case WebStatsBreakdown.Page: + return <>Path + case WebStatsBreakdown.InitialPage: + return <>Initial Path + case WebStatsBreakdown.InitialReferringDomain: + return <>Referring Domain + case WebStatsBreakdown.InitialUTMSource: + return <>UTM Source + case WebStatsBreakdown.InitialUTMCampaign: + return <>UTM Campaign + case WebStatsBreakdown.Browser: + return <>Browser + case WebStatsBreakdown.OS: + return <>OS + case WebStatsBreakdown.DeviceType: + return <>Device Type + default: + throw new UnexpectedNeverError(breakdownBy) + } +} + +const BreakdownValueCell: QueryContextColumnComponent = (props) => { + const { value, query } = props + const { source } = query + if (source.kind !== NodeKind.WebStatsTableQuery) { + return null + } + if (typeof value !== 'string') { + return null + } + + return +} + +export const webStatsBreakdownToPropertyName = (breakdownBy: WebStatsBreakdown): string => { + switch (breakdownBy) { + case WebStatsBreakdown.Page: + return '$pathname' + case WebStatsBreakdown.InitialPage: + return '$initial_pathname' + case WebStatsBreakdown.InitialReferringDomain: + return '$initial_referrer' + case WebStatsBreakdown.InitialUTMSource: + return '$initial_utm_source' + case WebStatsBreakdown.InitialUTMCampaign: + return '$initial_utm_campaign' + case WebStatsBreakdown.Browser: + return '$browser' + case WebStatsBreakdown.OS: + return '$os' + case WebStatsBreakdown.DeviceType: + return '$device_type' + default: + throw new UnexpectedNeverError(breakdownBy) + } +} + +const BreakdownValueCellInner = ({ value }: { value: string }): JSX.Element => { + return {value} +} + +export const webAnalyticsDataTableQueryContext: QueryContext = { + columns: { + breakdown_value: { + renderTitle: BreakdownValueTitle, + render: BreakdownValueCell, + }, + bounce_rate: { + title: 'Bounce Rate', + render: PercentageCell, + align: 'right', + }, + views: { + title: 'Views', + render: NumericCell, + align: 'right', + }, + visitors: { + title: 'Visitors', + render: NumericCell, + align: 'right', + }, + }, +} + +export const WebStatsTableTile = ({ + query, + breakdownBy, +}: { + query: DataTableNode + breakdownBy: WebStatsBreakdown +}): JSX.Element => { + const { togglePropertyFilter } = useActions(webAnalyticsLogic) + const propertyName = webStatsBreakdownToPropertyName(breakdownBy) + + const onClick = useCallback( + (record: unknown) => { + if (typeof record !== 'object' || !record || !('result' in record)) { + return + } + const result = record.result + if (!Array.isArray(result)) { + return + } + // assume that the first element is the value + togglePropertyFilter(propertyName, result[0]) + }, + [togglePropertyFilter, propertyName] + ) + + return ( + ({ + onClick: () => onClick(record), + className: 'hover:underline cursor-pointer hover:bg-mark', + }), + }} + /> + ) +} diff --git a/frontend/src/scenes/web-analytics/WebAnalyticsNotice.tsx b/frontend/src/scenes/web-analytics/WebAnalyticsNotice.tsx new file mode 100644 index 00000000000000..617ec0c62f5f12 --- /dev/null +++ b/frontend/src/scenes/web-analytics/WebAnalyticsNotice.tsx @@ -0,0 +1,34 @@ +import { useActions, useValues } from 'kea' +import { supportLogic } from 'lib/components/Support/supportLogic' +import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' +import { LemonBanner } from 'lib/lemon-ui/LemonBanner' +import { Link } from 'lib/lemon-ui/Link' +import { IconBugReport, IconFeedback, IconGithub } from 'lib/lemon-ui/icons' + +export const WebAnalyticsNotice = (): JSX.Element => { + const { openSupportForm } = useActions(supportLogic) + const { preflight } = useValues(preflightLogic) + + const showSupportOptions = preflight?.cloud + + return ( + +

PostHog Web Analytics is in closed Alpha. Thanks for taking part! We'd love to hear what you think.

+ {showSupportOptions ? ( +

+ openSupportForm('bug')}> + Report a bug + {' '} + -{' '} + openSupportForm('feedback')}> + Give feedback + {' '} + -{' '} + + View GitHub issue + +

+ ) : null} +
+ ) +} diff --git a/frontend/src/scenes/web-analytics/WebDashboard.tsx b/frontend/src/scenes/web-analytics/WebDashboard.tsx index 36b456ce6ec1a8..14746c11d7336b 100644 --- a/frontend/src/scenes/web-analytics/WebDashboard.tsx +++ b/frontend/src/scenes/web-analytics/WebDashboard.tsx @@ -1,124 +1,14 @@ import { Query } from '~/queries/Query/Query' import { useActions, useValues } from 'kea' -import { webAnalyticsLogic } from 'scenes/web-analytics/webAnalyticsLogic' +import { TabsTile, webAnalyticsLogic } from 'scenes/web-analytics/webAnalyticsLogic' import { PropertyFilters } from 'lib/components/PropertyFilters/PropertyFilters' import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' import { isEventPropertyFilter } from 'lib/components/PropertyFilters/utils' -import { DataTableNode, NodeKind, QuerySchema, WebStatsBreakdown } from '~/queries/schema' -import { QueryContext, QueryContextColumnComponent, QueryContextColumnTitleComponent } from '~/queries/types' -import { UnexpectedNeverError } from 'lib/utils' +import { NodeKind, QuerySchema } from '~/queries/schema' import { DateFilter } from 'lib/components/DateFilter/DateFilter' -import { LemonBanner } from 'lib/lemon-ui/LemonBanner' -import { supportLogic } from 'lib/components/Support/supportLogic' -import { IconBugReport, IconFeedback, IconGithub } from 'lib/lemon-ui/icons' -import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { Link } from 'lib/lemon-ui/Link' -import { useCallback } from 'react' - -const PercentageCell: QueryContextColumnComponent = ({ value }) => { - if (typeof value === 'number') { - return {`${(value * 100).toFixed(1)}%`} - } else { - return null - } -} - -const NumericCell: QueryContextColumnComponent = ({ value }) => { - return {typeof value === 'number' ? value.toLocaleString() : String(value)} -} - -const BreakdownValueTitle: QueryContextColumnTitleComponent = (props) => { - const { query } = props - const { source } = query - if (source.kind !== NodeKind.WebStatsTableQuery) { - return null - } - const { breakdownBy } = source - switch (breakdownBy) { - case WebStatsBreakdown.Page: - return <>Path - case WebStatsBreakdown.InitialPage: - return <>Initial Path - case WebStatsBreakdown.InitialReferringDomain: - return <>Referring Domain - case WebStatsBreakdown.InitialUTMSource: - return <>UTM Source - case WebStatsBreakdown.InitialUTMCampaign: - return <>UTM Campaign - case WebStatsBreakdown.Browser: - return <>Browser - case WebStatsBreakdown.OS: - return <>OS - case WebStatsBreakdown.DeviceType: - return <>Device Type - default: - throw new UnexpectedNeverError(breakdownBy) - } -} - -const BreakdownValueCell: QueryContextColumnComponent = (props) => { - const { value, query } = props - const { source } = query - if (source.kind !== NodeKind.WebStatsTableQuery) { - return null - } - if (typeof value !== 'string') { - return null - } - - return -} - -const webStatsBreakdownToPropertyName = (breakdownBy: WebStatsBreakdown): string => { - switch (breakdownBy) { - case WebStatsBreakdown.Page: - return '$pathname' - case WebStatsBreakdown.InitialPage: - return '$initial_pathname' - case WebStatsBreakdown.InitialReferringDomain: - return '$initial_referrer' - case WebStatsBreakdown.InitialUTMSource: - return '$initial_utm_source' - case WebStatsBreakdown.InitialUTMCampaign: - return '$initial_utm_campaign' - case WebStatsBreakdown.Browser: - return '$browser' - case WebStatsBreakdown.OS: - return '$os' - case WebStatsBreakdown.DeviceType: - return '$device_type' - default: - throw new UnexpectedNeverError(breakdownBy) - } -} - -const BreakdownValueCellInner = ({ value }: { value: string }): JSX.Element => { - return {value} -} - -const queryContext: QueryContext = { - columns: { - breakdown_value: { - renderTitle: BreakdownValueTitle, - render: BreakdownValueCell, - }, - bounce_rate: { - title: 'Bounce Rate', - render: PercentageCell, - align: 'right', - }, - views: { - title: 'Views', - render: NumericCell, - align: 'right', - }, - visitors: { - title: 'Visitors', - render: NumericCell, - align: 'right', - }, - }, -} +import { WebAnalyticsNotice } from 'scenes/web-analytics/WebAnalyticsNotice' +import { webAnalyticsDataTableQueryContext, WebStatsTableTile } from 'scenes/web-analytics/WebAnalyticsDataTable' +import { WebTabs } from 'scenes/web-analytics/WebTabs' const Filters = (): JSX.Element => { const { webAnalyticsFilters, dateTo, dateFrom } = useValues(webAnalyticsLogic) @@ -155,46 +45,11 @@ const Tiles = (): JSX.Element => { } flex flex-col`} > {title &&

{title}

} - + ) } else if ('tabs' in tile) { - const { tabs, activeTabId, layout, setTabId } = tile - const tab = tabs.find((t) => t.id === activeTabId) - if (!tab) { - return null - } - const { query, title } = tab - return ( -
-
- {

{title}

} - {tabs.length > 1 && ( -
- {/* TODO switch to a select if more than 3 */} - {tabs.map(({ id, linkText }) => ( - setTabId(id)} - > - {linkText} - - ))} -
- )} -
- {/* Setting key forces the component to be recreated when the tab changes */} - -
- ) + return } else { return null } @@ -203,86 +58,36 @@ const Tiles = (): JSX.Element => { ) } -const QueryTile = ({ query }: { query: QuerySchema }): JSX.Element => { - if (query.kind === NodeKind.DataTableNode && query.source.kind === NodeKind.WebStatsTableQuery) { - return - } - - return -} - -const WebStatsTableTile = ({ - query, - breakdownBy, -}: { - query: DataTableNode - breakdownBy: WebStatsBreakdown -}): JSX.Element => { - const { togglePropertyFilter } = useActions(webAnalyticsLogic) - const propertyName = webStatsBreakdownToPropertyName(breakdownBy) - - const onClick = useCallback( - (record: unknown) => { - if (typeof record !== 'object' || !record || !('result' in record)) { - return - } - const result = record.result - if (!Array.isArray(result)) { - return - } - // assume that the first element is the value - togglePropertyFilter(propertyName, result[0]) - }, - [togglePropertyFilter, propertyName] - ) +const TabsTileItem = ({ tile }: { tile: TabsTile }): JSX.Element => { + const { layout } = tile return ( - ({ - onClick: () => onClick(record), - className: 'hover:underline cursor-pointer hover:bg-mark', - }), - }} + ({ + id: tab.id, + content: , + linkText: tab.linkText, + title: tab.title, + }))} /> ) } -export const Notice = (): JSX.Element => { - const { openSupportForm } = useActions(supportLogic) - const { preflight } = useValues(preflightLogic) - - const showSupportOptions = preflight?.cloud +const WebQuery = ({ query }: { query: QuerySchema }): JSX.Element => { + if (query.kind === NodeKind.DataTableNode && query.source.kind === NodeKind.WebStatsTableQuery) { + return + } - return ( - -

PostHog Web Analytics is in closed Alpha. Thanks for taking part! We'd love to hear what you think.

- {showSupportOptions ? ( -

- openSupportForm('bug')}> - Report a bug - {' '} - -{' '} - openSupportForm('feedback')}> - Give feedback - {' '} - -{' '} - - View GitHub issue - -

- ) : null} -
- ) + return } export const WebAnalyticsDashboard = (): JSX.Element => { return (
- +
diff --git a/frontend/src/scenes/web-analytics/WebTabs.tsx b/frontend/src/scenes/web-analytics/WebTabs.tsx new file mode 100644 index 00000000000000..7820cf9ce46782 --- /dev/null +++ b/frontend/src/scenes/web-analytics/WebTabs.tsx @@ -0,0 +1,65 @@ +import clsx from 'clsx' +import React from 'react' +import { useSliderPositioning } from 'lib/lemon-ui/hooks' + +const TRANSITION_MS = 200 +export const WebTabs = ({ + className, + activeTabId, + tabs, + setActiveTabId, +}: { + className?: string + activeTabId: string + tabs: { id: string; title: string; linkText: string; content: React.ReactNode }[] + setActiveTabId: (id: string) => void +}): JSX.Element => { + const activeTab = tabs.find((t) => t.id === activeTabId) + const { containerRef, selectionRef, sliderWidth, sliderOffset, transitioning } = useSliderPositioning< + HTMLUListElement, + HTMLLIElement + >(activeTabId, TRANSITION_MS) + + return ( +
+
+ {

{activeTab?.title}

} +
+ {tabs.length > 1 && ( + // TODO switch to a select if more than 3 +
    + {tabs.map(({ id, linkText }) => ( +
  • + +
  • + ))} +
+ )} +
+
+
+
+
+
{activeTab?.content}
+
+ ) +} diff --git a/frontend/src/scenes/web-analytics/webAnalyticsLogic.ts b/frontend/src/scenes/web-analytics/webAnalyticsLogic.ts index 04c7bf786508b6..744750bfa21de2 100644 --- a/frontend/src/scenes/web-analytics/webAnalyticsLogic.ts +++ b/frontend/src/scenes/web-analytics/webAnalyticsLogic.ts @@ -5,13 +5,13 @@ import { NodeKind, QuerySchema, WebAnalyticsPropertyFilters, WebStatsBreakdown } import { EventPropertyFilter, HogQLPropertyFilter, PropertyFilterType, PropertyOperator } from '~/types' import { isNotNil } from 'lib/utils' -interface Layout { +export interface WebTileLayout { colSpan?: number rowSpan?: number } interface BaseTile { - layout: Layout + layout: WebTileLayout } interface QueryTile extends BaseTile { @@ -19,7 +19,7 @@ interface QueryTile extends BaseTile { query: QuerySchema } -interface TabsTile extends BaseTile { +export interface TabsTile extends BaseTile { activeTabId: string setTabId: (id: string) => void tabs: { diff --git a/frontend/src/styles/utilities.scss b/frontend/src/styles/utilities.scss index b441a308aeaeb6..8e27162b8c7e01 100644 --- a/frontend/src/styles/utilities.scss +++ b/frontend/src/styles/utilities.scss @@ -410,6 +410,10 @@ $decorations: underline, overline, line-through, no-underline; color: inherit; } +.text-current { + color: currentColor; +} + @each $name, $hex in $colors { .hover\:text-#{$name}:hover { color: $hex;