Skip to content

Commit

Permalink
🚸 (results) Add time filter to results table as…
Browse files Browse the repository at this point in the history
  • Loading branch information
baptisteArno committed Feb 6, 2024
1 parent 3e2533b commit 066fabc
Show file tree
Hide file tree
Showing 20 changed files with 376 additions and 67 deletions.
2 changes: 1 addition & 1 deletion apps/builder/src/components/DropdownList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export const DropdownList = <T extends Item>({
)}
</FormLabel>
)}
<Menu isLazy placement="bottom-end" matchWidth>
<Menu isLazy>
<MenuButton
as={Button}
rightIcon={<ChevronLeftIcon transform={'rotate(-90deg)'} />}
Expand Down
3 changes: 2 additions & 1 deletion apps/builder/src/components/inputs/VariableSearchInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -213,9 +213,9 @@ export const VariableSearchInput = ({
<Popover
isOpen={isOpen}
initialFocusRef={inputRef}
matchWidth
isLazy
offset={[0, 2]}
placement="auto-start"
>
<PopoverAnchor>
<Input
Expand All @@ -239,6 +239,7 @@ export const VariableSearchInput = ({
shadow="lg"
onMouseDown={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
minW="250px"
>
{isCreateVariableButtonDisplayed && (
<Button
Expand Down
89 changes: 89 additions & 0 deletions apps/builder/src/features/analytics/api/getStats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import prisma from '@typebot.io/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { z } from 'zod'
import { canReadTypebots } from '@/helpers/databaseRules'
import { Stats, statsSchema } from '@typebot.io/schemas'
import { defaultTimeFilter, timeFilterValues } from '../constants'
import { parseDateFromTimeFilter } from '../helpers/parseDateFromTimeFilter'

export const getStats = authenticatedProcedure
.meta({
openapi: {
method: 'GET',
path: '/v1/typebots/{typebotId}/analytics/stats',
protect: true,
summary: 'Get results stats',
tags: ['Analytics'],
},
})
.input(
z.object({
typebotId: z.string(),
timeFilter: z.enum(timeFilterValues).default(defaultTimeFilter),
})
)
.output(z.object({ stats: statsSchema }))
.query(async ({ input: { typebotId, timeFilter }, ctx: { user } }) => {
const typebot = await prisma.typebot.findFirst({
where: canReadTypebots(typebotId, user),
select: { publishedTypebot: true, id: true },
})
if (!typebot?.publishedTypebot)
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Published typebot not found',
})

const date = parseDateFromTimeFilter(timeFilter)

const [totalViews, totalStarts, totalCompleted] = await prisma.$transaction(
[
prisma.result.count({
where: {
typebotId: typebot.id,
isArchived: false,
createdAt: date
? {
gte: date,
}
: undefined,
},
}),
prisma.result.count({
where: {
typebotId: typebot.id,
isArchived: false,
hasStarted: true,
createdAt: date
? {
gte: date,
}
: undefined,
},
}),
prisma.result.count({
where: {
typebotId: typebot.id,
isArchived: false,
isCompleted: true,
createdAt: date
? {
gte: date,
}
: undefined,
},
}),
]
)

const stats: Stats = {
totalViews,
totalStarts,
totalCompleted,
}

return {
stats,
}
})
2 changes: 2 additions & 0 deletions apps/builder/src/features/analytics/api/router.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { router } from '@/helpers/server/trpc'
import { getTotalAnswers } from './getTotalAnswers'
import { getTotalVisitedEdges } from './getTotalVisitedEdges'
import { getStats } from './getStats'

export const analyticsRouter = router({
getTotalAnswers,
getTotalVisitedEdges,
getStats,
})
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, { useState } from 'react'
import React from 'react'
import { StatsCards } from './StatsCards'
import { ChangePlanModal } from '@/features/billing/components/ChangePlanModal'
import { Graph } from '@/features/graph/components/Graph'
Expand All @@ -15,14 +15,22 @@ 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'
import { timeFilterValues } from '../constants'

export const AnalyticsGraphContainer = ({ stats }: { stats?: Stats }) => {
type Props = {
timeFilter: (typeof timeFilterValues)[number]
onTimeFilterChange: (timeFilter: (typeof timeFilterValues)[number]) => void
stats?: Stats
}

export const AnalyticsGraphContainer = ({
timeFilter,
onTimeFilterChange,
stats,
}: Props) => {
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,
Expand Down Expand Up @@ -85,7 +93,7 @@ export const AnalyticsGraphContainer = ({ stats }: { stats?: Stats }) => {
stats={stats}
pos="absolute"
timeFilter={timeFilter}
setTimeFilter={setTimeFilter}
onTimeFilterChange={onTimeFilterChange}
/>
</Flex>
)
Expand Down
20 changes: 7 additions & 13 deletions apps/builder/src/features/analytics/components/StatsCards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +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'
import { timeFilterValues } from '../constants'
import { TimeFilterDropdown } from './TimeFilterDropdown'

const computeCompletionRate =
(notAvailableLabel: string) =>
Expand All @@ -23,12 +23,12 @@ const computeCompletionRate =
export const StatsCards = ({
stats,
timeFilter,
setTimeFilter,
onTimeFilterChange,
...props
}: {
stats?: Stats
timeFilter: (typeof timeFilterValues)[number]
setTimeFilter: (timeFilter: (typeof timeFilterValues)[number]) => void
onTimeFilterChange: (timeFilter: (typeof timeFilterValues)[number]) => void
} & GridProps) => {
const { t } = useTranslate()
const bg = useColorModeValue('white', 'gray.900')
Expand Down Expand Up @@ -69,15 +69,9 @@ 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])
}
<TimeFilterDropdown
timeFilter={timeFilter}
onTimeFilterChange={onTimeFilterChange}
backgroundColor="white"
boxShadow="md"
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { DropdownList } from '@/components/DropdownList'
import { timeFilterLabels, timeFilterValues } from '../constants'
import { ButtonProps } from '@chakra-ui/react'

type Props = {
timeFilter: (typeof timeFilterValues)[number]
onTimeFilterChange: (timeFilter: (typeof timeFilterValues)[number]) => void
} & ButtonProps

export const TimeFilterDropdown = ({
timeFilter,
onTimeFilterChange,
...props
}: Props) => (
<DropdownList
items={Object.entries(timeFilterLabels).map(([value, label]) => ({
label,
value,
}))}
currentItem={timeFilter}
onItemSelect={(val) =>
onTimeFilterChange(val as (typeof timeFilterValues)[number])
}
{...props}
/>
)
4 changes: 4 additions & 0 deletions apps/builder/src/features/results/ResultsProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/consta
import { parseResultHeader } from '@typebot.io/lib/results/parseResultHeader'
import { convertResultsToTableData } from '@typebot.io/lib/results/convertResultsToTableData'
import { parseCellContent } from './helpers/parseCellContent'
import { timeFilterValues } from '../analytics/constants'

const resultsContext = createContext<{
resultsList: { results: ResultWithAnswers[] }[] | undefined
Expand All @@ -30,11 +31,13 @@ const resultsContext = createContext<{
}>({})

export const ResultsProvider = ({
timeFilter,
children,
typebotId,
totalResults,
onDeleteResults,
}: {
timeFilter: (typeof timeFilterValues)[number]
children: ReactNode
typebotId: string
totalResults: number
Expand All @@ -43,6 +46,7 @@ export const ResultsProvider = ({
const { publishedTypebot } = useTypebot()
const { showToast } = useToast()
const { data, fetchNextPage, hasNextPage, refetch } = useResultsQuery({
timeFilter,
typebotId,
onError: (error) => {
showToast({ description: error })
Expand Down
14 changes: 14 additions & 0 deletions apps/builder/src/features/results/api/getResults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import { TRPCError } from '@trpc/server'
import { resultWithAnswersSchema } from '@typebot.io/schemas'
import { z } from 'zod'
import { isReadTypebotForbidden } from '@/features/typebot/helpers/isReadTypebotForbidden'
import {
timeFilterValues,
defaultTimeFilter,
} from '@/features/analytics/constants'
import { parseDateFromTimeFilter } from '@/features/analytics/helpers/parseDateFromTimeFilter'

const maxLimit = 100

Expand All @@ -26,6 +31,7 @@ export const getResults = authenticatedProcedure
),
limit: z.coerce.number().min(1).max(maxLimit).default(50),
cursor: z.string().optional(),
timeFilter: z.enum(timeFilterValues).default(defaultTimeFilter),
})
)
.output(
Expand Down Expand Up @@ -70,13 +76,21 @@ export const getResults = authenticatedProcedure
})
if (!typebot || (await isReadTypebotForbidden(typebot, user)))
throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' })

const date = parseDateFromTimeFilter(input.timeFilter)

const results = await prisma.result.findMany({
take: limit + 1,
cursor: cursor ? { id: cursor } : undefined,
where: {
typebotId: typebot.id,
hasStarted: true,
isArchived: false,
createdAt: date
? {
gte: date,
}
: undefined,
},
orderBy: {
createdAt: 'desc',
Expand Down
43 changes: 31 additions & 12 deletions apps/builder/src/features/results/components/ResultsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@ import {
} from '@chakra-ui/react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useMemo } from 'react'
import { useStats } from '../hooks/useStats'
import { useMemo, useState } from 'react'
import { ResultsProvider } from '../ResultsProvider'
import { ResultsTableContainer } from './ResultsTableContainer'
import { TypebotNotFoundPage } from '@/features/editor/components/TypebotNotFoundPage'
import { trpc } from '@/lib/trpc'
import {
defaultTimeFilter,
timeFilterValues,
} from '@/features/analytics/constants'

export const ResultsPage = () => {
const router = useRouter()
Expand All @@ -32,18 +36,25 @@ export const ResultsPage = () => {
router.pathname.endsWith('analytics') ? '#f4f5f8' : 'white',
router.pathname.endsWith('analytics') ? 'gray.850' : 'gray.900'
)
const [timeFilter, setTimeFilter] =
useState<(typeof timeFilterValues)[number]>(defaultTimeFilter)

const { showToast } = useToast()

const { stats, mutate } = useStats({
typebotId: publishedTypebot?.typebotId,
onError: (err) => showToast({ title: err.name, description: err.message }),
})
const { data: { stats } = {}, refetch } = trpc.analytics.getStats.useQuery(
{
typebotId: publishedTypebot?.typebotId as string,
timeFilter,
},
{
enabled: !!publishedTypebot,
onError: (err) => showToast({ description: err.message }),
}
)

const handleDeletedResults = (total: number) => {
const handleDeletedResults = () => {
if (!stats) return
mutate({
stats: { ...stats, totalStarts: stats.totalStarts - total },
})
refetch()
}

if (is404) return <TypebotNotFoundPage />
Expand Down Expand Up @@ -100,14 +111,22 @@ export const ResultsPage = () => {
{workspace &&
publishedTypebot &&
(isAnalytics ? (
<AnalyticsGraphContainer stats={stats} />
<AnalyticsGraphContainer
stats={stats}
timeFilter={timeFilter}
onTimeFilterChange={setTimeFilter}
/>
) : (
<ResultsProvider
timeFilter={timeFilter}
typebotId={publishedTypebot.typebotId}
totalResults={stats?.totalStarts ?? 0}
onDeleteResults={handleDeletedResults}
>
<ResultsTableContainer />
<ResultsTableContainer
timeFilter={timeFilter}
onTimeFilterChange={setTimeFilter}
/>
</ResultsProvider>
))}
</Flex>
Expand Down

0 comments on commit 066fabc

Please sign in to comment.