Skip to content

Commit

Permalink
feat(web-analytics): Improve clickability discovery of table rows (#1…
Browse files Browse the repository at this point in the history
  • Loading branch information
robbie-c authored Oct 30, 2023
1 parent 5e5b410 commit 8b3f09f
Show file tree
Hide file tree
Showing 9 changed files with 304 additions and 191 deletions.
1 change: 1 addition & 0 deletions frontend/src/lib/lemon-ui/LemonTable/TableRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ function TableRowRaw<T extends Record<string, any>>({
className={clsx(
rowClassNameDetermined,
rowStatusDetermined && `LemonTable__row--status-${rowStatusDetermined}`,
extraProps?.onClick ? 'hover:underline cursor-pointer hover:bg-primary-highlight' : undefined,
className
)}
// eslint-disable-next-line react/forbid-dom-props
Expand Down
1 change: 1 addition & 0 deletions frontend/src/queries/nodes/DataTable/DataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,7 @@ export function DataTable({ uniqueKey, query, setQuery, context, cachedResults }
(response as any).result.length > 0 ||
!responseLoading) && <LoadNext query={query.source} />
}
onRow={context?.rowProps}
/>
)}
{/* TODO: this doesn't seem like the right solution... */}
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/queries/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { InsightLogicProps } from '~/types'
import { ComponentType } from 'react'
import { ComponentType, HTMLProps } from 'react'
import { DataTableNode } from '~/queries/schema'

/** Pass custom metadata to queries. Used for e.g. custom columns in the DataTable. */
Expand All @@ -14,6 +14,7 @@ export interface QueryContext {
insightProps?: InsightLogicProps
emptyStateHeading?: string
emptyStateDetail?: string
rowProps?: (record: unknown) => Omit<HTMLProps<HTMLTableRowElement>, 'key'>
}

export type QueryContextColumnTitleComponent = ComponentType<{
Expand Down
164 changes: 164 additions & 0 deletions frontend/src/scenes/web-analytics/WebAnalyticsDataTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
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, useMemo } from 'react'
import { Query } from '~/queries/Query/Query'

const PercentageCell: QueryContextColumnComponent = ({ value }) => {
if (typeof value === 'number') {
return <span>{`${(value * 100).toFixed(1)}%`}</span>
} else {
return null
}
}

const NumericCell: QueryContextColumnComponent = ({ value }) => {
return <span>{typeof value === 'number' ? value.toLocaleString() : String(value)}</span>
}

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 <BreakdownValueCellInner value={value} />
}

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 <span>{value}</span>
}

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(
(breakdownValue: string) => {
togglePropertyFilter(propertyName, breakdownValue)
},
[togglePropertyFilter, propertyName]
)

const context = useMemo((): QueryContext => {
const rowProps: QueryContext['rowProps'] = (record: unknown) => {
const breakdownValue = getBreakdownValue(record)
if (breakdownValue === undefined) {
return {}
}
return {
onClick: () => onClick(breakdownValue),
}
}
return {
...webAnalyticsDataTableQueryContext,
rowProps,
}
}, [onClick])

return <Query query={query} readOnly={true} context={context} />
}

const getBreakdownValue = (record: unknown): string | undefined => {
if (typeof record !== 'object' || !record || !('result' in record)) {
return undefined
}
const result = record.result
if (!Array.isArray(result)) {
return undefined
}
// assume that the first element is the value
const breakdownValue = result[0]
if (typeof breakdownValue !== 'string') {
return undefined
}
return breakdownValue
}
34 changes: 34 additions & 0 deletions frontend/src/scenes/web-analytics/WebAnalyticsNotice.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<LemonBanner type={'info'}>
<p>PostHog Web Analytics is in closed Alpha. Thanks for taking part! We'd love to hear what you think.</p>
{showSupportOptions ? (
<p>
<Link onClick={() => openSupportForm('bug')}>
<IconBugReport /> Report a bug
</Link>{' '}
-{' '}
<Link onClick={() => openSupportForm('feedback')}>
<IconFeedback /> Give feedback
</Link>{' '}
-{' '}
<Link to={'https://github.com/PostHog/posthog/issues/18177'}>
<IconGithub /> View GitHub issue
</Link>
</p>
) : null}
</LemonBanner>
)
}
Loading

0 comments on commit 8b3f09f

Please sign in to comment.