🚸 (results) Add time filter to results table as…
This commit is contained in:
@ -79,7 +79,7 @@ export const DropdownList = <T extends Item>({
|
|||||||
)}
|
)}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
)}
|
)}
|
||||||
<Menu isLazy placement="bottom-end" matchWidth>
|
<Menu isLazy>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
as={Button}
|
as={Button}
|
||||||
rightIcon={<ChevronLeftIcon transform={'rotate(-90deg)'} />}
|
rightIcon={<ChevronLeftIcon transform={'rotate(-90deg)'} />}
|
||||||
|
@ -213,9 +213,9 @@ export const VariableSearchInput = ({
|
|||||||
<Popover
|
<Popover
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
initialFocusRef={inputRef}
|
initialFocusRef={inputRef}
|
||||||
matchWidth
|
|
||||||
isLazy
|
isLazy
|
||||||
offset={[0, 2]}
|
offset={[0, 2]}
|
||||||
|
placement="auto-start"
|
||||||
>
|
>
|
||||||
<PopoverAnchor>
|
<PopoverAnchor>
|
||||||
<Input
|
<Input
|
||||||
@ -239,6 +239,7 @@ export const VariableSearchInput = ({
|
|||||||
shadow="lg"
|
shadow="lg"
|
||||||
onMouseDown={(e) => e.stopPropagation()}
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
onPointerDown={(e) => e.stopPropagation()}
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
|
minW="250px"
|
||||||
>
|
>
|
||||||
{isCreateVariableButtonDisplayed && (
|
{isCreateVariableButtonDisplayed && (
|
||||||
<Button
|
<Button
|
||||||
|
89
apps/builder/src/features/analytics/api/getStats.ts
Normal file
89
apps/builder/src/features/analytics/api/getStats.ts
Normal file
@ -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,
|
||||||
|
}
|
||||||
|
})
|
@ -1,8 +1,10 @@
|
|||||||
import { router } from '@/helpers/server/trpc'
|
import { router } from '@/helpers/server/trpc'
|
||||||
import { getTotalAnswers } from './getTotalAnswers'
|
import { getTotalAnswers } from './getTotalAnswers'
|
||||||
import { getTotalVisitedEdges } from './getTotalVisitedEdges'
|
import { getTotalVisitedEdges } from './getTotalVisitedEdges'
|
||||||
|
import { getStats } from './getStats'
|
||||||
|
|
||||||
export const analyticsRouter = router({
|
export const analyticsRouter = router({
|
||||||
getTotalAnswers,
|
getTotalAnswers,
|
||||||
getTotalVisitedEdges,
|
getTotalVisitedEdges,
|
||||||
|
getStats,
|
||||||
})
|
})
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||||
import { Stats } from '@typebot.io/schemas'
|
import { Stats } from '@typebot.io/schemas'
|
||||||
import React, { useState } from 'react'
|
import React from 'react'
|
||||||
import { StatsCards } from './StatsCards'
|
import { StatsCards } from './StatsCards'
|
||||||
import { ChangePlanModal } from '@/features/billing/components/ChangePlanModal'
|
import { ChangePlanModal } from '@/features/billing/components/ChangePlanModal'
|
||||||
import { Graph } from '@/features/graph/components/Graph'
|
import { Graph } from '@/features/graph/components/Graph'
|
||||||
@ -15,14 +15,22 @@ import { useTranslate } from '@tolgee/react'
|
|||||||
import { trpc } from '@/lib/trpc'
|
import { trpc } from '@/lib/trpc'
|
||||||
import { isDefined } from '@typebot.io/lib'
|
import { isDefined } from '@typebot.io/lib'
|
||||||
import { EventsCoordinatesProvider } from '@/features/graph/providers/EventsCoordinateProvider'
|
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 { t } = useTranslate()
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||||
const { typebot, publishedTypebot } = useTypebot()
|
const { typebot, publishedTypebot } = useTypebot()
|
||||||
const [timeFilter, setTimeFilter] =
|
|
||||||
useState<(typeof timeFilterValues)[number]>(defaultTimeFilter)
|
|
||||||
const { data } = trpc.analytics.getTotalAnswers.useQuery(
|
const { data } = trpc.analytics.getTotalAnswers.useQuery(
|
||||||
{
|
{
|
||||||
typebotId: typebot?.id as string,
|
typebotId: typebot?.id as string,
|
||||||
@ -85,7 +93,7 @@ export const AnalyticsGraphContainer = ({ stats }: { stats?: Stats }) => {
|
|||||||
stats={stats}
|
stats={stats}
|
||||||
pos="absolute"
|
pos="absolute"
|
||||||
timeFilter={timeFilter}
|
timeFilter={timeFilter}
|
||||||
setTimeFilter={setTimeFilter}
|
onTimeFilterChange={onTimeFilterChange}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
)
|
||||||
|
@ -10,8 +10,8 @@ import {
|
|||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import { Stats } from '@typebot.io/schemas'
|
import { Stats } from '@typebot.io/schemas'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { DropdownList } from '@/components/DropdownList'
|
import { timeFilterValues } from '../constants'
|
||||||
import { timeFilterLabels, timeFilterValues } from '../constants'
|
import { TimeFilterDropdown } from './TimeFilterDropdown'
|
||||||
|
|
||||||
const computeCompletionRate =
|
const computeCompletionRate =
|
||||||
(notAvailableLabel: string) =>
|
(notAvailableLabel: string) =>
|
||||||
@ -23,12 +23,12 @@ const computeCompletionRate =
|
|||||||
export const StatsCards = ({
|
export const StatsCards = ({
|
||||||
stats,
|
stats,
|
||||||
timeFilter,
|
timeFilter,
|
||||||
setTimeFilter,
|
onTimeFilterChange,
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
stats?: Stats
|
stats?: Stats
|
||||||
timeFilter: (typeof timeFilterValues)[number]
|
timeFilter: (typeof timeFilterValues)[number]
|
||||||
setTimeFilter: (timeFilter: (typeof timeFilterValues)[number]) => void
|
onTimeFilterChange: (timeFilter: (typeof timeFilterValues)[number]) => void
|
||||||
} & GridProps) => {
|
} & GridProps) => {
|
||||||
const { t } = useTranslate()
|
const { t } = useTranslate()
|
||||||
const bg = useColorModeValue('white', 'gray.900')
|
const bg = useColorModeValue('white', 'gray.900')
|
||||||
@ -69,15 +69,9 @@ export const StatsCards = ({
|
|||||||
<Skeleton w="50%" h="10px" mt="2" />
|
<Skeleton w="50%" h="10px" mt="2" />
|
||||||
)}
|
)}
|
||||||
</Stat>
|
</Stat>
|
||||||
<DropdownList
|
<TimeFilterDropdown
|
||||||
items={Object.entries(timeFilterLabels).map(([value, label]) => ({
|
timeFilter={timeFilter}
|
||||||
label,
|
onTimeFilterChange={onTimeFilterChange}
|
||||||
value,
|
|
||||||
}))}
|
|
||||||
currentItem={timeFilter}
|
|
||||||
onItemSelect={(val) =>
|
|
||||||
setTimeFilter(val as (typeof timeFilterValues)[number])
|
|
||||||
}
|
|
||||||
backgroundColor="white"
|
backgroundColor="white"
|
||||||
boxShadow="md"
|
boxShadow="md"
|
||||||
/>
|
/>
|
||||||
|
@ -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}
|
||||||
|
/>
|
||||||
|
)
|
@ -14,6 +14,7 @@ import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/consta
|
|||||||
import { parseResultHeader } from '@typebot.io/lib/results/parseResultHeader'
|
import { parseResultHeader } from '@typebot.io/lib/results/parseResultHeader'
|
||||||
import { convertResultsToTableData } from '@typebot.io/lib/results/convertResultsToTableData'
|
import { convertResultsToTableData } from '@typebot.io/lib/results/convertResultsToTableData'
|
||||||
import { parseCellContent } from './helpers/parseCellContent'
|
import { parseCellContent } from './helpers/parseCellContent'
|
||||||
|
import { timeFilterValues } from '../analytics/constants'
|
||||||
|
|
||||||
const resultsContext = createContext<{
|
const resultsContext = createContext<{
|
||||||
resultsList: { results: ResultWithAnswers[] }[] | undefined
|
resultsList: { results: ResultWithAnswers[] }[] | undefined
|
||||||
@ -30,11 +31,13 @@ const resultsContext = createContext<{
|
|||||||
}>({})
|
}>({})
|
||||||
|
|
||||||
export const ResultsProvider = ({
|
export const ResultsProvider = ({
|
||||||
|
timeFilter,
|
||||||
children,
|
children,
|
||||||
typebotId,
|
typebotId,
|
||||||
totalResults,
|
totalResults,
|
||||||
onDeleteResults,
|
onDeleteResults,
|
||||||
}: {
|
}: {
|
||||||
|
timeFilter: (typeof timeFilterValues)[number]
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
typebotId: string
|
typebotId: string
|
||||||
totalResults: number
|
totalResults: number
|
||||||
@ -43,6 +46,7 @@ export const ResultsProvider = ({
|
|||||||
const { publishedTypebot } = useTypebot()
|
const { publishedTypebot } = useTypebot()
|
||||||
const { showToast } = useToast()
|
const { showToast } = useToast()
|
||||||
const { data, fetchNextPage, hasNextPage, refetch } = useResultsQuery({
|
const { data, fetchNextPage, hasNextPage, refetch } = useResultsQuery({
|
||||||
|
timeFilter,
|
||||||
typebotId,
|
typebotId,
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
showToast({ description: error })
|
showToast({ description: error })
|
||||||
|
@ -4,6 +4,11 @@ import { TRPCError } from '@trpc/server'
|
|||||||
import { resultWithAnswersSchema } from '@typebot.io/schemas'
|
import { resultWithAnswersSchema } from '@typebot.io/schemas'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { isReadTypebotForbidden } from '@/features/typebot/helpers/isReadTypebotForbidden'
|
import { isReadTypebotForbidden } from '@/features/typebot/helpers/isReadTypebotForbidden'
|
||||||
|
import {
|
||||||
|
timeFilterValues,
|
||||||
|
defaultTimeFilter,
|
||||||
|
} from '@/features/analytics/constants'
|
||||||
|
import { parseDateFromTimeFilter } from '@/features/analytics/helpers/parseDateFromTimeFilter'
|
||||||
|
|
||||||
const maxLimit = 100
|
const maxLimit = 100
|
||||||
|
|
||||||
@ -26,6 +31,7 @@ export const getResults = authenticatedProcedure
|
|||||||
),
|
),
|
||||||
limit: z.coerce.number().min(1).max(maxLimit).default(50),
|
limit: z.coerce.number().min(1).max(maxLimit).default(50),
|
||||||
cursor: z.string().optional(),
|
cursor: z.string().optional(),
|
||||||
|
timeFilter: z.enum(timeFilterValues).default(defaultTimeFilter),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.output(
|
.output(
|
||||||
@ -70,6 +76,9 @@ export const getResults = authenticatedProcedure
|
|||||||
})
|
})
|
||||||
if (!typebot || (await isReadTypebotForbidden(typebot, user)))
|
if (!typebot || (await isReadTypebotForbidden(typebot, user)))
|
||||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' })
|
throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' })
|
||||||
|
|
||||||
|
const date = parseDateFromTimeFilter(input.timeFilter)
|
||||||
|
|
||||||
const results = await prisma.result.findMany({
|
const results = await prisma.result.findMany({
|
||||||
take: limit + 1,
|
take: limit + 1,
|
||||||
cursor: cursor ? { id: cursor } : undefined,
|
cursor: cursor ? { id: cursor } : undefined,
|
||||||
@ -77,6 +86,11 @@ export const getResults = authenticatedProcedure
|
|||||||
typebotId: typebot.id,
|
typebotId: typebot.id,
|
||||||
hasStarted: true,
|
hasStarted: true,
|
||||||
isArchived: false,
|
isArchived: false,
|
||||||
|
createdAt: date
|
||||||
|
? {
|
||||||
|
gte: date,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
createdAt: 'desc',
|
createdAt: 'desc',
|
||||||
|
@ -14,11 +14,15 @@ import {
|
|||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { useMemo } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { useStats } from '../hooks/useStats'
|
|
||||||
import { ResultsProvider } from '../ResultsProvider'
|
import { ResultsProvider } from '../ResultsProvider'
|
||||||
import { ResultsTableContainer } from './ResultsTableContainer'
|
import { ResultsTableContainer } from './ResultsTableContainer'
|
||||||
import { TypebotNotFoundPage } from '@/features/editor/components/TypebotNotFoundPage'
|
import { TypebotNotFoundPage } from '@/features/editor/components/TypebotNotFoundPage'
|
||||||
|
import { trpc } from '@/lib/trpc'
|
||||||
|
import {
|
||||||
|
defaultTimeFilter,
|
||||||
|
timeFilterValues,
|
||||||
|
} from '@/features/analytics/constants'
|
||||||
|
|
||||||
export const ResultsPage = () => {
|
export const ResultsPage = () => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@ -32,18 +36,25 @@ export const ResultsPage = () => {
|
|||||||
router.pathname.endsWith('analytics') ? '#f4f5f8' : 'white',
|
router.pathname.endsWith('analytics') ? '#f4f5f8' : 'white',
|
||||||
router.pathname.endsWith('analytics') ? 'gray.850' : 'gray.900'
|
router.pathname.endsWith('analytics') ? 'gray.850' : 'gray.900'
|
||||||
)
|
)
|
||||||
|
const [timeFilter, setTimeFilter] =
|
||||||
|
useState<(typeof timeFilterValues)[number]>(defaultTimeFilter)
|
||||||
|
|
||||||
const { showToast } = useToast()
|
const { showToast } = useToast()
|
||||||
|
|
||||||
const { stats, mutate } = useStats({
|
const { data: { stats } = {}, refetch } = trpc.analytics.getStats.useQuery(
|
||||||
typebotId: publishedTypebot?.typebotId,
|
{
|
||||||
onError: (err) => showToast({ title: err.name, description: err.message }),
|
typebotId: publishedTypebot?.typebotId as string,
|
||||||
})
|
timeFilter,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!publishedTypebot,
|
||||||
|
onError: (err) => showToast({ description: err.message }),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const handleDeletedResults = (total: number) => {
|
const handleDeletedResults = () => {
|
||||||
if (!stats) return
|
if (!stats) return
|
||||||
mutate({
|
refetch()
|
||||||
stats: { ...stats, totalStarts: stats.totalStarts - total },
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is404) return <TypebotNotFoundPage />
|
if (is404) return <TypebotNotFoundPage />
|
||||||
@ -100,14 +111,22 @@ export const ResultsPage = () => {
|
|||||||
{workspace &&
|
{workspace &&
|
||||||
publishedTypebot &&
|
publishedTypebot &&
|
||||||
(isAnalytics ? (
|
(isAnalytics ? (
|
||||||
<AnalyticsGraphContainer stats={stats} />
|
<AnalyticsGraphContainer
|
||||||
|
stats={stats}
|
||||||
|
timeFilter={timeFilter}
|
||||||
|
onTimeFilterChange={setTimeFilter}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ResultsProvider
|
<ResultsProvider
|
||||||
|
timeFilter={timeFilter}
|
||||||
typebotId={publishedTypebot.typebotId}
|
typebotId={publishedTypebot.typebotId}
|
||||||
totalResults={stats?.totalStarts ?? 0}
|
totalResults={stats?.totalStarts ?? 0}
|
||||||
onDeleteResults={handleDeletedResults}
|
onDeleteResults={handleDeletedResults}
|
||||||
>
|
>
|
||||||
<ResultsTableContainer />
|
<ResultsTableContainer
|
||||||
|
timeFilter={timeFilter}
|
||||||
|
onTimeFilterChange={setTimeFilter}
|
||||||
|
/>
|
||||||
</ResultsProvider>
|
</ResultsProvider>
|
||||||
))}
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -6,8 +6,16 @@ import { useResults } from '../ResultsProvider'
|
|||||||
import { ResultModal } from './ResultModal'
|
import { ResultModal } from './ResultModal'
|
||||||
import { ResultsTable } from './table/ResultsTable'
|
import { ResultsTable } from './table/ResultsTable'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
|
import { timeFilterValues } from '@/features/analytics/constants'
|
||||||
|
|
||||||
export const ResultsTableContainer = () => {
|
type Props = {
|
||||||
|
timeFilter: (typeof timeFilterValues)[number]
|
||||||
|
onTimeFilterChange: (timeFilter: (typeof timeFilterValues)[number]) => void
|
||||||
|
}
|
||||||
|
export const ResultsTableContainer = ({
|
||||||
|
timeFilter,
|
||||||
|
onTimeFilterChange,
|
||||||
|
}: Props) => {
|
||||||
const { query } = useRouter()
|
const { query } = useRouter()
|
||||||
const {
|
const {
|
||||||
flatResults: results,
|
flatResults: results,
|
||||||
@ -61,8 +69,10 @@ export const ResultsTableContainer = () => {
|
|||||||
data={tableData}
|
data={tableData}
|
||||||
onScrollToBottom={fetchNextPage}
|
onScrollToBottom={fetchNextPage}
|
||||||
hasMore={hasNextPage}
|
hasMore={hasNextPage}
|
||||||
|
timeFilter={timeFilter}
|
||||||
onLogOpenIndex={handleLogOpenIndex}
|
onLogOpenIndex={handleLogOpenIndex}
|
||||||
onResultExpandIndex={handleResultExpandIndex}
|
onResultExpandIndex={handleResultExpandIndex}
|
||||||
|
onTimeFilterChange={onTimeFilterChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
@ -31,12 +31,16 @@ import { IndeterminateCheckbox } from './IndeterminateCheckbox'
|
|||||||
import { colors } from '@/lib/theme'
|
import { colors } from '@/lib/theme'
|
||||||
import { HeaderIcon } from '../HeaderIcon'
|
import { HeaderIcon } from '../HeaderIcon'
|
||||||
import { parseColumnsOrder } from '@typebot.io/lib/results/parseColumnsOrder'
|
import { parseColumnsOrder } from '@typebot.io/lib/results/parseColumnsOrder'
|
||||||
|
import { TimeFilterDropdown } from '@/features/analytics/components/TimeFilterDropdown'
|
||||||
|
import { timeFilterValues } from '@/features/analytics/constants'
|
||||||
|
|
||||||
type ResultsTableProps = {
|
type ResultsTableProps = {
|
||||||
resultHeader: ResultHeaderCell[]
|
resultHeader: ResultHeaderCell[]
|
||||||
data: TableData[]
|
data: TableData[]
|
||||||
hasMore?: boolean
|
hasMore?: boolean
|
||||||
preferences?: ResultsTablePreferences
|
preferences?: ResultsTablePreferences
|
||||||
|
timeFilter: (typeof timeFilterValues)[number]
|
||||||
|
onTimeFilterChange: (timeFilter: (typeof timeFilterValues)[number]) => void
|
||||||
onScrollToBottom: () => void
|
onScrollToBottom: () => void
|
||||||
onLogOpenIndex: (index: number) => () => void
|
onLogOpenIndex: (index: number) => () => void
|
||||||
onResultExpandIndex: (index: number) => () => void
|
onResultExpandIndex: (index: number) => () => void
|
||||||
@ -47,6 +51,8 @@ export const ResultsTable = ({
|
|||||||
data,
|
data,
|
||||||
hasMore,
|
hasMore,
|
||||||
preferences,
|
preferences,
|
||||||
|
timeFilter,
|
||||||
|
onTimeFilterChange,
|
||||||
onScrollToBottom,
|
onScrollToBottom,
|
||||||
onLogOpenIndex,
|
onLogOpenIndex,
|
||||||
onResultExpandIndex,
|
onResultExpandIndex,
|
||||||
@ -222,6 +228,11 @@ export const ResultsTable = ({
|
|||||||
onClearSelection={() => setRowSelection({})}
|
onClearSelection={() => setRowSelection({})}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<TimeFilterDropdown
|
||||||
|
timeFilter={timeFilter}
|
||||||
|
onTimeFilterChange={onTimeFilterChange}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
<TableSettingsButton
|
<TableSettingsButton
|
||||||
resultHeader={resultHeader}
|
resultHeader={resultHeader}
|
||||||
columnVisibility={columnsVisibility}
|
columnVisibility={columnsVisibility}
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
|
import { timeFilterValues } from '@/features/analytics/constants'
|
||||||
import { trpc } from '@/lib/trpc'
|
import { trpc } from '@/lib/trpc'
|
||||||
|
|
||||||
export const useResultsQuery = ({
|
type Params = {
|
||||||
typebotId,
|
timeFilter: (typeof timeFilterValues)[number]
|
||||||
onError,
|
|
||||||
}: {
|
|
||||||
typebotId: string
|
typebotId: string
|
||||||
onError?: (error: string) => void
|
onError?: (error: string) => void
|
||||||
}) => {
|
}
|
||||||
|
|
||||||
|
export const useResultsQuery = ({ timeFilter, typebotId, onError }: Params) => {
|
||||||
const { data, error, fetchNextPage, hasNextPage, refetch } =
|
const { data, error, fetchNextPage, hasNextPage, refetch } =
|
||||||
trpc.results.getResults.useInfiniteQuery(
|
trpc.results.getResults.useInfiniteQuery(
|
||||||
{
|
{
|
||||||
|
timeFilter,
|
||||||
typebotId,
|
typebotId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
import { Stats } from '@typebot.io/schemas'
|
|
||||||
import { fetcher } from '@/helpers/fetcher'
|
|
||||||
import useSWR from 'swr'
|
|
||||||
|
|
||||||
export const useStats = ({
|
|
||||||
typebotId,
|
|
||||||
onError,
|
|
||||||
}: {
|
|
||||||
typebotId?: string
|
|
||||||
onError: (error: Error) => void
|
|
||||||
}) => {
|
|
||||||
const { data, error, mutate } = useSWR<{ stats: Stats }, Error>(
|
|
||||||
typebotId ? `/api/typebots/${typebotId}/analytics/stats` : null,
|
|
||||||
fetcher
|
|
||||||
)
|
|
||||||
if (error) onError(error)
|
|
||||||
return {
|
|
||||||
stats: data?.stats,
|
|
||||||
isLoading: !error && !data,
|
|
||||||
mutate,
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,6 +5,7 @@ import { canReadTypebots } from '@/helpers/databaseRules'
|
|||||||
import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser'
|
import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser'
|
||||||
import { methodNotAllowed, notAuthenticated } from '@typebot.io/lib/api'
|
import { methodNotAllowed, notAuthenticated } from '@typebot.io/lib/api'
|
||||||
|
|
||||||
|
// TODO: Delete, as it has been migrated to tRPC
|
||||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
const user = await getAuthenticatedUser(req, res)
|
const user = await getAuthenticatedUser(req, res)
|
||||||
if (!user) return notAuthenticated(res)
|
if (!user) return notAuthenticated(res)
|
||||||
|
@ -8,7 +8,7 @@ const AnalyticsPage = ResultsPage
|
|||||||
export const getServerSideProps = async (
|
export const getServerSideProps = async (
|
||||||
context: GetServerSidePropsContext
|
context: GetServerSidePropsContext
|
||||||
) => {
|
) => {
|
||||||
if (!env.NEXT_PUBLIC_POSTHOG_KEY)
|
if (!env.NEXT_PUBLIC_POSTHOG_KEY || env.NEXT_PUBLIC_E2E_TEST)
|
||||||
return {
|
return {
|
||||||
props: {},
|
props: {},
|
||||||
}
|
}
|
||||||
|
4
apps/docs/api-reference/analytics/get-stats.mdx
Normal file
4
apps/docs/api-reference/analytics/get-stats.mdx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: 'Get stats'
|
||||||
|
openapi: GET /v1/typebots/{typebotId}/analytics/stats
|
||||||
|
---
|
@ -288,6 +288,10 @@
|
|||||||
"api-reference/results/list-logs"
|
"api-reference/results/list-logs"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"group": "Analytics",
|
||||||
|
"pages": ["api-reference/analytics/get-stats"]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"group": "Folder",
|
"group": "Folder",
|
||||||
"pages": [
|
"pages": [
|
||||||
|
@ -2900,6 +2900,131 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/v1/typebots/{typebotId}/analytics/stats": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "analytics-getStats",
|
||||||
|
"summary": "Get results stats",
|
||||||
|
"tags": [
|
||||||
|
"Analytics"
|
||||||
|
],
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Authorization": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "typebotId",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"in": "query",
|
||||||
|
"name": "timeFilter",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"today",
|
||||||
|
"last7Days",
|
||||||
|
"last30Days",
|
||||||
|
"yearToDate",
|
||||||
|
"allTime"
|
||||||
|
],
|
||||||
|
"default": "today"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"stats": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"totalViews": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"totalStarts": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"totalCompleted": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"totalViews",
|
||||||
|
"totalStarts",
|
||||||
|
"totalCompleted"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"stats"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid input data",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error.BAD_REQUEST"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Authorization not provided",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error.UNAUTHORIZED"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Insufficient access",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error.FORBIDDEN"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not found",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error.NOT_FOUND"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error.INTERNAL_SERVER_ERROR"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/v1/workspaces": {
|
"/v1/workspaces": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "workspace-listWorkspaces",
|
"operationId": "workspace-listWorkspaces",
|
||||||
@ -10851,6 +10976,21 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"in": "query",
|
||||||
|
"name": "timeFilter",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"today",
|
||||||
|
"last7Days",
|
||||||
|
"last30Days",
|
||||||
|
"yearToDate",
|
||||||
|
"allTime"
|
||||||
|
],
|
||||||
|
"default": "today"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
|
@ -24,11 +24,13 @@ export const answerInputSchema = answerSchema
|
|||||||
storageUsed: z.number().nullish(),
|
storageUsed: z.number().nullish(),
|
||||||
}) satisfies z.ZodType<Prisma.AnswerUncheckedUpdateInput>
|
}) satisfies z.ZodType<Prisma.AnswerUncheckedUpdateInput>
|
||||||
|
|
||||||
export type Stats = {
|
export const statsSchema = z.object({
|
||||||
totalViews: number
|
totalViews: z.number(),
|
||||||
totalStarts: number
|
totalStarts: z.number(),
|
||||||
totalCompleted: number
|
totalCompleted: z.number(),
|
||||||
}
|
})
|
||||||
|
|
||||||
|
export type Stats = z.infer<typeof statsSchema>
|
||||||
|
|
||||||
export type Answer = z.infer<typeof answerSchema>
|
export type Answer = z.infer<typeof answerSchema>
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user