Skip to content
Merged
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
63 changes: 57 additions & 6 deletions org.knime.python3.scripting.nodes/js-src/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion org.knime.python3.scripting.nodes/js-src/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
},
"dependencies": {
"@knime/components": "1.43.0",
"@knime/kds-components": "0.5.2",
"@knime/kds-components": "0.6.6",
"@knime/scripting-editor": "0.0.121",
"@knime/styles": "1.8.0",
"@knime/ui-extension-service": "2.6.0",
Expand Down
1 change: 1 addition & 0 deletions org.knime.python3.scripting.nodes/js-src/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<sonar.nodejs.executable>${project.build.directory}/node/node</sonar.nodejs.executable>
<sonar.javascript.lcov.reportPaths>coverage/lcov.info</sonar.javascript.lcov.reportPaths>
<sonar.exclusions>node_modules/**/*,dist/**/*,src/dev/**/*,.nyc_output/**/*,coverage/**/*,*.log,config/**/*,**/config.js,**/*.config.js,buildtools/**</sonar.exclusions>
<sonar.coverage.exclusions>**/__tests__/**,**/__mocks__/**,test-setup/**</sonar.coverage.exclusions>
<sonar.css.stylelint.reportPaths>target/stylelint.json</sonar.css.stylelint.reportPaths>
<sonar.eslint.reportPaths>target/eslint.json</sonar.eslint.reportPaths>
<node.version>v22.11.0</node.version>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
<script setup lang="ts">
import { onMounted, ref } from "vue";

import { Button } from "@knime/components";
import { KdsEmptyState } from "@knime/kds-components";

import { pythonScriptingService } from "@/python-scripting-service";
import { usePythonPreviewStatusStore, useSessionStatusStore } from "@/store";
import { usePythonPreviewStatusStore } from "@/store";

const IFRAME_SOURCE = "./preview.html";

const iframe = ref<HTMLIFrameElement | null>(null);
const pythonPreviewStatus = usePythonPreviewStatusStore();
const sessionStatus = useSessionStatusStore();

onMounted(() => {
pythonPreviewStatus.updateViewCallback = () => {
Expand All @@ -32,29 +30,17 @@ onMounted(() => {
:src="IFRAME_SOURCE"
/>
</div>
<div
v-show="!pythonPreviewStatus.hasValidView"
class="placeholder-container"
>
<img id="preview-img" src="/assets/plot-placeholder.svg" />
<div v-if="!pythonPreviewStatus.isExecutedOnce" class="placeholder-text">
Run the code to see the preview.
</div>
<div v-else class="placeholder-text">
The view cannot be displayed.<br />
Check the error message and re-execute the script.
</div>
<Button
:with-border="true"
compact
:disabled="
!sessionStatus.isRunningSupported ||
sessionStatus.status === 'RUNNING_ALL' ||
sessionStatus.status === 'RUNNING_SELECTED'
"
@click="pythonScriptingService.runScript()"
>Run code</Button
>
<div v-if="!pythonPreviewStatus.hasValidView" class="empty-state-container">
<KdsEmptyState
v-if="!pythonPreviewStatus.isExecutedOnce"
headline="Run the code to see the preview"
description="Views generated by the Python script will be displayed here."
/>
<KdsEmptyState
v-else
headline="The view cannot be displayed"
description="Check the error message and re-execute the script."
/>
Comment on lines +35 to +43
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

This duplicates the KdsEmptyState component and only varies headline/description. Consider rendering a single KdsEmptyState and deriving headline/description via computed values (or inline ternaries) to reduce duplication and keep the empty-state messaging easier to maintain.

Suggested change
v-if="!pythonPreviewStatus.isExecutedOnce"
headline="Run the code to see the preview"
description="Views generated by the Python script will be displayed here."
/>
<KdsEmptyState
v-else
headline="The view cannot be displayed"
description="Check the error message and re-execute the script."
/>
:headline="
pythonPreviewStatus.isExecutedOnce
? 'The view cannot be displayed'
: 'Run the code to see the preview'
"
:description="
pythonPreviewStatus.isExecutedOnce
? 'Check the error message and re-execute the script.'
: 'Views generated by the Python script will be displayed here.'
"
/>

Copilot uses AI. Check for mistakes.
</div>
</div>
</template>
Expand All @@ -69,26 +55,11 @@ iframe,
border: none;
}

.placeholder-container {
.empty-state-container {
display: flex;
flex-direction: column;
gap: 20px;
align-items: center;
justify-content: center;
width: 100%;
min-width: 15vw;
height: 100%;
}

#preview-img {
max-width: 80px;
height: auto;
opacity: 0.3;
}

.placeholder-text {
margin-right: 20px;
margin-left: 20px;
text-align: center;
}
</style>
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
<script setup lang="ts">
import { type Ref, onMounted, onUnmounted, ref } from "vue";
import { type Ref, computed, onMounted, onUnmounted, ref } from "vue";
import { useDebounceFn, useResizeObserver } from "@vueuse/core";

import { KdsButton } from "@knime/kds-components";
import { KdsButton, KdsEmptyState } from "@knime/kds-components";

import {
getPythonInitialData,
pythonScriptingService,
} from "@/python-scripting-service";
import { usePythonPreviewStatusStore, useSessionStatusStore } from "@/store";
import {
usePythonPreviewStatusStore,
useSessionStatusStore,
useWorkspaceStore,
} from "@/store";

import PythonWorkspaceBody from "./PythonWorkspaceBody.vue";
import PythonWorkspaceHeader, {
Expand All @@ -35,6 +39,10 @@ const useTotalWidth = () => {
return { totalWidth, headerWidths };
};

// check workspace store for emptiness to decide whether to show the workspace or the empty state
const workspaceStore = useWorkspaceStore();
const isEmpty = computed(() => (workspaceStore?.workspace?.length ?? 0) === 0);

const resetButtonEnabled: Ref<boolean> = ref(false);
const pythonPreviewStatus = usePythonPreviewStatusStore();
const resetWorkspace = async () => {
Expand Down Expand Up @@ -62,7 +70,7 @@ onMounted(() => {
</script>

<template>
<div ref="resizeContainer" class="container">
<div v-if="!isEmpty" ref="resizeContainer" class="container">
<div ref="workspaceRef" class="workspace">
<table>
<PythonWorkspaceHeader
Expand All @@ -87,6 +95,12 @@ onMounted(() => {
/>
</div>
</div>
<div v-else class="container-empty-state">
<KdsEmptyState
headline="No temporary values yet"
description="Run the Python script to display temporary values generated during execution."
/>
</div>
</template>

<style scoped lang="postcss">
Expand All @@ -97,6 +111,13 @@ onMounted(() => {
height: 100%;
}

.container-empty-state {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}

.workspace {
--controls-height: 40px;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { ref } from "vue";
import { mount } from "@vue/test-utils";

import { Button } from "@knime/components";
import { editor, getScriptingService } from "@knime/scripting-editor";
import { KdsEmptyState } from "@knime/kds-components";
import { getScriptingService } from "@knime/scripting-editor";

import { usePythonPreviewStatusStore, useSessionStatusStore } from "@/store";
import { usePythonPreviewStatusStore } from "@/store";
import PythonViewPreview from "../PythonViewPreview.vue";

describe("PythonViewPreview", () => {
Expand All @@ -23,6 +22,7 @@ describe("PythonViewPreview", () => {
});

it("renders iframe", () => {
previewStatusStore.hasValidView = true;
const wrapper = mount(PythonViewPreview);
const iframe = wrapper.find("iframe");
expect(iframe.exists()).toBeTruthy();
Expand All @@ -36,6 +36,7 @@ describe("PythonViewPreview", () => {
});

it("replaces iframe when update view callback is called", () => {
previewStatusStore.hasValidView = true;
// Mock the iframe's contentWindow
const mockIframeContentWindow = {
location: {
Expand Down Expand Up @@ -64,76 +65,40 @@ describe("PythonViewPreview", () => {
previewStatusStore.hasValidView = true;
const wrapper = mount(PythonViewPreview);
expect(wrapper.find(".iframe-container").isVisible()).toBeTruthy();
expect(wrapper.find(".placeholder-container").isVisible()).toBeFalsy();
expect(wrapper.find(".empty-state-container").exists()).toBeFalsy();
});

describe("placeholder", () => {
it("shows placeholder if valid view does not exist", () => {
describe("empty-state", () => {
it("shows empty state component if valid view does not exist", () => {
const wrapper = mount(PythonViewPreview);
expect(wrapper.find(".iframe-container").isVisible()).toBeFalsy();
expect(wrapper.find(".placeholder-container").isVisible()).toBeTruthy();
expect(wrapper.find("#preview-img").isVisible()).toBeTruthy();
expect(wrapper.find(".empty-state-container").isVisible()).toBeTruthy();
expect(wrapper.findComponent(KdsEmptyState).exists()).toBeTruthy();
});

it("shows placeholder text before first execution", () => {
it("shows empty state component before first execution", () => {
const wrapper = mount(PythonViewPreview);
const emptyState = wrapper.findComponent(KdsEmptyState);
expect(wrapper.find(".iframe-container").isVisible()).toBeFalsy();
expect(wrapper.find(".placeholder-container").isVisible()).toBeTruthy();
expect(wrapper.find(".placeholder-text").text()).toContain(
"Run the code to see the preview.",
expect(wrapper.find(".empty-state-container").isVisible()).toBeTruthy();
expect(emptyState.props().headline).toBe(
"Run the code to see the preview",
);
expect(emptyState.props().description).toBe(
"Views generated by the Python script will be displayed here.",
);
});

it("shows error text after first execution", () => {
previewStatusStore.isExecutedOnce = true;
const wrapper = mount(PythonViewPreview);
const emptyState = wrapper.findComponent(KdsEmptyState);
expect(wrapper.find(".iframe-container").isVisible()).toBeFalsy();
expect(wrapper.find(".placeholder-container").isVisible()).toBeTruthy();
expect(wrapper.find(".placeholder-text").text()).toContain(
"The view cannot be displayed.",
expect(wrapper.find(".empty-state-container").isVisible()).toBeTruthy();
expect(emptyState.props().headline).toBe("The view cannot be displayed");
expect(emptyState.props().description).toBe(
"Check the error message and re-execute the script.",
);
});

it("placholder button executes script", () => {
useSessionStatusStore().isRunningSupported = true;
editor.useMainCodeEditorStore().value = { text: ref("myScript") } as any;
previewStatusStore.hasValidView = false;
const wrapper = mount(PythonViewPreview);
wrapper.find("button").trigger("click");
expect(sendToServiceMock).toHaveBeenCalledWith("runScript", ["myScript"]);
});

it("shows placeholder button", () => {
useSessionStatusStore().status = "IDLE";
useSessionStatusStore().isRunningSupported = true;
const wrapper = mount(PythonViewPreview);
const button = wrapper.findComponent(Button);
expect(button.exists()).toBeTruthy();
expect(button.props().disabled).toBeFalsy();
});

it("disables placeholder button if no inputs available", () => {
useSessionStatusStore().status = "IDLE";
useSessionStatusStore().isRunningSupported = false;
const wrapper = mount(PythonViewPreview);
const button = wrapper.findComponent(Button);
expect(button.props().disabled).toBeTruthy();
});

it("disables placeholder button if running all", () => {
useSessionStatusStore().status = "RUNNING_ALL";
useSessionStatusStore().isRunningSupported = true;
const wrapper = mount(PythonViewPreview);
const button = wrapper.findComponent(Button);
expect(button.props().disabled).toBeTruthy();
});

it("disables placeholder button if running selected lines", () => {
useSessionStatusStore().status = "RUNNING_SELECTED";
useSessionStatusStore().isRunningSupported = true;
const wrapper = mount(PythonViewPreview);
const button = wrapper.findComponent(Button);
expect(button.props().disabled).toBeTruthy();
});
});
});
Loading