2
0

🐛 (usage) Archive typebot to be able to compute usage

This commit is contained in:
Baptiste Arnaud
2022-10-01 08:36:49 +02:00
committed by Baptiste Arnaud
parent 75ca255af2
commit 15dbc9577d
20 changed files with 152 additions and 84 deletions

View File

@ -26,6 +26,7 @@ import { useWorkspace } from 'contexts/WorkspaceContext'
import { EmojiOrImageIcon } from 'components/shared/EmojiOrImageIcon'
import { WorkspaceSettingsModal } from './WorkspaceSettingsModal'
import { isNotDefined } from 'utils'
import { PlanTag } from 'components/shared/PlanTag'
export const DashboardHeader = () => {
const { user } = useUser()
@ -90,9 +91,13 @@ export const DashboardHeader = () => {
/>
</SkeletonCircle>
{workspace && (
<>
<Text noOfLines={1} maxW="200px">
{workspace.name}
</Text>
<PlanTag plan={workspace.plan}/>
</>
)}
<ChevronLeftIcon transform="rotate(-90deg)" />
</HStack>
@ -112,6 +117,7 @@ export const DashboardHeader = () => {
defaultIcon={HardDriveIcon}
/>
<Text>{workspace.name}</Text>
<PlanTag plan={workspace.plan}/>
</HStack>
</MenuItem>
))}

View File

