feat: handle version limits in publish flows via subscription

This commit is contained in:
Björn Steinhagen
2026-02-02 12:13:59 +02:00
parent d807acb38e
commit bcb1729bed
3 changed files with 113 additions and 24 deletions
+45 -8
View File
@@ -4,6 +4,8 @@
:model-card="modelCard"
:project="project"
:can-edit="canEdit"
:cta-disabled="ctaDisabled"
:cta-disabled-message="ctaDisabledMessage"
@manual-publish-or-load="sendOrCancel"
>
<div class="flex max-[275px]:w-full overflow-hidden my-2">
@@ -17,7 +19,6 @@
full-width
@click.stop="openFilterDialog = true"
>
<!-- Sending&nbsp; -->
<span class="font-bold">{{ modelCard.sendFilter?.name }}:&nbsp;</span>
<span class="truncate">{{ modelCard.sendFilter?.summary }}</span>
</FormButton>
@@ -31,13 +32,18 @@
<FilterListSelect :filter="modelCard.sendFilter" @update:filter="updateFilter" />
<div class="mt-4 flex justify-end items-center space-x-2">
<!-- TODO: Ux wise, users might want to just save the selection and publish it later. -->
<FormButton size="sm" color="outline" @click.stop="saveFilter()">
Save
</FormButton>
<FormButton size="sm" @click.stop="saveFilterAndSend()">
Save & Publish
</FormButton>
<div v-tippy="!canCreateVersionPerm ? canCreateVersionMessage : ''">
<FormButton
size="sm"
:disabled="!canCreateVersionPerm"
@click.stop="saveFilterAndSend()"
>
Save & Publish
</FormButton>
</div>
</div>
</CommonDialog>
@@ -108,7 +114,7 @@
</ModelCardBase>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ref, onMounted } from 'vue'
import ModelCardBase from '~/components/model/CardBase.vue'
import { Square3Stack3DIcon } from '@heroicons/vue/20/solid'
import type { ModelCardNotification } from '~/lib/models/card/notification'
@@ -125,12 +131,14 @@ import {
import { useAccountStore, type DUIAccount } from '~/store/accounts'
import { setVersionMessageMutation } from '~/lib/graphql/mutationsAndQueries'
import { workspacePlanUsageUpdatedSubscription } from '~/lib/workspaces/graphql/subscriptions'
import { useCheckGraphql } from '~/lib/core/composables/useCheckGraphql'
const store = useHostAppStore()
const accountStore = useAccountStore()
const { trackEvent } = useMixpanel()
const app = useNuxtApp()
const { canCreateModelIngestion } = useCheckGraphql()
const cardBase = ref<InstanceType<typeof ModelCardBase>>()
const props = defineProps<{
@@ -149,6 +157,26 @@ app.$baseBinding?.on('documentChanged', () => {
openFilterDialog.value = false
})
const canCreateVersionPerm = ref(true)
const canCreateVersionMessage = ref<string | null>(null)
const checkPermissions = async () => {
const res = await canCreateModelIngestion(
props.modelCard.projectId,
props.modelCard.modelId,
props.modelCard.accountId
)
if (res.queryAvailable) {
canCreateVersionPerm.value = res.authorized
canCreateVersionMessage.value = res.message || null
}
}
const ctaDisabled = computed(
() => !canCreateVersionPerm.value || !!props.modelCard.progress
)
const ctaDisabledMessage = computed(() => canCreateVersionMessage.value || undefined)
const { onResult: onWorkspacePlanUsageUpdated } = useSubscription(
workspacePlanUsageUpdatedSubscription,
() => ({
@@ -160,11 +188,15 @@ const { onResult: onWorkspacePlanUsageUpdated } = useSubscription(
)
onWorkspacePlanUsageUpdated(() => {
// TODO: refetch canCreateVersion and disable CTAs, and potentialls on other CTAs like `Save & Publish` etc. basically same as first approach
void checkPermissions()
})
onMounted(() => {
void checkPermissions()
})
const sendOrCancel = () => {
if (!props.canEdit) {
if (!props.canEdit || !canCreateVersionPerm.value) {
return
}
if (props.modelCard.progress) {
@@ -246,6 +278,7 @@ const setVersionMessage = async (message: string) => {
}
const saveFilterAndSend = async () => {
if (!canCreateVersionPerm.value) return
await saveFilter()
store.sendModel(props.modelCard.modelCardId, 'Filter')
hasSetVersionMessage.value = false
@@ -285,6 +318,10 @@ const expiredNotification = computed(() => {
const ctaType = props.modelCard.progress ? 'Restart' : 'Update'
notification.cta = {
name: ctaType,
disabled: !canCreateVersionPerm.value,
tooltipText: !canCreateVersionPerm.value
? canCreateVersionMessage.value || 'Publish limit reached'
: undefined,
action: async () => {
hasSetVersionMessage.value = false
if (props.modelCard.progress) {
+64 -16
View File
@@ -21,7 +21,6 @@
@search-text-update="updateSearchText"
/>
</div>
<!-- Model selector wizard -->
<div v-if="step === 2 && selectedProject && selectedAccountId">
<WizardModelSelector
:project="selectedProject"
@@ -32,7 +31,6 @@
@next="selectModel"
/>
</div>
<!-- Version selector wizard -->
<div v-if="step === 3">
<SendFiltersAndSettings
v-model="filter"
@@ -44,8 +42,10 @@
}
"
/>
<div class="mt-2">
<FormButton full-width @click="addModel">Publish</FormButton>
<div v-tippy="!canPublish ? publishLimitMessage : ''" class="mt-2">
<FormButton full-width :disabled="!canPublish" @click="addModel">
Publish
</FormButton>
</div>
</div>
<div v-if="urlParseError" class="p-2 text-danger">
@@ -55,6 +55,7 @@
</template>
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useSubscription } from '@vue/apollo-composable'
import type {
ModelListModelItemFragment,
ProjectListProjectItemFragment
@@ -67,6 +68,8 @@ import { useMixpanel } from '~/lib/core/composables/mixpanel'
import { useSettingsTracking } from '~/lib/core/composables/trackSettings'
import type { CardSetting } from '~/lib/models/card/setting'
import { useAddByUrl } from '~/lib/core/composables/addByUrl'
import { useCheckGraphql } from '~/lib/core/composables/useCheckGraphql'
import { workspacePlanUsageUpdatedSubscription } from '~/lib/workspaces/graphql/subscriptions'
const { trackEvent } = useMixpanel()
const { trackSettingsChange } = useSettingsTracking()
@@ -87,6 +90,11 @@ const settings = ref<CardSetting[] | undefined>(undefined)
const settingsWereChanged = ref(false)
const { tryParseUrl, urlParsedData, urlParseError } = useAddByUrl()
const { canCreateModelIngestion } = useCheckGraphql()
const canPublish = ref(true)
const publishLimitMessage = ref<string | null>(null)
const updateSearchText = (text: string | undefined) => {
urlParseError.value = undefined
if (!text) return
@@ -105,6 +113,56 @@ watch(showSendDialog, (newVal) => {
}
})
const checkPermissions = async () => {
if (!selectedProject.value || !selectedModel.value) return
const res = await canCreateModelIngestion(
selectedProject.value.id,
selectedModel.value.id,
selectedAccountId.value
)
if (res.queryAvailable) {
canPublish.value = res.authorized
publishLimitMessage.value = res.message || null
} else {
canPublish.value = true
publishLimitMessage.value = null
}
}
watch(step, async (newVal, oldVal) => {
if (newVal > oldVal) {
if (newVal === 3) {
await checkPermissions()
}
return // exit fast on forward
}
if (newVal === 1) {
selectedProject.value = undefined
selectedModel.value = undefined
}
if (newVal === 2) selectedModel.value = undefined
})
const workspaceId = computed(() => selectedProject.value?.workspace?.id)
const { onResult: onUsageUpdate } = useSubscription(
workspacePlanUsageUpdatedSubscription,
() => ({
input: {
workspaceId: workspaceId.value || ''
}
}),
() => ({
enabled: !!workspaceId.value && step.value === 3,
clientId: selectedAccountId.value
})
)
onUsageUpdate(() => {
void checkPermissions()
})
const selectProject = (accountId: string, project: ProjectListProjectItemFragment) => {
step.value++
selectedAccountId.value = accountId
@@ -125,22 +183,12 @@ const selectModel = (model: ModelListModelItemFragment) => {
void trackEvent('DUI3 Action', { name: 'Publish Wizard', step: 'model selected' })
}
// Clears data if going backwards in the wizard
watch(step, (newVal, oldVal) => {
if (newVal > oldVal) {
return // exit fast on forward
}
if (newVal === 1) {
selectedProject.value = undefined
selectedModel.value = undefined
}
if (newVal === 2) selectedModel.value = undefined
})
const hostAppStore = useHostAppStore()
// accountId, serverUrl, projectId, modelId, sendFilter, settings
const addModel = async () => {
if (!canPublish.value) return
void trackEvent('DUI3 Action', {
name: 'Publish Wizard',
step: 'objects selected',
+4
View File
@@ -3105,6 +3105,8 @@ export type PendingStreamCollaborator = {
id: Scalars['String']['output'];
inviteId: Scalars['String']['output'];
invitedBy: LimitedUser;
/** The project this invite is for */
project?: Maybe<LimitedProject>;
projectId: Scalars['String']['output'];
projectName: Scalars['String']['output'];
role: Scalars['String']['output'];
@@ -6664,6 +6666,7 @@ export enum WorkspaceFeatureName {
HideSpeckleBranding = 'hideSpeckleBranding',
Issues = 'issues',
Markup = 'markup',
ModelValidation = 'modelValidation',
OidcSso = 'oidcSso',
PortfolioDashboards = 'portfolioDashboards',
Presentation = 'presentation',
@@ -7001,6 +7004,7 @@ export type WorkspacePermissionChecks = {
canAcceptInvite: PermissionCheckResult;
canAcceptJoinRequest: PermissionCheckResult;
canAccessDashboards: PermissionCheckResult;
canAccessModelValidation: PermissionCheckResult;
canAccessSso: PermissionCheckResult;
canChangeSeatType: PermissionCheckResult;
canCreateDashboards: PermissionCheckResult;