From 165ba37a22e8aa6584c3d137dd32dc9b574394ab Mon Sep 17 00:00:00 2001 From: Kai Franze Date: Fri, 5 Dec 2025 13:10:56 +0100 Subject: [PATCH 1/5] NXT-4224: Minor formatting and code style changes NXT-4224 (Automatically / Manually sync when editing workflows in the browser) --- .../eclipse/org/knime/ui/java/api/DesktopAPI.java | 4 ++-- .../eclipse/org/knime/ui/java/api/OpenProject.java | 5 +++-- .../eclipse/org/knime/ui/java/api/SaveProject.java | 10 ++++++++++ .../org/knime/ui/java/browser/lifecycle/Init.java | 3 +-- .../org/knime/ui/java/util/CreateProject.java | 12 +----------- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/org.knime.ui.java/src/eclipse/org/knime/ui/java/api/DesktopAPI.java b/org.knime.ui.java/src/eclipse/org/knime/ui/java/api/DesktopAPI.java index d7551cbdc..805ab39ae 100644 --- a/org.knime.ui.java/src/eclipse/org/knime/ui/java/api/DesktopAPI.java +++ b/org.knime.ui.java/src/eclipse/org/knime/ui/java/api/DesktopAPI.java @@ -62,7 +62,6 @@ import org.eclipse.swt.widgets.Display; import org.knime.core.node.NodeLogger; import org.knime.gateway.api.service.GatewayException; -import org.knime.ui.java.util.ProgressReporter; import org.knime.gateway.api.webui.entity.GatewayProblemDescriptionEnt; import org.knime.gateway.api.webui.service.util.MutableServiceCallException; import org.knime.gateway.api.webui.service.util.ServiceExceptions.LoggedOutException; @@ -86,6 +85,7 @@ import org.knime.ui.java.profile.UserProfile; import org.knime.ui.java.util.ExampleProjects; import org.knime.ui.java.util.MostRecentlyUsedProjects; +import org.knime.ui.java.util.ProgressReporter; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -268,7 +268,7 @@ private static Object invokeMethod(final Method m, final Object[] args) throws T * @param progressReporter * @throws IllegalStateException if the dependencies have been already injected */ - @SuppressWarnings({"java:S107", "JavadocDeclaration", "javadoc"}) // Parameter count + @SuppressWarnings({"java:S107", "JavadocDeclaration"}) // Parameter count public static void injectDependencies( // final ProjectManager projectManager, // final WorkflowMiddleware workflowMiddleware, // diff --git a/org.knime.ui.java/src/eclipse/org/knime/ui/java/api/OpenProject.java b/org.knime.ui.java/src/eclipse/org/knime/ui/java/api/OpenProject.java index accbb5a5a..03f889c75 100644 --- a/org.knime.ui.java/src/eclipse/org/knime/ui/java/api/OpenProject.java +++ b/org.knime.ui.java/src/eclipse/org/knime/ui/java/api/OpenProject.java @@ -58,7 +58,6 @@ import org.knime.core.node.workflow.contextv2.HubSpaceLocationInfo; import org.knime.core.util.Pair; import org.knime.core.util.hub.NamedItemVersion; -import org.knime.ui.java.util.ProgressReporter; import org.knime.gateway.api.util.VersionId; import org.knime.gateway.api.webui.entity.SpaceItemReferenceEnt.ProjectTypeEnum; import org.knime.gateway.api.webui.entity.SpaceItemVersionEnt; @@ -75,6 +74,7 @@ import org.knime.ui.java.util.CreateProject; import org.knime.ui.java.util.DesktopAPUtil; import org.knime.ui.java.util.MostRecentlyUsedProjects; +import org.knime.ui.java.util.ProgressReporter; import org.knime.workbench.core.imports.RepoObjectImport; /** @@ -136,7 +136,8 @@ static void openProject(final String spaceId, final String itemId, final String project = optProject.get(); } else { final var origin = new Origin(spaceProviderId, spaceId, itemId, projectType); - project = CreateProject.createProjectFromOrigin(origin, DesktopAPI.getDeps(ProgressReporter.class), space); + final var progressReporter = DesktopAPI.getDeps(ProgressReporter.class); + project = CreateProject.createProjectFromOrigin(origin, progressReporter, space); } // already trigger loading of wfm here because we want to abort and not register the project if this fails diff --git a/org.knime.ui.java/src/eclipse/org/knime/ui/java/api/SaveProject.java b/org.knime.ui.java/src/eclipse/org/knime/ui/java/api/SaveProject.java index 01b9a4b00..fa0e43538 100644 --- a/org.knime.ui.java/src/eclipse/org/knime/ui/java/api/SaveProject.java +++ b/org.knime.ui.java/src/eclipse/org/knime/ui/java/api/SaveProject.java @@ -142,6 +142,10 @@ private static boolean isExecutionInProgress(final WorkflowManager wfm) { return state.isExecutionInProgress() || state.isExecutingRemotely(); } + /** + * Starts a separate thread to show a progress bar while saving the project. This is a blocking operation since we + * wait for the saving to finish and block the UI thread in the meantime. + */ private static Boolean saveProjectWithProgressBar(final WorkflowManager wfm, final boolean localOnly, final boolean allowOverwritePrompt) { var wasSaveSuccessful = new AtomicBoolean(); @@ -277,6 +281,9 @@ private static boolean saveBackToServerOrHub(final IProgressMonitor rootMonitor, } } + /** + * Checks whether an upload to Hub can proceed, possibly prompting the user about overwriting existing items. + */ private static boolean checkHubUpload(final String mountId, final HubSpaceLocationInfo hubInfo, final Space hubSpace, final boolean allowOverwritePrompt) throws GatewayException { @@ -307,6 +314,9 @@ private static boolean hubItemExists(final HubSpaceLocationInfo hubInfo, final S } } + /** + * Checks whether an upload to Server can proceed, possibly prompting the user about overwriting existing items. + */ private static boolean checkServerUpload(final URI mountpointUri) { final var remoteStore = (RemoteExplorerFileStore)ExplorerMountTable.getFileSystem().getStore(mountpointUri); final var fetchedInfo = remoteStore.fetchInfo(); diff --git a/org.knime.ui.java/src/eclipse/org/knime/ui/java/browser/lifecycle/Init.java b/org.knime.ui.java/src/eclipse/org/knime/ui/java/browser/lifecycle/Init.java index 1bf1c1949..a602c31c3 100644 --- a/org.knime.ui.java/src/eclipse/org/knime/ui/java/browser/lifecycle/Init.java +++ b/org.knime.ui.java/src/eclipse/org/knime/ui/java/browser/lifecycle/Init.java @@ -226,8 +226,7 @@ static LifeCycleStateInternal run(final LifeCycleStateInternal state, final bool state.getWelcomeApEndpoint(), // createExampleProjects(), // state.getUserProfile(), // - progressReporter // - ); + progressReporter); // Register listeners var softwareUpdateProgressListener = registerSoftwareUpdateProgressListener(eventConsumer); diff --git a/org.knime.ui.java/src/eclipse/org/knime/ui/java/util/CreateProject.java b/org.knime.ui.java/src/eclipse/org/knime/ui/java/util/CreateProject.java index 9e50a4f73..72cc689c6 100644 --- a/org.knime.ui.java/src/eclipse/org/knime/ui/java/util/CreateProject.java +++ b/org.knime.ui.java/src/eclipse/org/knime/ui/java/util/CreateProject.java @@ -103,17 +103,7 @@ public static Project createProjectFromOrigin(final Origin origin, final Progres return createProjectFromOrigin(projectId, name, origin, progressReporter, space); } - /** - * Create a {@link Project} instance that corresponds to an {@link Origin}, i.e. an item in a {@link Space}. - * - * @param projectId - - * @param name - - * @param origin - - * @param progressReporter - - * @param space - - * @return - - */ - public static Project createProjectFromOrigin(final String projectId, final String name, final Origin origin, + private static Project createProjectFromOrigin(final String projectId, final String name, final Origin origin, final ProgressReporter progressReporter, final Space space) { return Project.builder() // .setWfmLoader(fromOriginWithProgressReporter(origin, progressReporter, space)) // From 780a10f7ee1ca18bb0d363974970d524b6a7bd3e Mon Sep 17 00:00:00 2001 From: Kai Franze Date: Fri, 5 Dec 2025 13:11:56 +0100 Subject: [PATCH 2/5] NXT-4224: WIP: Adding workflow sync provider to 'init' phase NXT-4224 (Automatically / Manually sync when editing workflows in the browser) --- .../eclipse/org/knime/ui/java/browser/lifecycle/Init.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/org.knime.ui.java/src/eclipse/org/knime/ui/java/browser/lifecycle/Init.java b/org.knime.ui.java/src/eclipse/org/knime/ui/java/browser/lifecycle/Init.java index a602c31c3..c6a259813 100644 --- a/org.knime.ui.java/src/eclipse/org/knime/ui/java/browser/lifecycle/Init.java +++ b/org.knime.ui.java/src/eclipse/org/knime/ui/java/browser/lifecycle/Init.java @@ -102,6 +102,7 @@ import org.knime.gateway.impl.webui.spaces.SpaceProvidersManager.Key; import org.knime.gateway.impl.webui.spaces.local.LocalSpace; import org.knime.gateway.impl.webui.spaces.local.LocalSpaceProvider; +import org.knime.gateway.impl.webui.syncing.WorkflowSyncerProvider; import org.knime.gateway.json.util.ObjectMapperUtil; import org.knime.js.cef.CEFPlugin; import org.knime.js.cef.commservice.CEFCommService; @@ -182,6 +183,7 @@ static LifeCycleStateInternal run(final LifeCycleStateInternal state, final bool var selectionEventBus = createSelectionEventBus(eventConsumer); NodeCategoryExtensions nodeCategoryExtensions = () -> NodeSpecCollectionProvider.getInstance().getCategoryExtensions(); + var workflowSyncerProvider = WorkflowSyncerProvider.disabled(); MostRecentlyUsedProjects mostRecentlyUsedProjects; if (state.mostRecentlyUsedProjects() != null) { @@ -209,7 +211,8 @@ static LifeCycleStateInternal run(final LifeCycleStateInternal state, final bool nodeCollections, // nodeCategoryExtensions, // selectionEventBus, // - linkVariants // + linkVariants, // + workflowSyncerProvider // ); DesktopAPI.injectDependencies( // From 03524af806aae4dfa8e28b20638f4a0188247020 Mon Sep 17 00:00:00 2001 From: Kai Franze Date: Fri, 5 Dec 2025 13:12:33 +0100 Subject: [PATCH 3/5] NXT-4224: WIP: Adding hacky first iteration workflow sync UI NXT-4224 (Automatically / Manually sync when editing workflows in the browser) --- .../src/api/gateway-api/generated-api.ts | 101 +++++++++++++++++- .../components/toolbar/WorkflowToolbar.vue | 73 ++++++++++++- .../src/store/application/application.ts | 47 ++++++++ .../src/toastPresets/application.ts | 16 +++ 4 files changed, 231 insertions(+), 6 deletions(-) diff --git a/org.knime.ui.js/src/api/gateway-api/generated-api.ts b/org.knime.ui.js/src/api/gateway-api/generated-api.ts index 5022eb915..597cd35f2 100644 --- a/org.knime.ui.js/src/api/gateway-api/generated-api.ts +++ b/org.knime.ui.js/src/api/gateway-api/generated-api.ts @@ -688,6 +688,12 @@ export interface AppState { * @memberof AppState */ spaceProviders?: Array; + /** + * + * @type {ProjectSyncState} + * @memberof AppState + */ + projectSyncState?: ProjectSyncState; } @@ -3947,6 +3953,52 @@ export interface ProjectMetadata extends EditableMetadata { */ export namespace ProjectMetadata { } +/** + * The synchronization state of a project. + * @export + * @interface ProjectSyncState + */ +export interface ProjectSyncState { + + /** + * + * @type {string} + * @memberof ProjectSyncState + */ + state: ProjectSyncState.StateEnum; + /** + * Whether automatic synchronization is currently enabled. + * @type {boolean} + * @memberof ProjectSyncState + */ + isAutoSyncEnabled: boolean; + /** + * + * @type {SyncStateDetails} + * @memberof ProjectSyncState + */ + details?: SyncStateDetails; + +} + + +/** + * @export + * @namespace ProjectSyncState + */ +export namespace ProjectSyncState { + /** + * @export + * @enum {string} + */ + export enum StateEnum { + SYNCED = 'SYNCED', + DIRTY = 'DIRTY', + BLOCKED = 'BLOCKED', + UPLOAD = 'UPLOAD', + ERROR = 'ERROR' + } +} /** * Remove a port from a node * @export @@ -4742,6 +4794,53 @@ export interface StyleRange { } +/** + * Additional details about a certain project sync state. + * @export + * @interface SyncStateDetails + */ +export interface SyncStateDetails { + + /** + * + * @type {string} + * @memberof SyncStateDetails + */ + code: string; + /** + * + * @type {string} + * @memberof SyncStateDetails + */ + title: string; + /** + * + * @type {Array} + * @memberof SyncStateDetails + */ + details: Array; + /** + * + * @type {boolean} + * @memberof SyncStateDetails + */ + canCopy: boolean; + /** + * + * @type {number} + * @memberof SyncStateDetails + */ + status?: number; + /** + * + * @type {string} + * @memberof SyncStateDetails + */ + stackTrace?: string; + +} + + /** * The link of a metanode or component. * @export @@ -5920,7 +6019,7 @@ const componenteditor = function(rpcClient: RPCClient) { return rpcClient.call('ComponentEditorService.applyComponentEditorConfig', { ...defaultParams, ...params }); }, /** - * Returns the state required to render the component editor. + * Returns the state required to render the component editor. * @param {string} params.projectId ID of the workflow-project. * @param {string} params.workflowId The ID of a workflow which has the same format as a node-id. * @param {string} params.nodeId The ID of a node. The node-id format: Node IDs always start with 'root' and optionally followed by numbers separated by ':' referring to nested nodes/subworkflows,e.g. root:3:6:4. Nodes within components require an additional trailing '0', e.g. 'root:3:6:0:4' (if 'root:3:6' is a component). diff --git a/org.knime.ui.js/src/components/toolbar/WorkflowToolbar.vue b/org.knime.ui.js/src/components/toolbar/WorkflowToolbar.vue index 01f7beda2..0a55b5b91 100644 --- a/org.knime.ui.js/src/components/toolbar/WorkflowToolbar.vue +++ b/org.knime.ui.js/src/components/toolbar/WorkflowToolbar.vue @@ -4,12 +4,15 @@ import { API } from "@api"; import { storeToRefs } from "pinia"; import { type MenuItem, SubMenu, useHint } from "@knime/components"; -import { useKdsDynamicModal } from "@knime/kds-components"; +import { KdsButton, useKdsDynamicModal } from "@knime/kds-components"; import ArrowMoveIcon from "@knime/styles/img/icons/arrow-move.svg"; import CloudUploadIcon from "@knime/styles/img/icons/cloud-upload.svg"; import DeploymentIcon from "@knime/styles/img/icons/deployment.svg"; -import { WorkflowInfo } from "@/api/gateway-api/generated-api"; +import { + ProjectSyncState, + WorkflowInfo, +} from "@/api/gateway-api/generated-api"; import AnnotationModeIcon from "@/assets/annotation-mode.svg"; import SelectionModeIcon from "@/assets/selection-mode.svg"; import ToolbarButton from "@/components/common/ToolbarButton.vue"; @@ -44,9 +47,12 @@ import ZoomMenu from "./ZoomMenu.vue"; const $shortcuts = useShortcuts(); const uiControls = useUIControlsStore(); const workflowVersionsStore = useWorkflowVersionsStore(); -const { activeProjectId, activeProjectOrigin, isUnknownProject } = storeToRefs( - useApplicationStore(), -); +const { + activeProjectId, + activeProjectOrigin, + isUnknownProject, + projectSyncState, +} = storeToRefs(useApplicationStore()); const { activeWorkflow, isWorkflowEmpty, isActiveWorkflowFixedVersion } = storeToRefs(useWorkflowStore()); const { getSelectedNodes: selectedNodes } = storeToRefs(useSelectionStore()); @@ -266,10 +272,63 @@ const ToolbarButtonWithHint = defineComponent( ); const { isSVGRenderer } = useCanvasRendererUtils(); + +// --- Hacky sync button logic ------------------------------------------------ +const { toastPresets } = getToastPresets(); + +const isSyncButtonVisible = computed(() => { + return isBrowser(); +}); + +const onSyncClick = async () => { + const projectId = activeProjectId.value!; + + try { + await API.workflow.saveProject({ projectId }); + } catch (error) { + toastPresets.app.syncProjectFailed({ error }); + } +}; + +const isSyncButtonDisabled = computed( + () => projectSyncState.value?.state !== ProjectSyncState.StateEnum.DIRTY, +); + +const syncState = computed(() => { + const state = projectSyncState.value?.state; + const isAutoSyncEnabled = projectSyncState.value?.isAutoSyncEnabled; + switch (state) { + case ProjectSyncState.StateEnum.SYNCED: + return "Synced"; + case ProjectSyncState.StateEnum.DIRTY: + return isAutoSyncEnabled + ? "Dirty (auto-sync enabled)" + : "Dirty (auto-sync disabled)"; + case ProjectSyncState.StateEnum.BLOCKED: + return "Syncing (blocked)"; + case ProjectSyncState.StateEnum.UPLOAD: + return "Syncing (upload)"; + case ProjectSyncState.StateEnum.ERROR: + return "Sync error"; + default: + return "Unexpected state"; // This should never happen + } +}); +// ----------------------------------------------------------------------------