Skip to content

Commit

Permalink
✨ (analytics) Add time dropdown to filter analytics with a time range
Browse files Browse the repository at this point in the history
  • Loading branch information
baptisteArno committed Jan 29, 2024
1 parent 07928c7 commit 515fcaf
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 15 deletions.
49 changes: 40 additions & 9 deletions apps/builder/src/components/DropdownList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,22 @@ import { ChevronLeftIcon } from '@/components/icons'
import React, { ReactNode } from 'react'
import { MoreInfoTooltip } from './MoreInfoTooltip'

type Item =
| string
| number
| {
label: string
value: string
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Props<T extends readonly any[]> = {
currentItem: T[number] | undefined
onItemSelect: (item: T[number]) => void
items: T
type Props<T extends Item> = {
currentItem: string | number | undefined
onItemSelect: (
value: T extends string ? T : T extends number ? T : string,
item?: T
) => void
items: readonly T[]
placeholder?: string
label?: string
isRequired?: boolean
Expand All @@ -31,7 +42,7 @@ type Props<T extends readonly any[]> = {
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const DropdownList = <T extends readonly any[]>({
export const DropdownList = <T extends Item>({
currentItem,
onItemSelect,
items,
Expand All @@ -43,8 +54,14 @@ export const DropdownList = <T extends readonly any[]>({
moreInfoTooltip,
...props
}: Props<T> & ButtonProps) => {
const handleMenuItemClick = (operator: T[number]) => () => {
onItemSelect(operator)
const handleMenuItemClick = (item: T) => () => {
if (typeof item === 'string' || typeof item === 'number')
onItemSelect(item as T extends string ? T : T extends number ? T : string)
else
onItemSelect(
item.value as T extends string ? T : T extends number ? T : string,
item
)
}
return (
<FormControl
Expand Down Expand Up @@ -73,7 +90,15 @@ export const DropdownList = <T extends readonly any[]>({
{...props}
>
<chakra.span noOfLines={1} display="block">
{currentItem ?? placeholder ?? 'Select an item'}
{currentItem
? getItemLabel(
items?.find((item) =>
typeof item === 'string' || typeof item === 'number'
? currentItem === item
: currentItem === item.value
)
)
: placeholder ?? 'Select an item'}
</chakra.span>
</MenuButton>
<Portal>
Expand All @@ -88,7 +113,7 @@ export const DropdownList = <T extends readonly any[]>({
textOverflow="ellipsis"
onClick={handleMenuItemClick(item)}
>
{item}
{typeof item === 'object' ? item.label : item}
</MenuItem>
))}
</Stack>
Expand All @@ -99,3 +124,9 @@ export const DropdownList = <T extends readonly any[]>({
</FormControl>
)
}

const getItemLabel = (item?: Item) => {
if (!item) return ''
if (typeof item === 'object') return item.label
return item
}
12 changes: 11 additions & 1 deletion apps/builder/src/features/analytics/api/getTotalAnswers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { canReadTypebots } from '@/helpers/databaseRules'
import { totalAnswersSchema } from '@typebot.io/schemas/features/analytics'
import { parseGroups } from '@typebot.io/schemas'
import { isInputBlock } from '@typebot.io/lib'
import { defaultTimeFilter, timeFilterValues } from '../constants'
import { parseDateFromTimeFilter } from '../helpers/parseDateFromTimeFilter'

export const getTotalAnswers = authenticatedProcedure
.meta({
Expand All @@ -20,10 +22,11 @@ export const getTotalAnswers = authenticatedProcedure
.input(
z.object({
typebotId: z.string(),
timeFilter: z.enum(timeFilterValues).default(defaultTimeFilter),
})
)
.output(z.object({ totalAnswers: z.array(totalAnswersSchema) }))
.query(async ({ input: { typebotId }, ctx: { user } }) => {
.query(async ({ input: { typebotId, timeFilter }, ctx: { user } }) => {
const typebot = await prisma.typebot.findFirst({
where: canReadTypebots(typebotId, user),
select: { publishedTypebot: true },
Expand All @@ -34,11 +37,18 @@ export const getTotalAnswers = authenticatedProcedure
message: 'Published typebot not found',
})

const date = parseDateFromTimeFilter(timeFilter)

const totalAnswersPerBlock = await prisma.answer.groupBy({
by: ['blockId'],
where: {
result: {
typebotId: typebot.publishedTypebot.typebotId,
createdAt: date
? {
gte: date,
}
: undefined,
},
blockId: {
in: parseGroups(typebot.publishedTypebot.groups, {
Expand Down
12 changes: 11 additions & 1 deletion apps/builder/src/features/analytics/api/getTotalVisitedEdges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { TRPCError } from '@trpc/server'
import { z } from 'zod'
import { canReadTypebots } from '@/helpers/databaseRules'
import { totalVisitedEdgesSchema } from '@typebot.io/schemas'
import { defaultTimeFilter, timeFilterValues } from '../constants'
import { parseDateFromTimeFilter } from '../helpers/parseDateFromTimeFilter'

export const getTotalVisitedEdges = authenticatedProcedure
.meta({
Expand All @@ -18,14 +20,15 @@ export const getTotalVisitedEdges = authenticatedProcedure
.input(
z.object({
typebotId: z.string(),
timeFilter: z.enum(timeFilterValues).default(defaultTimeFilter),
})
)
.output(
z.object({
totalVisitedEdges: z.array(totalVisitedEdgesSchema),
})
)
.query(async ({ input: { typebotId }, ctx: { user } }) => {
.query(async ({ input: { typebotId, timeFilter }, ctx: { user } }) => {
const typebot = await prisma.typebot.findFirst({
where: canReadTypebots(typebotId, user),
select: { id: true },
Expand All @@ -36,11 +39,18 @@ export const getTotalVisitedEdges = authenticatedProcedure
message: 'Published typebot not found',
})

const date = parseDateFromTimeFilter(timeFilter)

const edges = await prisma.visitedEdge.groupBy({
by: ['edgeId'],
where: {
result: {
typebotId: typebot.id,
createdAt: date
? {
gte: date,
}
: undefined,
},
},
_count: { resultId: true },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from '@chakra-ui/react'
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
import { Stats } from '@typebot.io/schemas'
import React from 'react'
import React, { useState } from 'react'
import { StatsCards } from './StatsCards'
import { ChangePlanModal } from '@/features/billing/components/ChangePlanModal'
import { Graph } from '@/features/graph/components/Graph'
Expand All @@ -15,21 +15,26 @@ import { useTranslate } from '@tolgee/react'
import { trpc } from '@/lib/trpc'
import { isDefined } from '@typebot.io/lib'
import { EventsCoordinatesProvider } from '@/features/graph/providers/EventsCoordinateProvider'
import { defaultTimeFilter, timeFilterValues } from '../constants'

export const AnalyticsGraphContainer = ({ stats }: { stats?: Stats }) => {
const { t } = useTranslate()
const { isOpen, onOpen, onClose } = useDisclosure()
const { typebot, publishedTypebot } = useTypebot()
const [timeFilter, setTimeFilter] =
useState<(typeof timeFilterValues)[number]>(defaultTimeFilter)
const { data } = trpc.analytics.getTotalAnswers.useQuery(
{
typebotId: typebot?.id as string,
timeFilter,
},
{ enabled: isDefined(publishedTypebot) }
)

const { data: edgesData } = trpc.analytics.getTotalVisitedEdges.useQuery(
{
typebotId: typebot?.id as string,
timeFilter,
},
{ enabled: isDefined(publishedTypebot) }
)
Expand Down Expand Up @@ -76,7 +81,12 @@ export const AnalyticsGraphContainer = ({ stats }: { stats?: Stats }) => {
type={t('billing.limitMessage.analytics')}
excludedPlans={['STARTER']}
/>
<StatsCards stats={stats} pos="absolute" />
<StatsCards
stats={stats}
pos="absolute"
timeFilter={timeFilter}
setTimeFilter={setTimeFilter}
/>
</Flex>
)
}
29 changes: 27 additions & 2 deletions apps/builder/src/features/analytics/components/StatsCards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
} from '@chakra-ui/react'
import { Stats } from '@typebot.io/schemas'
import React from 'react'
import { DropdownList } from '@/components/DropdownList'
import { timeFilterLabels, timeFilterValues } from '../constants'

const computeCompletionRate =
(notAvailableLabel: string) =>
Expand All @@ -20,13 +22,24 @@ const computeCompletionRate =

export const StatsCards = ({
stats,
timeFilter,
setTimeFilter,
...props
}: { stats?: Stats } & GridProps) => {
}: {
stats?: Stats
timeFilter: (typeof timeFilterValues)[number]
setTimeFilter: (timeFilter: (typeof timeFilterValues)[number]) => void
} & GridProps) => {
const { t } = useTranslate()
const bg = useColorModeValue('white', 'gray.900')

return (
<SimpleGrid columns={{ base: 1, md: 3 }} spacing="6" {...props}>
<SimpleGrid
columns={{ base: 1, md: 4 }}
spacing="6"
alignItems="center"
{...props}
>
<Stat bgColor={bg} p="4" rounded="md" boxShadow="md">
<StatLabel>{t('analytics.viewsLabel')}</StatLabel>
{stats ? (
Expand Down Expand Up @@ -56,6 +69,18 @@ export const StatsCards = ({
<Skeleton w="50%" h="10px" mt="2" />
)}
</Stat>
<DropdownList
items={Object.entries(timeFilterLabels).map(([value, label]) => ({
label,
value,
}))}
currentItem={timeFilter}
onItemSelect={(val) =>
setTimeFilter(val as (typeof timeFilterValues)[number])
}
backgroundColor="white"
boxShadow="md"
/>
</SimpleGrid>
)
}
20 changes: 20 additions & 0 deletions apps/builder/src/features/analytics/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export const timeFilterValues = [
'today',
'last7Days',
'last30Days',
'yearToDate',
'allTime',
] as const

export const timeFilterLabels: Record<
(typeof timeFilterValues)[number],
string
> = {
today: 'Today',
last7Days: 'Last 7 days',
last30Days: 'Last 30 days',
yearToDate: 'Year to date',
allTime: 'All time',
}

export const defaultTimeFilter = 'today' as const
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { timeFilterValues } from '../constants'

export const parseDateFromTimeFilter = (
timeFilter: (typeof timeFilterValues)[number]
): Date | undefined => {
switch (timeFilter) {
case 'today':
return new Date(new Date().setHours(0, 0, 0, 0))
case 'last7Days':
return new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
case 'last30Days':
return new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
case 'yearToDate':
return new Date(new Date().getFullYear(), 0, 1)
case 'allTime':
return
}
}

0 comments on commit 515fcaf

Please sign in to comment.