feat: handle model card state according to given ingestion id (#89)

* feat: handle model card state according to given ingestion id

* chore: linting
This commit is contained in:
Oğuzhan Koral
2026-02-25 14:00:59 +03:00
committed by GitHub
parent ed4aa92ce1
commit 85f806368a
6 changed files with 459 additions and 110 deletions
+1
View File
@@ -26,6 +26,7 @@ export interface ISendBindingEvents
modelCardId: string
versionId: string
sendConversionResults: ConversionResult[]
ingestionId?: string
}) => void
setIdMap: (args: {
modelCardId: string
+6
View File
@@ -61,6 +61,7 @@ type Documents = {
"\n mutation FailModelIngestionWithError($input: ModelIngestionFailedInput!) {\n projectMutations {\n modelIngestionMutations {\n failWithError(input: $input) {\n id\n }\n }\n }\n }\n": typeof types.FailModelIngestionWithErrorDocument,
"\n mutation FailModelIngestionWithCancel($input: ModelIngestionCancelledInput!) {\n projectMutations {\n modelIngestionMutations {\n failWithCancel(input: $input) {\n id\n }\n }\n }\n }\n": typeof types.FailModelIngestionWithCancelDocument,
"\n query CanCreateIngestion($modelId: String!, $projectId: String!) {\n project(id: $projectId) {\n model(id: $modelId) {\n permissions {\n canCreateIngestion {\n authorized\n code\n message\n }\n }\n }\n }\n }\n": typeof types.CanCreateIngestionDocument,
"\n subscription ProjectModelIngestionUpdated(\n $input: ProjectModelIngestionSubscriptionInput!\n ) {\n projectModelIngestionUpdated(input: $input) {\n type\n modelIngestion {\n id\n statusData {\n __typename\n ... on ModelIngestionSuccessStatus {\n status\n versionId\n }\n ... on ModelIngestionProcessingStatus {\n status\n progressMessage\n progress\n }\n ... on ModelIngestionFailedStatus {\n status\n errorReason\n }\n ... on ModelIngestionCancelledStatus {\n status\n cancellationMessage\n }\n ... on ModelIngestionQueuedStatus {\n status\n progressMessage\n }\n }\n }\n }\n }\n": typeof types.ProjectModelIngestionUpdatedDocument,
"\n fragment IssuesItem on Issue {\n id\n status\n title\n priority\n viewerState\n identifier\n resourceIdString\n activities(input: { limit: 1, sortDirection: asc }) {\n totalCount\n items {\n actor {\n id\n user {\n name\n id\n avatar\n }\n }\n eventType\n createdAt\n }\n }\n replies {\n totalCount\n items {\n id\n author {\n id\n user {\n name\n id\n avatar\n }\n }\n createdAt\n description {\n doc\n }\n }\n }\n description {\n doc\n }\n labels {\n hexColor\n id\n name\n }\n author {\n id\n user {\n id\n name\n avatar\n }\n }\n dueDate\n assignee {\n id\n user {\n id\n avatar\n name\n }\n }\n }\n": typeof types.IssuesItemFragmentDoc,
"\n query IssuesList($projectId: String!) {\n project(id: $projectId) {\n id\n issues {\n totalCount\n items {\n ...IssuesItem\n }\n }\n }\n }\n": typeof types.IssuesListDocument,
"\n subscription WorkspacePlanUsageUpdated($input: WorkspacePlanUsageSubscriptionInput!) {\n workspacePlanUsageUpdated(input: $input)\n }\n": typeof types.WorkspacePlanUsageUpdatedDocument,
@@ -113,6 +114,7 @@ const documents: Documents = {
"\n mutation FailModelIngestionWithError($input: ModelIngestionFailedInput!) {\n projectMutations {\n modelIngestionMutations {\n failWithError(input: $input) {\n id\n }\n }\n }\n }\n": types.FailModelIngestionWithErrorDocument,
"\n mutation FailModelIngestionWithCancel($input: ModelIngestionCancelledInput!) {\n projectMutations {\n modelIngestionMutations {\n failWithCancel(input: $input) {\n id\n }\n }\n }\n }\n": types.FailModelIngestionWithCancelDocument,
"\n query CanCreateIngestion($modelId: String!, $projectId: String!) {\n project(id: $projectId) {\n model(id: $modelId) {\n permissions {\n canCreateIngestion {\n authorized\n code\n message\n }\n }\n }\n }\n }\n": types.CanCreateIngestionDocument,
"\n subscription ProjectModelIngestionUpdated(\n $input: ProjectModelIngestionSubscriptionInput!\n ) {\n projectModelIngestionUpdated(input: $input) {\n type\n modelIngestion {\n id\n statusData {\n __typename\n ... on ModelIngestionSuccessStatus {\n status\n versionId\n }\n ... on ModelIngestionProcessingStatus {\n status\n progressMessage\n progress\n }\n ... on ModelIngestionFailedStatus {\n status\n errorReason\n }\n ... on ModelIngestionCancelledStatus {\n status\n cancellationMessage\n }\n ... on ModelIngestionQueuedStatus {\n status\n progressMessage\n }\n }\n }\n }\n }\n": types.ProjectModelIngestionUpdatedDocument,
"\n fragment IssuesItem on Issue {\n id\n status\n title\n priority\n viewerState\n identifier\n resourceIdString\n activities(input: { limit: 1, sortDirection: asc }) {\n totalCount\n items {\n actor {\n id\n user {\n name\n id\n avatar\n }\n }\n eventType\n createdAt\n }\n }\n replies {\n totalCount\n items {\n id\n author {\n id\n user {\n name\n id\n avatar\n }\n }\n createdAt\n description {\n doc\n }\n }\n }\n description {\n doc\n }\n labels {\n hexColor\n id\n name\n }\n author {\n id\n user {\n id\n name\n avatar\n }\n }\n dueDate\n assignee {\n id\n user {\n id\n avatar\n name\n }\n }\n }\n": types.IssuesItemFragmentDoc,
"\n query IssuesList($projectId: String!) {\n project(id: $projectId) {\n id\n issues {\n totalCount\n items {\n ...IssuesItem\n }\n }\n }\n }\n": types.IssuesListDocument,
"\n subscription WorkspacePlanUsageUpdated($input: WorkspacePlanUsageSubscriptionInput!) {\n workspacePlanUsageUpdated(input: $input)\n }\n": types.WorkspacePlanUsageUpdatedDocument,
@@ -320,6 +322,10 @@ export function graphql(source: "\n mutation FailModelIngestionWithCancel($inpu
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query CanCreateIngestion($modelId: String!, $projectId: String!) {\n project(id: $projectId) {\n model(id: $modelId) {\n permissions {\n canCreateIngestion {\n authorized\n code\n message\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query CanCreateIngestion($modelId: String!, $projectId: String!) {\n project(id: $projectId) {\n model(id: $modelId) {\n permissions {\n canCreateIngestion {\n authorized\n code\n message\n }\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n subscription ProjectModelIngestionUpdated(\n $input: ProjectModelIngestionSubscriptionInput!\n ) {\n projectModelIngestionUpdated(input: $input) {\n type\n modelIngestion {\n id\n statusData {\n __typename\n ... on ModelIngestionSuccessStatus {\n status\n versionId\n }\n ... on ModelIngestionProcessingStatus {\n status\n progressMessage\n progress\n }\n ... on ModelIngestionFailedStatus {\n status\n errorReason\n }\n ... on ModelIngestionCancelledStatus {\n status\n cancellationMessage\n }\n ... on ModelIngestionQueuedStatus {\n status\n progressMessage\n }\n }\n }\n }\n }\n"): (typeof documents)["\n subscription ProjectModelIngestionUpdated(\n $input: ProjectModelIngestionSubscriptionInput!\n ) {\n projectModelIngestionUpdated(input: $input) {\n type\n modelIngestion {\n id\n statusData {\n __typename\n ... on ModelIngestionSuccessStatus {\n status\n versionId\n }\n ... on ModelIngestionProcessingStatus {\n status\n progressMessage\n progress\n }\n ... on ModelIngestionFailedStatus {\n status\n errorReason\n }\n ... on ModelIngestionCancelledStatus {\n status\n cancellationMessage\n }\n ... on ModelIngestionQueuedStatus {\n status\n progressMessage\n }\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
File diff suppressed because one or more lines are too long
+95 -3
View File
@@ -1,4 +1,8 @@
import { provideApolloClient, useMutation } from '@vue/apollo-composable'
import {
provideApolloClient,
useMutation,
useSubscription
} from '@vue/apollo-composable'
import { useAccountStore } from '~/store/accounts'
import { useHostAppStore } from '~/store/hostApp'
import {
@@ -8,7 +12,11 @@ import {
failModelIngestionWithError,
failModelIngestionWithCancel
} from '../graphql/mutations'
import type { SourceDataInput } from '~~/lib/common/generated/gql/graphql'
import { projectModelIngestionUpdatedSubscription } from '../graphql/subscriptions'
import type {
SourceDataInput,
ProjectModelIngestionUpdatedSubscription
} from '~~/lib/common/generated/gql/graphql'
import type { ISenderModelCard } from '~/lib/models/card/send'
import { storeToRefs } from 'pinia'
import { ToastNotificationType } from '@speckle/ui-components'
@@ -217,11 +225,95 @@ export const useModelIngestion = () => {
return res?.data?.projectMutations.modelIngestionMutations.completeWithVersion
}
// Tracks active ingestion subscriptions so they can be stopped on cancel or terminal state
const activeSubscriptions: Record<string, () => void> = {}
/**
* Subscribes to ingestion status updates for a given ingestionId.
* Used when the connector (.NET SDK) handles the ingestion and passes the ingestionId
* back to the DUI via setModelSendResult. The DUI then subscribes to track
* the server-side processing state until a terminal status is reached.
*
* Manages model card state directly: updates progress, sets versionId on success,
* sets error on failure, and clears progress on terminal states.
*/
const subscribeToIngestion = (
senderModelCard: ISenderModelCard,
ingestionId: string
) => {
const client = accountStore.getAccountClient(senderModelCard.accountId)
senderModelCard.progress = { status: 'Remote processing...' }
const { onResult, onError, stop } = provideApolloClient(client)(() =>
useSubscription(projectModelIngestionUpdatedSubscription, () => ({
input: {
projectId: senderModelCard.projectId,
ingestionReference: { ingestionId }
}
}))
)
activeSubscriptions[senderModelCard.modelCardId] = stop
onResult((result) => {
const data = result.data as ProjectModelIngestionUpdatedSubscription | undefined
const statusData = data?.projectModelIngestionUpdated?.modelIngestion?.statusData
if (!statusData) return
switch (statusData.__typename) {
case 'ModelIngestionSuccessStatus':
senderModelCard.latestCreatedVersionId = statusData.versionId
senderModelCard.progress = undefined
unsubscribeFromIngestion(senderModelCard.modelCardId)
break
case 'ModelIngestionProcessingStatus':
senderModelCard.progress = {
status: statusData.progressMessage,
progress: statusData.progress ?? undefined
}
break
case 'ModelIngestionFailedStatus':
senderModelCard.error = {
errorMessage: statusData.errorReason,
dismissible: true
}
senderModelCard.progress = undefined
unsubscribeFromIngestion(senderModelCard.modelCardId)
break
case 'ModelIngestionCancelledStatus':
senderModelCard.progress = undefined
unsubscribeFromIngestion(senderModelCard.modelCardId)
break
case 'ModelIngestionQueuedStatus':
senderModelCard.progress = {
status: statusData.progressMessage
}
break
}
})
onError((err) => {
console.error('Ingestion subscription error:', err)
unsubscribeFromIngestion(senderModelCard.modelCardId)
})
}
const unsubscribeFromIngestion = (modelCardId: string) => {
const stop = activeSubscriptions[modelCardId]
if (stop) {
stop()
delete activeSubscriptions[modelCardId]
}
}
return {
startIngestion,
updateIngestion,
failIngestion,
cancelIngestion,
completeIngestionWithVersion
completeIngestionWithVersion,
subscribeToIngestion,
unsubscribeFromIngestion
}
}
+38
View File
@@ -0,0 +1,38 @@
import { graphql } from '~~/lib/common/generated/gql'
export const projectModelIngestionUpdatedSubscription = graphql(`
subscription ProjectModelIngestionUpdated(
$input: ProjectModelIngestionSubscriptionInput!
) {
projectModelIngestionUpdated(input: $input) {
type
modelIngestion {
id
statusData {
__typename
... on ModelIngestionSuccessStatus {
status
versionId
}
... on ModelIngestionProcessingStatus {
status
progressMessage
progress
}
... on ModelIngestionFailedStatus {
status
errorReason
}
... on ModelIngestionCancelledStatus {
status
cancellationMessage
}
... on ModelIngestionQueuedStatus {
status
progressMessage
}
}
}
}
}
`)
+17 -3
View File
@@ -51,7 +51,9 @@ export const useHostAppStore = defineStore('hostAppStore', () => {
updateIngestion,
failIngestion,
cancelIngestion,
completeIngestionWithVersion
completeIngestionWithVersion,
subscribeToIngestion,
unsubscribeFromIngestion
} = useModelIngestion()
const isDistributedBySpeckle = ref<boolean>(true)
const latestAvailableVersion = ref<Version | null>(null)
@@ -477,6 +479,9 @@ export const useHostAppStore = defineStore('hostAppStore', () => {
void trackEvent('DUI3 Action', { name: 'Send Cancel' }, model.accountId)
model.latestCreatedVersionId = undefined
// Clean up any active ingestion subscription from SDK-based connectors
unsubscribeFromIngestion(modelCardId)
// Cancel the ingestion if applicable
if (shouldHandleIngestion.value) {
const ingestionId = activeIngestions.value[modelCardId]
@@ -500,13 +505,22 @@ export const useHostAppStore = defineStore('hostAppStore', () => {
modelCardId: string
versionId: string
sendConversionResults: ConversionResult[]
ingestionId?: string
}) => {
const model = documentModelStore.value.models.find(
(m) => m.modelCardId === args.modelCardId
) as ISenderModelCard
model.latestCreatedVersionId = args.versionId
// Conversion results are always valid regardless of ingestion state
model.report = args.sendConversionResults
model.progress = undefined
if (args.ingestionId) {
// Connector handled ingestion via SDK — composable subscribes and manages model card state to 'Version created' bla bla
subscribeToIngestion(model, args.ingestionId)
} else {
// Legacy path or no ingestion — behave as before
model.latestCreatedVersionId = args.versionId
model.progress = undefined
}
}
app.$sendBinding?.on('setModelSendResult', setModelSendResult)