Merge branch 'main' into iain/web-2142-multi-region-db-connection-health

This commit is contained in:
Iain Sproat 2024-11-20 15:01:21 +00:00
Родитель e1f7498cac 738275d842
Коммит 1ac1fe013f
Не найден ключ, соответствующий данной подписи
18 изменённых файлов: 437 добавлений и 265 удалений

Просмотреть файл

@ -10,14 +10,22 @@ const getDbClients = require('../knex')
async function main() {
const cmdArgs = process.argv.slice(2)
const [filePath, userId, streamId, branchName, commitMessage, fileId, regionName] =
cmdArgs
const [
filePath,
userId,
streamId,
branchName,
commitMessage,
fileId,
branchId,
regionName
] = cmdArgs
const logger = Observability.extendLoggerComponent(
parentLogger.child({ streamId, branchName, userId, fileId, filePath }),
'ifc'
)
logger.info('ARGV: ', filePath, userId, streamId, branchName, commitMessage)
logger.info('ARGV: ', filePath, userId, streamId, branchName, branchId, commitMessage)
const data = fs.readFileSync(filePath)
@ -27,6 +35,7 @@ async function main() {
userId,
message: commitMessage || ' Imported file',
fileId,
branchId,
logger
}
if (branchName) ifcInput.branchName = branchName

Просмотреть файл

@ -14,6 +14,7 @@ const parseAndCreateCommitFactory =
userId,
message = 'Manual IFC file upload',
fileId,
branchId,
logger
}) => {
if (!logger) {
@ -45,12 +46,7 @@ const parseAndCreateCommitFactory =
totalChildrenCount: tCount
}
const branch = await serverApi.getBranchByNameAndStreamId({
streamId,
name: branchName
})
if (!branch) {
if (!branchId) {
logger.info("Branch '{branchName}' not found, creating it.")
await serverApi.createBranch({
name: branchName,

Просмотреть файл

@ -81,7 +81,8 @@ async function doTask(mainDb, regionName, taskDb, task) {
fileSize: fileSizeForMetric,
userId: info.userId,
streamId: info.streamId,
branchName: info.branchName
branchName: info.branchName,
branchId: info.branchId
})
fs.mkdirSync(TMP_INPUT_DIR, { recursive: true })
@ -136,6 +137,7 @@ async function doTask(mainDb, regionName, taskDb, task) {
info.branchName,
`File upload: ${info.fileName}`,
info.id,
existingBranch?.id,
regionName
],
{
@ -153,7 +155,10 @@ async function doTask(mainDb, regionName, taskDb, task) {
info.userId,
info.streamId,
info.branchName,
`File upload: ${info.fileName}`
`File upload: ${info.fileName}`,
info.id,
existingBranch?.id,
regionName
],
{
USER_TOKEN: tempUserToken
@ -178,7 +183,10 @@ async function doTask(mainDb, regionName, taskDb, task) {
info.userId,
info.streamId,
info.branchName,
`File upload: ${info.fileName}`
`File upload: ${info.fileName}`,
info.id,
existingBranch?.id,
regionName
],
{
USER_TOKEN: tempUserToken

Просмотреть файл

@ -190,9 +190,6 @@ const toggleSettingsDialog = (target: AvailableSettingsMenuKeys) => {
const deleteSettingsQuery = (): void => {
const currentQueryParams = { ...route.query }
delete currentQueryParams.settings
delete currentQueryParams.workspace
delete currentQueryParams.error
router.push({ query: currentQueryParams })
}
@ -202,8 +199,6 @@ const openFeedbackDialog = () => {
onMounted(() => {
const settingsQuery = route.query?.settings
const workspaceQuery = route.query?.workspace
const errorQuery = route.query?.error
if (settingsQuery && isString(settingsQuery)) {
if (settingsQuery.includes('server') && !isAdmin.value) {
@ -215,22 +210,6 @@ onMounted(() => {
return
}
if (workspaceQuery && isString(workspaceQuery)) {
workspaceSettingsDialogTarget.value = workspaceQuery
if (errorQuery && isString(errorQuery)) {
triggerNotification({
type: ToastNotificationType.Danger,
title: errorQuery
})
} else {
triggerNotification({
type: ToastNotificationType.Success,
title: 'SSO settings successfully updated'
})
}
}
showSettingsDialog.value = true
settingsDialogTarget.value = settingsQuery
deleteSettingsQuery()

Просмотреть файл

@ -1,14 +1,20 @@
<template>
<section>
<div class="md:mx-auto pb-6 md:pb-0">
<div class="md:max-w-5xl md:mx-auto pb-6 md:pb-0">
<SettingsSectionHeader title="Billing" text="Your workspace billing details" />
<template v-if="isBillingIntegrationEnabled">
<div class="flex flex-col gap-y-4 md:gap-y-6">
<BillingAlert v-if="workspaceResult" :workspace="workspaceResult.workspace" />
<BillingAlert
v-if="
workspaceResult &&
workspaceResult.workspace?.plan?.status !== WorkspacePlanStatuses.Valid
"
:workspace="workspaceResult.workspace"
/>
<SettingsSectionHeader title="Billing summary" subheading class="pt-4" />
<div class="border border-outline-3 rounded-lg">
<div
class="grid grid-cols-1 md:grid-cols-3 divide-y md:divide-y-0 md:divide-x"
class="grid grid-cols-1 md:grid-cols-3 divide-y divide-outline-3 md:divide-y-0 md:divide-x"
>
<div class="p-5 pt-4 flex flex-col gap-y-1">
<h3 class="text-body-xs text-foreground-2 pb-2">
@ -17,7 +23,7 @@
<p class="text-heading-lg text-foreground capitalize">
{{ currentPlan?.name ?? WorkspacePlans.Team }} plan
</p>
<p class="text-body-xs text-foreground-2">
<p v-if="isPurchasablePlan" class="text-body-xs text-foreground-2">
£{{ seatPrice }} per seat/month, billed
{{
subscription?.billingInterval === BillingInterval.Yearly
@ -36,16 +42,22 @@
: 'Monthly bill'
}}
</h3>
<p class="text-heading-lg text-foreground capitalize">Coming soon</p>
<p class="text-heading-lg text-foreground capitalize">
{{ isPurchasablePlan ? 'Coming soon' : 'Not applicable' }}
</p>
</div>
<div class="p-5 pt-4 flex flex-col gap-y-1">
<h3 class="text-body-xs text-foreground-2 pb-2">
{{ isTrialPeriod ? 'First payment due' : 'Next payment due' }}
{{
isTrialPeriod && isPurchasablePlan
? 'First payment due'
: 'Next payment due'
}}
</h3>
<p class="text-heading-lg text-foreground capitalize">
{{ nextPaymentDue }}
{{ isPurchasablePlan ? nextPaymentDue : 'Not applicable' }}
</p>
<p v-if="isPaidPlan" class="text-body-xs text-foreground-2">
<p v-if="isPurchasablePlan" class="text-body-xs text-foreground-2">
<span class="capitalize">
{{
subscription?.billingInterval === BillingInterval.Yearly
@ -58,7 +70,7 @@
</div>
</div>
<div
v-if="isActivePlan"
v-if="isActivePlan && isPurchasablePlan"
class="flex flex-row gap-x-4 p-5 items-center border-t border-outline-3"
>
<div class="text-body-xs gap-y-2 flex-1">
@ -77,6 +89,7 @@
class="pt-6"
:workspace-id="workspaceId"
:current-plan="currentPlan"
:is-admin="isAdmin"
/>
</div>
</template>
@ -99,10 +112,12 @@ import {
import { useBillingActions } from '~/lib/billing/composables/actions'
import { pricingPlansConfig } from '~/lib/billing/helpers/constants'
import { Roles } from '@speckle/shared'
graphql(`
fragment SettingsWorkspacesBilling_Workspace on Workspace {
...BillingAlert_Workspace
id
role
plan {
...SettingsWorkspacesBillingPricingTable_WorkspacePlan
name
@ -120,13 +135,6 @@ const props = defineProps<{
}>()
const isBillingIntegrationEnabled = useIsBillingIntegrationEnabled()
const seatPrices = ref({
[WorkspacePlans.Team]: pricingPlansConfig.plans[WorkspacePlans.Team].cost,
[WorkspacePlans.Pro]: pricingPlansConfig.plans[WorkspacePlans.Pro].cost,
[WorkspacePlans.Business]: pricingPlansConfig.plans[WorkspacePlans.Business].cost
})
const route = useRoute()
const { result: workspaceResult } = useQuery(
settingsWorkspaceBillingQuery,
() => ({
@ -136,15 +144,16 @@ const { result: workspaceResult } = useQuery(
enabled: isBillingIntegrationEnabled
})
)
const { billingPortalRedirect, cancelCheckoutSession } = useBillingActions()
const { billingPortalRedirect } = useBillingActions()
const seatPrices = ref({
[WorkspacePlans.Team]: pricingPlansConfig.plans[WorkspacePlans.Team].cost,
[WorkspacePlans.Pro]: pricingPlansConfig.plans[WorkspacePlans.Pro].cost,
[WorkspacePlans.Business]: pricingPlansConfig.plans[WorkspacePlans.Business].cost
})
const currentPlan = computed(() => workspaceResult.value?.workspace.plan)
const subscription = computed(() => workspaceResult.value?.workspace.subscription)
const isPaidPlan = computed(
() =>
currentPlan.value?.name !== WorkspacePlans.Academia &&
currentPlan.value?.name !== WorkspacePlans.Unlimited
)
const isTrialPeriod = computed(
() =>
currentPlan.value?.status === WorkspacePlanStatuses.Trial ||
@ -156,6 +165,13 @@ const isActivePlan = computed(
currentPlan.value?.status !== WorkspacePlanStatuses.Trial &&
currentPlan.value?.status !== WorkspacePlanStatuses.Canceled
)
const isPurchasablePlan = computed(
() =>
currentPlan.value?.name === WorkspacePlans.Team ||
currentPlan.value?.name === WorkspacePlans.Pro ||
currentPlan.value?.name === WorkspacePlans.Business ||
!currentPlan.value?.name // no plan equals pro trial plan
)
const seatPrice = computed(() =>
currentPlan.value && subscription.value
? seatPrices.value[currentPlan.value.name as keyof typeof seatPrices.value][
@ -167,18 +183,12 @@ const seatPrice = computed(() =>
)
const nextPaymentDue = computed(() =>
currentPlan.value
? isPaidPlan.value
? isPurchasablePlan.value
? dayjs(subscription.value?.currentBillingCycleEnd).format('MMMM D, YYYY')
: 'Never'
: dayjs().add(30, 'days').format('MMMM D, YYYY')
)
onMounted(() => {
const paymentStatusQuery = route.query?.payment_status
const sessionIdQuery = route.query?.session_id
if (sessionIdQuery && String(paymentStatusQuery) === WorkspacePlanStatuses.Canceled) {
cancelCheckoutSession(String(sessionIdQuery), props.workspaceId)
}
})
const isAdmin = computed(
() => workspaceResult.value?.workspace.role === Roles.Workspace.Admin
)
</script>

Просмотреть файл

@ -1,156 +0,0 @@
<template>
<div class="flex flex-col gap-y-6">
<div class="flex justify-between">
<SettingsSectionHeader
:title="hasTrialPlan ? 'Start your subscription' : 'Upgrade your plan'"
subheading
/>
<div class="flex items-center gap-x-4">
<p class="text-foreground-3 text-body-xs">Save 20% with annual billing</p>
<FormSwitch v-model="isYearlyPlan" :show-label="false" name="annual billing" />
</div>
</div>
<table class="w-full flex flex-col">
<thead>
<tr class="w-full flex">
<th class="w-1/4 flex pl-5 pr-6 pt-4 pb-2 font-medium">
<h4>Compare plans</h4>
</th>
<th
v-for="plan in pricingPlans"
:key="plan.name"
class="w-1/4 flex flex-col gap-y-1 px-6 pt-4 pb-2 font-normal"
:class="[
plan.name === WorkspacePlans.Team
? 'border border-b-0 border-outline-3 bg-foundation-2 rounded-t-lg'
: ''
]"
scope="col"
>
<h4 class="text-foreground text-body-xs">
Workspace
<span class="capitalize">{{ plan.name }}</span>
</h4>
<p class="text-foreground text-heading font-normal">
£{{
isYearlyPlan
? plan.cost.yearly[Roles.Workspace.Member]
: plan.cost.monthly[Roles.Workspace.Member]
}}
per seat/month
</p>
<p class="text-foreground-2 text-body-2xs pt-1">
Billed {{ isYearlyPlan ? 'annually' : 'monthly' }}
</p>
<FormButton
:color="plan.name === WorkspacePlans.Team ? 'primary' : 'outline'"
:disabled="!hasTrialPlan && !canUpgradeToPlan(plan.name)"
class="mt-3"
@click="onUpgradePlanClick(plan.name)"
>
{{ hasTrialPlan ? 'Subscribe' : 'Upgrade' }} to&nbsp;
<span class="capitalize">{{ plan.name }}</span>
</FormButton>
</th>
</tr>
</thead>
<tbody class="w-full flex flex-col">
<tr v-for="(feature, key, index) in features" :key="key" class="flex">
<th
class="font-normal text-foreground text-body-xs w-1/4 pr-3 pt-1"
scope="row"
>
<div class="border-b border-outline-3 min-h-[42px] pl-5 flex items-center">
{{ feature.name }}
</div>
</th>
<td
v-for="plan in pricingPlans"
:key="plan.name"
class="px-3 w-1/4 pt-1"
:class="[
plan.name === WorkspacePlans.Team
? 'border-l border-r border-outline-3 bg-foundation-2'
: '',
plan.name === WorkspacePlans.Team &&
index === Object.values(features).length - 1
? 'pb-6 border-b rounded-b-lg'
: ''
]"
>
<div class="border-b border-outline-3 flex items-center px-3 min-h-[42px]">
<CheckIcon
v-if="plan.features.includes(feature.name)"
class="w-3 h-3 text-foreground"
/>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script setup lang="ts">
import type { SettingsWorkspacesBillingPricingTable_WorkspacePlanFragment } from '~/lib/common/generated/gql/graphql'
import {
WorkspacePlans,
BillingInterval,
WorkspacePlanStatuses
} from '~/lib/common/generated/gql/graphql'
import { pricingPlansConfig } from '~/lib/billing/helpers/constants'
import { CheckIcon } from '@heroicons/vue/24/outline'
import { useBillingActions } from '~/lib/billing/composables/actions'
import { graphql } from '~/lib/common/generated/gql'
import type { MaybeNullOrUndefined } from '@speckle/shared'
import { Roles } from '@speckle/shared'
graphql(`
fragment SettingsWorkspacesBillingPricingTable_WorkspacePlan on WorkspacePlan {
name
status
}
`)
const props = defineProps<{
workspaceId: string
currentPlan: MaybeNullOrUndefined<SettingsWorkspacesBillingPricingTable_WorkspacePlanFragment>
}>()
const { upgradePlanRedirect } = useBillingActions()
const pricingPlans = ref([
pricingPlansConfig.plans[WorkspacePlans.Team],
pricingPlansConfig.plans[WorkspacePlans.Pro],
pricingPlansConfig.plans[WorkspacePlans.Business]
])
const features = ref(pricingPlansConfig.features)
const isYearlyPlan = ref(false)
const onUpgradePlanClick = (plan: WorkspacePlans) => {
upgradePlanRedirect({
plan,
cycle: isYearlyPlan.value ? BillingInterval.Yearly : BillingInterval.Monthly,
workspaceId: props.workspaceId
})
}
const hasTrialPlan = computed(
() => props.currentPlan?.status === WorkspacePlanStatuses.Trial || !props.currentPlan
)
const canUpgradeToPlan = (plan: WorkspacePlans) => {
if (!props.currentPlan?.name) return false
const allowedUpgrades: Record<WorkspacePlans, WorkspacePlans[]> = {
[WorkspacePlans.Team]: [WorkspacePlans.Pro, WorkspacePlans.Business],
[WorkspacePlans.Pro]: [WorkspacePlans.Business],
[WorkspacePlans.Business]: [],
[WorkspacePlans.Academia]: [],
[WorkspacePlans.Unlimited]: []
}
return allowedUpgrades[props.currentPlan.name]?.includes(plan) ?? false
}
</script>

Просмотреть файл

@ -0,0 +1,82 @@
<template>
<table class="w-full flex flex-col">
<thead>
<tr class="w-full flex">
<th class="w-1/4 flex pl-5 pr-6 pt-4 pb-2 font-medium">
<h4>Compare plans</h4>
</th>
<th
v-for="plan in plans"
:key="`desktop-${plan.name}`"
class="w-1/4 px-6 pt-4 pb-2"
:class="[
plan.name === WorkspacePlans.Team
? 'border border-b-0 border-outline-3 bg-foundation-2 rounded-t-lg'
: ''
]"
scope="col"
>
<SettingsWorkspacesBillingPricingTableHeader
:plan="plan"
:is-yearly-plan="isYearlyPlan"
:current-plan="currentPlan"
:workspace-id="workspaceId"
:is-admin="isAdmin"
/>
</th>
</tr>
</thead>
<tbody class="w-full flex flex-col">
<tr v-for="(feature, key, index) in features" :key="key" class="flex">
<th
class="font-normal text-foreground text-body-xs w-1/4 pr-3 pt-1"
scope="row"
>
<div class="border-b border-outline-3 min-h-[42px] pl-5 flex items-center">
{{ feature.name }}
</div>
</th>
<td
v-for="plan in plans"
:key="plan.name"
class="px-3 w-1/4 pt-1"
:class="[
plan.name === WorkspacePlans.Team
? 'border-l border-r border-outline-3 bg-foundation-2'
: '',
plan.name === WorkspacePlans.Team &&
index === Object.values(features).length - 1
? 'pb-6 border-b rounded-b-lg'
: ''
]"
>
<div class="border-b border-outline-3 flex items-center px-3 min-h-[42px]">
<CheckIcon
v-if="plan.features.includes(feature.name as PlanFeaturesList)"
class="w-3 h-3 text-foreground"
/>
</div>
</td>
</tr>
</tbody>
</table>
</template>
<script setup lang="ts">
import type { WorkspacePlan } from '~/lib/common/generated/gql/graphql'
import { WorkspacePlans } from '~/lib/common/generated/gql/graphql'
import { pricingPlansConfig } from '~/lib/billing/helpers/constants'
import type { PlanFeaturesList } from '~/lib/billing/helpers/types'
import { CheckIcon } from '@heroicons/vue/24/outline'
import type { MaybeNullOrUndefined } from '@speckle/shared'
defineProps<{
isYearlyPlan: boolean
currentPlan: MaybeNullOrUndefined<WorkspacePlan>
workspaceId: string
isAdmin: boolean
}>()
const plans = ref(pricingPlansConfig.plans)
const features = ref(pricingPlansConfig.features)
</script>

Просмотреть файл

@ -0,0 +1,79 @@
<template>
<div class="flex flex-col gap-y-1 font-normal">
<h4 class="text-foreground text-body-xs">
Workspace
<span class="capitalize">{{ plan.name }}</span>
</h4>
<p class="text-foreground text-heading">
£{{
isYearlyPlan
? plan.cost.yearly[Roles.Workspace.Member]
: plan.cost.monthly[Roles.Workspace.Member]
}}
per seat/month
</p>
<p class="text-foreground-2 text-body-2xs pt-1">
Billed {{ isYearlyPlan ? 'annually' : 'monthly' }}
</p>
<div class="w-full">
<FormButton
:color="plan.name === WorkspacePlans.Team ? 'primary' : 'outline'"
:disabled="(!hasTrialPlan && !canUpgradeToPlan) || !isAdmin"
class="mt-3"
full-width
@click="onUpgradePlanClick(plan.name)"
>
{{ hasTrialPlan ? 'Subscribe' : 'Upgrade' }} to&nbsp;
<span class="capitalize">{{ plan.name }}</span>
</FormButton>
</div>
</div>
</template>
<script setup lang="ts">
import { type PricingPlan } from '@/lib/billing/helpers/types'
import { Roles } from '@speckle/shared'
import {
type WorkspacePlan,
WorkspacePlanStatuses,
WorkspacePlans,
BillingInterval
} from '~/lib/common/generated/gql/graphql'
import { useBillingActions } from '~/lib/billing/composables/actions'
import type { MaybeNullOrUndefined } from '@speckle/shared'
const props = defineProps<{
plan: PricingPlan
isYearlyPlan: boolean
currentPlan: MaybeNullOrUndefined<WorkspacePlan>
workspaceId: string
isAdmin: boolean
}>()
const { upgradePlanRedirect } = useBillingActions()
const canUpgradeToPlan = computed(() => {
if (!props.currentPlan) return false
const allowedUpgrades: Record<WorkspacePlans, WorkspacePlans[]> = {
[WorkspacePlans.Team]: [WorkspacePlans.Pro, WorkspacePlans.Business],
[WorkspacePlans.Pro]: [WorkspacePlans.Business],
[WorkspacePlans.Business]: [],
[WorkspacePlans.Academia]: [],
[WorkspacePlans.Unlimited]: []
}
return allowedUpgrades[props.currentPlan.name].includes(props.plan.name)
})
const hasTrialPlan = computed(
() => props.currentPlan?.status === WorkspacePlanStatuses.Trial || !props.currentPlan
)
const onUpgradePlanClick = (plan: WorkspacePlans) => {
upgradePlanRedirect({
plan,
cycle: props.isYearlyPlan ? BillingInterval.Yearly : BillingInterval.Monthly,
workspaceId: props.workspaceId
})
}
</script>

Просмотреть файл

@ -0,0 +1,48 @@
<template>
<div class="flex flex-col gap-y-4">
<div
v-for="plan in plans"
:key="`mobile-${plan.name}`"
class="border border-outline-3 bg-foundation rounded-lg p-4 pb-2"
>
<SettingsWorkspacesBillingPricingTableHeader
:plan="plan"
:is-yearly-plan="isYearlyPlan"
:current-plan="currentPlan"
:workspace-id="workspaceId"
:is-admin="isAdmin"
/>
<ul class="flex flex-col gap-y-2 mt-6">
<li
v-for="feature in features"
:key="feature.name"
class="flex items-center justify-between border-b last:border-b-0 border-outline-3 pb-2"
>
{{ feature.name }}
<CheckIcon
v-if="plan.features.includes(feature.name as PlanFeaturesList)"
class="w-3 h-3 text-foreground"
/>
</li>
</ul>
</div>
</div>
</template>
<script setup lang="ts">
import type { WorkspacePlan } from '~/lib/common/generated/gql/graphql'
import { pricingPlansConfig } from '~/lib/billing/helpers/constants'
import type { PlanFeaturesList } from '~/lib/billing/helpers/types'
import { CheckIcon } from '@heroicons/vue/24/outline'
import type { MaybeNullOrUndefined } from '@speckle/shared'
defineProps<{
isYearlyPlan: boolean
currentPlan: MaybeNullOrUndefined<WorkspacePlan>
workspaceId: string
isAdmin: boolean
}>()
const plans = ref(pricingPlansConfig.plans)
const features = ref(pricingPlansConfig.features)
</script>

Просмотреть файл

@ -0,0 +1,60 @@
<template>
<div class="flex flex-col gap-y-6">
<div class="flex flex-col lg:flex-row justify-between gap-y-4">
<SettingsSectionHeader
:title="hasTrialPlan ? 'Start your subscription' : 'Upgrade your plan'"
subheading
/>
<div class="flex items-center gap-x-4">
<p class="text-foreground-3 text-body-xs">Save 20% with annual billing</p>
<FormSwitch v-model="isYearlyPlan" :show-label="false" name="annual billing" />
</div>
</div>
<component
:is="isDesktop ? DesktopTable : MobileTable"
:workspace-id="workspaceId"
:current-plan="currentPlan"
:is-yearly-plan="isYearlyPlan"
:is-admin="isAdmin"
/>
</div>
</template>
<script setup lang="ts">
import { useBreakpoints } from '@vueuse/core'
import { TailwindBreakpoints } from '~~/lib/common/helpers/tailwind'
import {
WorkspacePlanStatuses,
type WorkspacePlan
} from '~/lib/common/generated/gql/graphql'
import { graphql } from '~/lib/common/generated/gql'
import type { MaybeNullOrUndefined } from '@speckle/shared'
graphql(`
fragment SettingsWorkspacesBillingPricingTable_WorkspacePlan on WorkspacePlan {
name
status
}
`)
const props = defineProps<{
workspaceId: string
currentPlan: MaybeNullOrUndefined<WorkspacePlan>
isAdmin: boolean
}>()
const breakpoints = useBreakpoints(TailwindBreakpoints)
const DesktopTable = defineAsyncComponent(
() => import('@/components/settings/workspaces/billing/PricingTable/Desktop.vue')
)
const MobileTable = defineAsyncComponent(
() => import('@/components/settings/workspaces/billing/PricingTable/Mobile.vue')
)
const isDesktop = breakpoints.greaterOrEqual('lg')
const isYearlyPlan = ref(false)
const hasTrialPlan = computed(
() => props.currentPlan?.status === WorkspacePlanStatuses.Trial || !props.currentPlan
)
</script>

Просмотреть файл

@ -127,7 +127,7 @@ import {
SettingMenuKeys,
type AvailableSettingsMenuKeys
} from '~/lib/settings/helpers/types'
import { useBillingActions } from '~/lib/billing/composables/actions'
graphql(`
fragment WorkspaceProjectList_ProjectCollection on ProjectCollection {
totalCount
@ -138,6 +138,7 @@ graphql(`
}
`)
const { validateCheckoutSession } = useBillingActions()
const { workspaceMixpanelUpdateGroup } = useWorkspacesMixpanel()
const areQueriesLoading = useQueryLoading()
const route = useRoute()
@ -280,6 +281,7 @@ onResult((queryResult) => {
useHeadSafe({
title: queryResult.data.workspaceBySlug.name
})
validateCheckoutSession(queryResult.data.workspaceBySlug.id)
}
})
</script>

Просмотреть файл

@ -5,10 +5,15 @@ import type {
BillingInterval
} from '~/lib/common/generated/gql/graphql'
import { settingsBillingCancelCheckoutSessionMutation } from '~/lib/settings/graphql/mutations'
import { WorkspacePlanStatuses } from '~/lib/common/generated/gql/graphql'
import { ToastNotificationType, useGlobalToast } from '~~/lib/common/composables/toast'
import { useMixpanel } from '~/lib/core/composables/mp'
export const useBillingActions = () => {
const mixpanel = useMixpanel()
const route = useRoute()
const router = useRouter()
const { triggerNotification } = useGlobalToast()
const { client: apollo } = useApolloClient()
const { mutate: cancelCheckoutSessionMutation } = useMutation(
settingsBillingCancelCheckoutSessionMutation
@ -58,9 +63,35 @@ export const useBillingActions = () => {
})
}
const validateCheckoutSession = (workspaceId: string) => {
const sessionIdQuery = route.query?.session_id
const paymentStatusQuery = route.query?.payment_status
if (sessionIdQuery && paymentStatusQuery) {
if (paymentStatusQuery === WorkspacePlanStatuses.Canceled) {
cancelCheckoutSession(String(sessionIdQuery), workspaceId)
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Your payment was canceled'
})
} else {
triggerNotification({
type: ToastNotificationType.Success,
title: 'Your workspace plan was successfully updated'
})
}
const currentQueryParams = { ...route.query }
delete currentQueryParams.session_id
delete currentQueryParams.payment_status
router.push({ query: currentQueryParams })
}
}
return {
billingPortalRedirect,
upgradePlanRedirect,
cancelCheckoutSession
cancelCheckoutSession,
validateCheckoutSession
}
}

Просмотреть файл

@ -1,16 +1,6 @@
import { WorkspacePlans, BillingInterval } from '~/lib/common/generated/gql/graphql'
import { Roles } from '@speckle/shared'
enum PlanFeaturesList {
Workspaces = 'Workspaces',
RoleManagement = 'Role management',
GuestUsers = 'Guest users',
PrivateAutomateFunctions = 'Private automate functions',
DomainSecurity = 'Domain security',
SSO = 'Single Sign-On (SSO)',
CustomerDataRegion = 'Customer data region',
PrioritySupport = 'Priority support'
}
import { PlanFeaturesList, type PricingPlan } from '@/lib/billing/helpers/types'
const baseFeatures = [
PlanFeaturesList.Workspaces,
@ -20,7 +10,13 @@ const baseFeatures = [
PlanFeaturesList.DomainSecurity
]
export const pricingPlansConfig = {
export const pricingPlansConfig: {
features: Record<PlanFeaturesList, { name: string; description: string }>
plans: Record<
WorkspacePlans.Team | WorkspacePlans.Pro | WorkspacePlans.Business,
PricingPlan
>
} = {
features: {
[PlanFeaturesList.Workspaces]: {
name: PlanFeaturesList.Workspaces,
@ -46,8 +42,8 @@ export const pricingPlansConfig = {
name: PlanFeaturesList.SSO,
description: ''
},
[PlanFeaturesList.CustomerDataRegion]: {
name: PlanFeaturesList.CustomerDataRegion,
[PlanFeaturesList.CustomDataRegion]: {
name: PlanFeaturesList.CustomDataRegion,
description: ''
},
[PlanFeaturesList.PrioritySupport]: {
@ -93,7 +89,7 @@ export const pricingPlansConfig = {
features: [
...baseFeatures,
PlanFeaturesList.SSO,
PlanFeaturesList.CustomerDataRegion,
PlanFeaturesList.CustomDataRegion,
PlanFeaturesList.PrioritySupport
],
cost: {

Просмотреть файл

@ -0,0 +1,24 @@
import type {
BillingInterval,
WorkspacePlans
} from '~/lib/common/generated/gql/graphql'
import type { WorkspaceRoles } from '@speckle/shared'
export enum PlanFeaturesList {
Workspaces = 'Workspaces',
RoleManagement = 'Role management',
GuestUsers = 'Guest users',
PrivateAutomateFunctions = 'Private automate functions',
DomainSecurity = 'Domain security',
SSO = 'Single Sign-On (SSO)',
CustomDataRegion = 'Custom data region',
PrioritySupport = 'Priority support'
}
export type PricingPlan = {
name: WorkspacePlans
features: PlanFeaturesList[]
cost: {
[I in BillingInterval]: Record<WorkspaceRoles, number>
}
}

Просмотреть файл

@ -114,7 +114,7 @@ const documents = {
"\n fragment SettingsUserProfileDeleteAccount_User on User {\n id\n email\n }\n": types.SettingsUserProfileDeleteAccount_UserFragmentDoc,
"\n fragment SettingsUserProfileDetails_User on User {\n id\n name\n company\n ...UserProfileEditDialogAvatar_User\n }\n": types.SettingsUserProfileDetails_UserFragmentDoc,
"\n fragment UserProfileEditDialogAvatar_User on User {\n id\n avatar\n ...ActiveUserAvatar\n }\n": types.UserProfileEditDialogAvatar_UserFragmentDoc,
"\n fragment SettingsWorkspacesBilling_Workspace on Workspace {\n ...BillingAlert_Workspace\n id\n plan {\n ...SettingsWorkspacesBillingPricingTable_WorkspacePlan\n name\n status\n }\n subscription {\n billingInterval\n currentBillingCycleEnd\n }\n }\n": types.SettingsWorkspacesBilling_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesBilling_Workspace on Workspace {\n ...BillingAlert_Workspace\n id\n role\n plan {\n ...SettingsWorkspacesBillingPricingTable_WorkspacePlan\n name\n status\n }\n subscription {\n billingInterval\n currentBillingCycleEnd\n }\n }\n": types.SettingsWorkspacesBilling_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesGeneral_Workspace on Workspace {\n ...SettingsWorkspacesGeneralEditAvatar_Workspace\n ...SettingsWorkspaceGeneralDeleteDialog_Workspace\n ...SettingsWorkspacesGeneralEditSlugDialog_Workspace\n id\n name\n slug\n description\n logo\n role\n defaultProjectRole\n }\n": types.SettingsWorkspacesGeneral_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspaceGeneralDeleteDialog_Workspace on Workspace {\n id\n name\n }\n": types.SettingsWorkspaceGeneralDeleteDialog_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesGeneralEditAvatar_Workspace on Workspace {\n id\n logo\n name\n defaultLogoIndex\n }\n": types.SettingsWorkspacesGeneralEditAvatar_WorkspaceFragmentDoc,
@ -770,7 +770,7 @@ export function graphql(source: "\n fragment UserProfileEditDialogAvatar_User o
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment SettingsWorkspacesBilling_Workspace on Workspace {\n ...BillingAlert_Workspace\n id\n plan {\n ...SettingsWorkspacesBillingPricingTable_WorkspacePlan\n name\n status\n }\n subscription {\n billingInterval\n currentBillingCycleEnd\n }\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesBilling_Workspace on Workspace {\n ...BillingAlert_Workspace\n id\n plan {\n ...SettingsWorkspacesBillingPricingTable_WorkspacePlan\n name\n status\n }\n subscription {\n billingInterval\n currentBillingCycleEnd\n }\n }\n"];
export function graphql(source: "\n fragment SettingsWorkspacesBilling_Workspace on Workspace {\n ...BillingAlert_Workspace\n id\n role\n plan {\n ...SettingsWorkspacesBillingPricingTable_WorkspacePlan\n name\n status\n }\n subscription {\n billingInterval\n currentBillingCycleEnd\n }\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesBilling_Workspace on Workspace {\n ...BillingAlert_Workspace\n id\n role\n plan {\n ...SettingsWorkspacesBillingPricingTable_WorkspacePlan\n name\n status\n }\n subscription {\n billingInterval\n currentBillingCycleEnd\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -16,7 +16,7 @@ import { once } from 'events'
import type http from 'http'
import type express from 'express'
import type net from 'net'
import { MaybeAsync, MaybeNullOrUndefined } from '@speckle/shared'
import { MaybeAsync, MaybeNullOrUndefined, wait } from '@speckle/shared'
import type mocha from 'mocha'
import {
getAvailableRegionKeysFactory,
@ -126,15 +126,17 @@ export const resetPubSubFactory = (deps: { db: Knex }) => async () => {
const ensureAivenExtras = ensureAivenExtrasFactory(deps)
await ensureAivenExtras()
type SubInfo = {
subname: string
subconninfo: string
subpublications: string[]
subslotname: string
}
const subscriptions = (await deps.db.raw(
`SELECT subname, subconninfo, subpublications, subslotname FROM aiven_extras.pg_list_all_subscriptions() WHERE subname ILIKE 'test_%';`
)) as {
rows: Array<{
subname: string
subconninfo: string
subpublications: string[]
subslotname: string
}>
rows: Array<SubInfo>
}
const publications = (await deps.db.raw(
`SELECT pubname FROM pg_publication WHERE pubname ILIKE 'test_%';`
@ -142,22 +144,24 @@ export const resetPubSubFactory = (deps: { db: Knex }) => async () => {
rows: Array<{ pubname: string }>
}
// Drop all subs
for (const sub of subscriptions.rows) {
// Running serially, otherwise some kind of race condition issue can pop up
const dropSubs = async (info: SubInfo) => {
await deps.db.raw(
`SELECT * FROM aiven_extras.pg_alter_subscription_disable('${sub.subname}');`
`SELECT * FROM aiven_extras.pg_alter_subscription_disable('${info.subname}');`
)
await wait(500)
await deps.db.raw(
`SELECT * FROM aiven_extras.pg_drop_subscription('${sub.subname}');`
`SELECT * FROM aiven_extras.pg_drop_subscription('${info.subname}');`
)
await wait(1000)
await deps.db.raw(
`SELECT * FROM aiven_extras.dblink_slot_create_or_drop('${info.subconninfo}', '${info.subslotname}', 'drop');`
)
// TODO: Causes flaky test breakages, maybe we dont need it? ("error: replication slot "test_userssub_region1" is active for PID 2294")
// await deps.db.raw(
// `SELECT * FROM aiven_extras.dblink_slot_create_or_drop('${sub.subconninfo}', '${sub.subslotname}', 'drop');`
// )
}
// Drop all subs
// (concurrently, cause it seems possible and we have those delays there)
await Promise.all(subscriptions.rows.map(dropSubs))
// Drop all pubs
for (const pub of publications.rows) {
await deps.db.raw(`DROP PUBLICATION ${pub.pubname};`)

Просмотреть файл

@ -20,8 +20,8 @@
>
<slot name="title-icon"></slot>
</div>
<div class="flex flex-1 items-center justify-between">
<h6 class="font-semibold text-foreground-2 truncate text-body-2xs">
<div class="flex flex-1 items-center justify-between truncate">
<h6 class="font-semibold text-foreground-2 truncate text-body-2xs pr-2">
{{ title }}
</h6>
<CommonBadge v-if="tag" rounded>
@ -33,8 +33,8 @@
<div v-if="$slots['title-icon']" class="flex items-center justify-center">
<slot name="title-icon"></slot>
</div>
<div class="flex flex-1 items-center justify-between">
<h6 class="font-semibold text-foreground-2 truncate text-body-2xs">
<div class="flex flex-1 items-center justify-between truncate">
<h6 class="font-semibold text-foreground-2 truncate text-body-2xs pr-2">
{{ title }}
</h6>
<CommonBadge v-if="tag" rounded>