Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(web-analytics): Improve clickability discovery of table rows #18220

Merged
merged 4 commits into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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