Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The removal of 'javadoc' from the suppressions list should be accompanied by ensuring that all Javadoc issues are resolved. Verify that no Javadoc warnings remain for this method.

Copilot uses AI. Check for mistakes.
public static void injectDependencies( //
final ProjectManager projectManager, //
final WorkflowMiddleware workflowMiddleware, //
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

/**
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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 {

Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,8 @@ static LifeCycleStateInternal run(final LifeCycleStateInternal state, final bool
nodeCollections, //
nodeCategoryExtensions, //
selectionEventBus, //
linkVariants //
linkVariants, //
null // WorkflowSyncerProvider not needed in desktop UI
);

DesktopAPI.injectDependencies( //
Expand All @@ -226,8 +227,7 @@ static LifeCycleStateInternal run(final LifeCycleStateInternal state, final bool
state.getWelcomeApEndpoint(), //
createExampleProjects(), //
state.getUserProfile(), //
progressReporter //
);
progressReporter);

// Register listeners
var softwareUpdateProgressListener = registerSoftwareUpdateProgressListener(eventConsumer);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)) //
Expand Down
101 changes: 100 additions & 1 deletion org.knime.ui.js/src/api/gateway-api/generated-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -688,6 +688,12 @@ export interface AppState {
* @memberof AppState
*/
spaceProviders?: Array<SpaceProvider>;
/**
*
* @type {ProjectSyncState}
* @memberof AppState
*/
projectSyncState?: ProjectSyncState;

}

Expand Down Expand Up @@ -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',
WRITING = 'WRITING',
UPLOAD = 'UPLOAD',
ERROR = 'ERROR'
}
}
/**
* Remove a port from a node
* @export
Expand Down Expand Up @@ -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<string>}
* @memberof SyncStateDetails
*/
details: Array<string>;
/**
*
* @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
Expand Down Expand Up @@ -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 &#39;root&#39; and optionally followed by numbers separated by &#39;:&#39; referring to nested nodes/subworkflows,e.g. root:3:6:4. Nodes within components require an additional trailing &#39;0&#39;, e.g. &#39;root:3:6:0:4&#39; (if &#39;root:3:6&#39; is a component).
Expand Down
73 changes: 68 additions & 5 deletions org.knime.ui.js/src/components/toolbar/WorkflowToolbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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.WRITING:
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
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message 'Unexpected state' is not actionable for users or developers. Consider returning a more descriptive message that includes the actual state value, such as Unknown sync state: ${state}, to aid debugging.

Suggested change
return "Unexpected state"; // This should never happen
return `Unknown sync state: ${String(state)}`; // This should never happen

Copilot uses AI. Check for mistakes.
}
});
// ----------------------------------------------------------------------------
</script>

<template>
<div class="toolbar">
<!-- Hacky sync button directly displayed here --------------------------->
<KdsButton
v-if="isSyncButtonVisible"
:label="syncState"
:disabled="isSyncButtonDisabled"
class="sync-button"
@click="onSyncClick"
/>
<!------------------------------------------------------------------------>

<transition-group tag="div" name="button-list">
<!--
setting :key="the list of all visible buttons",
Expand Down Expand Up @@ -369,6 +428,10 @@ const { isSVGRenderer } = useCanvasRendererUtils();
background-color: var(--knime-gray-ultra-light);
border-bottom: 1px solid var(--knime-silver-sand);

& .sync-button {
margin-right: 12px;
}

& .button-list {
transition: opacity 150ms ease-out;
flex-shrink: 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,14 @@ vi.mock("@knime/components", async (importOriginal) => {
const { askConfirmationMock } = vi.hoisted(() => ({
askConfirmationMock: vi.fn(() => Promise.resolve({ confirmed: true })),
}));
vi.mock("@knime/kds-components", () => ({
useKdsDynamicModal: () => ({ askConfirmation: askConfirmationMock }),
}));
vi.mock("@knime/kds-components", async (importOriginal) => {
const actual = await importOriginal<unknown>();
return {
// @ts-expect-error
...actual,
useKdsDynamicModal: () => ({ askConfirmation: askConfirmationMock }),
};
});

vi.mock("@/environment");

Expand Down
Loading
Loading