@ -1,10 +1,10 @@
import { Button, HStack, useDisclosure, Text } from '@chakra-ui/react'
import { FolderPlusIcon } from 'assets/icons'
import { LockTag } from 'components/shared/LockTag'
import {
LimitReached,
ChangePlanModal,
} from 'components/shared/modals/ChangePlanModal'
import { PlanTag } from 'components/shared/PlanTag'
import { useWorkspace } from 'contexts/WorkspaceContext'
import { Plan } from 'db'
import React from 'react'
@ -28,7 +28,7 @@ export const CreateFolderButton = ({ isLoading, onClick }: Props) => {
>
<HStack>
<Text>Create a folder</Text>
{isFreePlan(workspace) && <PlanTag plan={Plan.STARTER} />}
{isFreePlan(workspace) && <LockTag plan={Plan.STARTER} />}
</HStack>
<ChangePlanModal
isOpen={isOpen}

View File

@ -46,7 +46,7 @@ export const CurrentSubscriptionContent = ({
return (
<Stack gap="2">
<Heading fontSize="3xl">Subscription</Heading>
<HStack>
<HStack data-testid="current-subscription">
<Text>Current workspace subscription: </Text>
{isCancelling ? (
<Spinner color="gray.500" size="xs" />

View File

@ -1,5 +1,5 @@
import { HStack, Text, Tooltip } from '@chakra-ui/react'
import { PlanTag } from 'components/shared/PlanTag'
import { LockTag } from 'components/shared/LockTag'
import { useWorkspace } from 'contexts/WorkspaceContext'
import { Plan } from 'db'
import {
@ -54,7 +54,7 @@ export const BlockTypeLabel = ({ type }: Props): JSX.Element => {
<Tooltip label="Upload Files">
<HStack>
<Text>File</Text>
{isFreePlan(workspace) && <PlanTag plan={Plan.STARTER} />}
{isFreePlan(workspace) && <LockTag plan={Plan.STARTER} />}
</HStack>
</Tooltip>
)

View File

@ -1,9 +1,9 @@
import { Flex, FormLabel, Stack, Switch, useDisclosure } from '@chakra-ui/react'
import { LockTag } from 'components/shared/LockTag'
import {
ChangePlanModal,
LimitReached,
} from 'components/shared/modals/ChangePlanModal'
import { PlanTag } from 'components/shared/PlanTag'
import { SwitchWithLabel } from 'components/shared/SwitchWithLabel'
import { useWorkspace } from 'contexts/WorkspaceContext'
import { Plan } from 'db'
@ -66,7 +66,7 @@ export const GeneralSettingsForm = ({
>
<FormLabel htmlFor="branding" mb="0">
Typebot.io branding{' '}
{isWorkspaceFreePlan && <PlanTag plan={Plan.STARTER} />}
{isWorkspaceFreePlan && <LockTag plan={Plan.STARTER} />}
</FormLabel>
<Switch
id="branding"

View File

@ -10,8 +10,8 @@ import {
import { TrashIcon } from 'assets/icons'
import { UpgradeButton } from 'components/shared/buttons/UpgradeButton'
import { useToast } from 'components/shared/hooks/useToast'
import { LockTag } from 'components/shared/LockTag'
import { LimitReached } from 'components/shared/modals/ChangePlanModal'
import { PlanTag } from 'components/shared/PlanTag'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
import { useWorkspace } from 'contexts/WorkspaceContext'
import { Plan } from 'db'
@ -82,19 +82,22 @@ export const ShareContent = () => {
/>
</HStack>
)}
{isWorkspaceProPlan(workspace) &&
isNotDefined(typebot?.customDomain) ? (
<CustomDomainsDropdown
onCustomDomainSelect={handleCustomDomainChange}
/>
) : (
<UpgradeButton
colorScheme="gray"
limitReachedType={LimitReached.CUSTOM_DOMAIN}
>
<Text mr="2">Add my domain</Text> <PlanTag plan={Plan.PRO} />
</UpgradeButton>
)}
{isNotDefined(typebot?.customDomain) ? (
<>
{isWorkspaceProPlan(workspace) ? (
<CustomDomainsDropdown
onCustomDomainSelect={handleCustomDomainChange}
/>
) : (
<UpgradeButton
colorScheme="gray"
limitReachedType={LimitReached.CUSTOM_DOMAIN}
>
<Text mr="2">Add my domain</Text> <LockTag plan={Plan.PRO} />
</UpgradeButton>
)}
</>
) : null}
</Stack>
<Stack spacing={4}>

View File

@ -0,0 +1,14 @@
import { Tag, TagProps } from '@chakra-ui/react'
import { LockedIcon } from 'assets/icons'
import { Plan } from 'db'
import { planColorSchemes } from './PlanTag'
export const LockTag = ({ plan, ...props }: { plan?: Plan } & TagProps) => (
<Tag
colorScheme={plan ? planColorSchemes[plan] : 'gray'}
data-testid={`${plan?.toLowerCase()}-lock-tag`}
{...props}
>
<LockedIcon />
</Tag>
)

View File

@ -1,18 +1,34 @@
import { Tag, TagProps } from '@chakra-ui/react'
import { Tag, TagProps, ThemeTypings } from '@chakra-ui/react'
import { Plan } from 'db'
export const planColorSchemes: Record<Plan, ThemeTypings['colorSchemes']> = {
[Plan.LIFETIME]: 'purple',
[Plan.PRO]: 'blue',
[Plan.OFFERED]: 'orange',
[Plan.STARTER]: 'orange',
[Plan.FREE]: 'gray',
}
export const PlanTag = ({ plan, ...props }: { plan?: Plan } & TagProps) => {
switch (plan) {
case Plan.LIFETIME: {
return (
<Tag colorScheme="purple" data-testid="lifetime-plan-tag" {...props}>
<Tag
colorScheme={planColorSchemes[plan]}
data-testid="lifetime-plan-tag"
{...props}
>
Lifetime
</Tag>
)
}
case Plan.PRO: {
return (
<Tag colorScheme="blue" data-testid="pro-plan-tag" {...props}>
<Tag
colorScheme={planColorSchemes[plan]}
data-testid="pro-plan-tag"
{...props}
>
Pro
</Tag>
)
@ -20,14 +36,22 @@ export const PlanTag = ({ plan, ...props }: { plan?: Plan } & TagProps) => {
case Plan.OFFERED:
case Plan.STARTER: {
return (
<Tag colorScheme="orange" data-testid="starter-plan-tag" {...props}>
<Tag
colorScheme={planColorSchemes[plan]}
data-testid="starter-plan-tag"
{...props}
>
Starter
</Tag>
)
}
default: {
return (
<Tag colorScheme="gray" data-testid="free-plan-tag" {...props}>
<Tag
colorScheme={planColorSchemes[Plan.FREE]}
data-testid="free-plan-tag"
{...props}
>
Free
</Tag>
)

View File

@ -139,13 +139,14 @@ export const WorkspaceContext = ({ children }: WorkspaceContextProps) => {
if (!currentWorkspace || !workspaces || workspaces.length < 2) return
const { data } = await deleteWorkspace(currentWorkspace.id)
if (!data || !currentWorkspace) return
setCurrentWorkspace(workspaces[0])
const newWorkspaces = (workspaces ?? []).filter((w) =>
w.id === currentWorkspace.id
? { ...data.workspace, members: w.members }
: w
)
setCurrentWorkspace(newWorkspaces[0])
mutate({
workspaces: (workspaces ?? []).filter((w) =>
w.id === currentWorkspace.id
? { ...data.workspace, members: w.members }
: w
),
workspaces: newWorkspaces,
})
}

View File

@ -31,6 +31,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
{
workspace: { members: { some: { userId: user.id } } },
id: { in: typebotIds },
isArchived: { not: true },
},
{
id: { in: typebotIds },
@ -39,6 +40,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
userId: user.id,
},
},
isArchived: { not: true },
},
],
},
@ -51,6 +53,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
where: {
OR: [
{
isArchived: { not: true },
folderId,
workspace: {
id: workspaceId,
@ -63,6 +66,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
},
},
{
isArchived: { not: true },
workspace: {
id: workspaceId,
members: {

View File

@ -13,13 +13,17 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const typebotId = req.query.typebotId as string
if (req.method === 'GET') {
const typebot = await prisma.typebot.findFirst({
where: canReadTypebot(typebotId, user),
where: {
...canReadTypebot(typebotId, user),
isArchived: { not: true },
},
include: {
publishedTypebot: true,
collaborators: { select: { userId: true, type: true } },
webhooks: true,
},
})
console.log(typebot)
if (!typebot) return res.send({ typebot: null })
const { publishedTypebot, collaborators, webhooks, ...restOfTypebot } =
typebot
@ -35,8 +39,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
}
if (req.method === 'DELETE') {
const typebots = await prisma.typebot.deleteMany({
const typebots = await prisma.typebot.updateMany({
where: canWriteTypebot(typebotId, user),
data: { isArchived: true },
})
await archiveResults(res)({
typebotId,

View File

@ -2,7 +2,7 @@ import { withSentry } from '@sentry/nextjs'
import { Prisma, Workspace, WorkspaceRole } from 'db'
import prisma from 'libs/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { archiveResults, getAuthenticatedUser } from 'services/api/utils'
import { getAuthenticatedUser } from 'services/api/utils'
import { methodNotAllowed, notAuthenticated } from 'utils/api'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
@ -28,23 +28,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
id,
members: { some: { userId: user.id, role: WorkspaceRole.ADMIN } },
}
const deletedTypebots = await prisma.typebot.findMany({
where: {
workspace: workspaceFilter,
},
})
await prisma.workspace.deleteMany({
where: workspaceFilter,
})
await Promise.all(
deletedTypebots.map((typebot) =>
archiveResults(res)({
typebotId: typebot.id,
user,
resultsFilter: { typebotId: typebot.id },
})
)
)
return res.status(200).json({
message: 'success',
})

View File

@ -102,14 +102,16 @@ export const setupDatabase = async () => {
return setupCredentials()
}
export const setupWorkspaces = async () =>
prisma.workspace.createMany({
export const setupWorkspaces = async () => {
await prisma.workspace.create({
data: {
id: freeWorkspaceId,
name: 'Free workspace',
plan: Plan.FREE,
},
})
await prisma.workspace.createMany({
data: [
{
id: freeWorkspaceId,
name: 'Free workspace',
plan: Plan.FREE,
},
{
id: starterWorkspaceId,
name: 'Starter workspace',
@ -128,6 +130,7 @@ export const setupWorkspaces = async () =>
},
],
})
}
export const createWorkspaces = async (workspaces: Partial<Workspace>[]) => {
const workspaceIds = workspaces.map((workspace) => workspace.id ?? cuid())
@ -231,11 +234,15 @@ export const getSignedInUser = (email: string) =>
prisma.user.findFirst({ where: { email } })
export const createTypebots = async (partialTypebots: Partial<Typebot>[]) => {
const typebotsWithId = partialTypebots.map((typebot) => ({
...typebot,
id: typebot.id ?? cuid(),
}))
await prisma.typebot.createMany({
data: partialTypebots.map(parseTestTypebot),
data: typebotsWithId.map(parseTestTypebot),
})
return prisma.publicTypebot.createMany({
data: partialTypebots.map((t) =>
data: typebotsWithId.map((t) =>
parseTypebotToPublicTypebot(t.id + '-public', parseTestTypebot(t))
),
})
@ -304,7 +311,7 @@ const parseTypebotToPublicTypebot = (
})
const parseTestTypebot = (partialTypebot: Partial<Typebot>): Typebot => ({
id: partialTypebot.id ?? 'typebot',
id: cuid(),
workspaceId: proWorkspaceId,
folderId: null,
name: 'My typebot',

View File

@ -165,9 +165,13 @@ test('plan changes should work', async ({ page }) => {
await page.goto('/typebots')
await page.click('text=Settings & Members')
await page.click('text=Billing & Usage')
await expect(page.locator('[data-testid="pro-plan-tag"]')).toBeVisible()
await expect(page.locator('[data-testid="current-subscription"]')).toHaveText(
'Current workspace subscription: ProCancel my subscription'
)
await page.click('button >> text="Cancel my subscription"')
await expect(page.locator('[data-testid="free-plan-tag"]')).toBeVisible()
await expect(page.locator('[data-testid="current-subscription"]')).toHaveText(
'Current workspace subscription: Free'
)
})
test('should display invoices', async ({ page }) => {

View File

@ -57,7 +57,7 @@ test.describe('Starter workspace', () => {
},
])
await page.goto(`/typebots/${typebotId}/share`)
await expect(page.locator('text=Pro')).toBeVisible()
await expect(page.locator('[data-testid="pro-lock-tag"]')).toBeVisible()
await page.click('text=Add my domain')
await expect(
page.locator(

View File

@ -75,7 +75,7 @@ test.describe('Free user', () => {
await page.goto('/typebots')
await page.click('text="Pro workspace"')
await page.click('text="Free workspace"')
await expect(page.locator('[data-testid="starter-plan-tag"]')).toBeVisible()
await expect(page.locator('[data-testid="starter-lock-tag"]')).toBeVisible()
await page.click('text=Create a folder')
await expect(
page.locator(

View File

@ -135,7 +135,9 @@ test.describe.parallel('Settings page', () => {
typebotViewer(page).locator('text="What\'s your name?"')
).toBeVisible()
await page.click('button:has-text("General")')
await expect(page.locator('text=Starter')).toBeVisible()
await expect(
page.locator('[data-testid="starter-lock-tag"]')
).toBeVisible()
await page.click('text=Typebot.io branding')
await expect(
page.locator(

View File

@ -144,7 +144,7 @@ test('can manage members', async ({ page }) => {
test("can't add new members when limit is reached", async ({ page }) => {
await page.goto('/typebots')
await page.click('text="Pro workspace"')
await page.click('text="My awesome workspace"')
await page.click('text="Free workspace"')
await page.click('text=Settings & Members')
await page.click('text="Members"')