From a216743b8f0d8b70f9ceae647ae46698e1a7dfad Mon Sep 17 00:00:00 2001 From: Jonas B <97200640+SmiteDeluxe@users.noreply.github.com> Date: Sat, 11 May 2024 14:28:20 -0600 Subject: [PATCH] feat: eda plot view (#1161) Closes #955 Closes #986 ### Summary of Changes Implemented history/action structure for webview to add new actions to state and have them (if external and info not already present) send execute requests to runner that are then cancellable or deployed in correct order. Only fully working for Plots/Tabs at the moment, that are on deploy added to tabs state and set as currentIndex. All Tabs and Table retain their state. Runner uses existing methods in RunnerAPI to get back to relevant state by executing past manipulating actions and then returns the result of the new action (only if plots right now). Tabs can be created by selecting columns and right clicking, zooming in on profiling images or by creating an empty tab with the plus icon in the sidebar. There in the guided menu users can change the current Tab to display other info. At that point the tab will go into building state and show prompts, loading screens and buttons accordingly. Typings are adapted to abstract as much as possible (mainly around column count for tabs, none, one and two) and stores are heavily used for reactivity. --------- Co-authored-by: Lars Reimann Co-authored-by: WinPlay02 Co-authored-by: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com> --- .eslintignore | 1 + packages/safe-ds-eda/consts.config.ts | 1 + packages/safe-ds-eda/src/App.svelte | 33 +- packages/safe-ds-eda/src/apis/extensionApi.ts | 20 +- packages/safe-ds-eda/src/apis/historyApi.ts | 245 ++++++++++ .../src/components/NewTabButton.svelte | 25 + .../safe-ds-eda/src/components/Sidebar.svelte | 63 ++- .../src/components/TableView.svelte | 286 ++++++----- .../ColumnFilters.svelte | 2 +- .../components/profiling/ProfilingInfo.svelte | 75 ++- .../src/components/tabs/LinePlotTab.svelte | 58 --- .../src/components/tabs/SidebarTab.svelte | 93 ++++ .../src/components/tabs/TabContent.svelte | 451 ++++++++++++++++++ .../tabs/content/ImageContent.svelte | 25 + .../utilities/DropDownButton.svelte | 169 +++++++ packages/safe-ds-eda/src/icons/BarPlot.svelte | 31 +- packages/safe-ds-eda/src/icons/BoxPlot.svelte | 18 + packages/safe-ds-eda/src/icons/Heatmap.svelte | 15 + .../safe-ds-eda/src/icons/LinePlot.svelte | 19 +- packages/safe-ds-eda/src/icons/Plus.svelte | 16 + .../safe-ds-eda/src/icons/ScatterPlot.svelte | 8 + packages/safe-ds-eda/src/icons/Swap.svelte | 6 + packages/safe-ds-eda/src/icons/Table.svelte | 2 +- packages/safe-ds-eda/src/icons/Zoom.svelte | 11 + .../src/toggleNonContextMenuEffects.ts | 47 ++ packages/safe-ds-eda/src/webviewState.ts | 16 +- packages/safe-ds-eda/tsconfig.json | 2 +- packages/safe-ds-eda/types/messaging.d.ts | 54 --- packages/safe-ds-eda/types/messaging.ts | 85 ++++ packages/safe-ds-eda/types/state.d.ts | 206 -------- packages/safe-ds-eda/types/state.ts | 348 ++++++++++++++ packages/safe-ds-vscode/media/styles.css | 16 + .../src/extension/eda/apis/runnerApi.ts | 396 ++++++++++++--- .../src/extension/eda/edaPanel.ts | 144 +++--- 34 files changed, 2366 insertions(+), 621 deletions(-) create mode 100644 packages/safe-ds-eda/consts.config.ts create mode 100644 packages/safe-ds-eda/src/apis/historyApi.ts create mode 100644 packages/safe-ds-eda/src/components/NewTabButton.svelte rename packages/safe-ds-eda/src/components/{columnFilters => column-filters}/ColumnFilters.svelte (99%) delete mode 100644 packages/safe-ds-eda/src/components/tabs/LinePlotTab.svelte create mode 100644 packages/safe-ds-eda/src/components/tabs/SidebarTab.svelte create mode 100644 packages/safe-ds-eda/src/components/tabs/TabContent.svelte create mode 100644 packages/safe-ds-eda/src/components/tabs/content/ImageContent.svelte create mode 100644 packages/safe-ds-eda/src/components/utilities/DropDownButton.svelte create mode 100644 packages/safe-ds-eda/src/icons/BoxPlot.svelte create mode 100644 packages/safe-ds-eda/src/icons/Heatmap.svelte create mode 100644 packages/safe-ds-eda/src/icons/Plus.svelte create mode 100644 packages/safe-ds-eda/src/icons/ScatterPlot.svelte create mode 100644 packages/safe-ds-eda/src/icons/Swap.svelte create mode 100644 packages/safe-ds-eda/src/icons/Zoom.svelte create mode 100644 packages/safe-ds-eda/src/toggleNonContextMenuEffects.ts delete mode 100644 packages/safe-ds-eda/types/messaging.d.ts create mode 100644 packages/safe-ds-eda/types/messaging.ts delete mode 100644 packages/safe-ds-eda/types/state.d.ts create mode 100644 packages/safe-ds-eda/types/state.ts diff --git a/.eslintignore b/.eslintignore index b2ebd2b63..e3878766d 100644 --- a/.eslintignore +++ b/.eslintignore @@ -9,5 +9,6 @@ # Configuration /vitest.config.ts /packages/safe-ds-eda/svelte.config.js +/packages/safe-ds-eda/consts.config.ts /packages/safe-ds-eda/vite.config.ts /packages/safe-ds-eda/types/*.d.ts diff --git a/packages/safe-ds-eda/consts.config.ts b/packages/safe-ds-eda/consts.config.ts new file mode 100644 index 000000000..0dc42f1bf --- /dev/null +++ b/packages/safe-ds-eda/consts.config.ts @@ -0,0 +1 @@ +export const imageWidthToHeightRatio = 1 + 1 / 3; diff --git a/packages/safe-ds-eda/src/App.svelte b/packages/safe-ds-eda/src/App.svelte index 59541c42e..b436865b1 100644 --- a/packages/safe-ds-eda/src/App.svelte +++ b/packages/safe-ds-eda/src/App.svelte @@ -2,6 +2,8 @@ import TableView from './components/TableView.svelte'; import Sidebar from './components/Sidebar.svelte'; import { throttle } from 'lodash'; + import { currentTabIndex, currentState } from './webviewState'; + import TabContent from './components/tabs/TabContent.svelte'; let sidebarWidth = 307; // Initial width of the sidebar in pixels @@ -37,12 +39,21 @@
-
+
-
- +
+
+ +
+ {#if $currentState.tabs} + {#each $currentState.tabs as tab, index} +
+ +
+ {/each} + {/if}
@@ -59,15 +70,17 @@ flex-shrink: 0; overflow: hidden; position: relative; - background-color: var(--bg-bright); + background-color: var(--bg-dark); } - .white-bg { - background-color: var(--bg-bright); + .contentWrapper { + flex: 1; + width: 100%; } - .tableWrapper { - flex: 1; + .contentWrapper * { + height: 100%; + width: 100%; } .resizer { @@ -79,4 +92,8 @@ cursor: ew-resize; background-color: transparent; } + + .hide { + display: none; + } diff --git a/packages/safe-ds-eda/src/apis/extensionApi.ts b/packages/safe-ds-eda/src/apis/extensionApi.ts index 58e4f41a7..883dbe7da 100644 --- a/packages/safe-ds-eda/src/apis/extensionApi.ts +++ b/packages/safe-ds-eda/src/apis/extensionApi.ts @@ -1,18 +1,4 @@ -import type { State } from '../../types/state'; - -export const setCurrentGlobalState = function (state: State) { - window.injVscode.postMessage({ - command: 'setCurrentGlobalState', - value: state, - }); -}; - -export const resetGlobalState = function () { - window.injVscode.postMessage({ - command: 'resetGlobalState', - value: null, - }); -}; +import type { HistoryEntry } from '../../types/state'; export const createInfoToast = function (message: string) { window.injVscode.postMessage({ command: 'setInfo', value: message }); @@ -21,3 +7,7 @@ export const createInfoToast = function (message: string) { export const createErrorToast = function (message: string) { window.injVscode.postMessage({ command: 'setError', value: message }); }; + +export const executeRunner = function (pastEntries: HistoryEntry[], newEntry: HistoryEntry) { + window.injVscode.postMessage({ command: 'executeRunner', value: { pastEntries, newEntry } }); +}; diff --git a/packages/safe-ds-eda/src/apis/historyApi.ts b/packages/safe-ds-eda/src/apis/historyApi.ts new file mode 100644 index 000000000..de723ebd5 --- /dev/null +++ b/packages/safe-ds-eda/src/apis/historyApi.ts @@ -0,0 +1,245 @@ +import { get } from 'svelte/store'; +import type { FromExtensionMessage, RunnerExecutionResultMessage } from '../../types/messaging'; +import type { + EmptyTab, + ExternalHistoryEntry, + HistoryEntry, + InteralEmptyTabHistoryEntry, + InternalHistoryEntry, + RealTab, + Tab, + TabHistoryEntry, +} from '../../types/state'; +import { cancelTabIdsWaiting, currentState, currentTabIndex } from '../webviewState'; +import { executeRunner } from './extensionApi'; + +// Wait for results to return from the server +const asyncQueue: (ExternalHistoryEntry & { id: number })[] = []; +let messagesWaitingForTurn: RunnerExecutionResultMessage[] = []; +let entryIdCounter = 0; + +export const getAndIncrementEntryId = function (): number { + return entryIdCounter++; +}; + +window.addEventListener('message', (event) => { + const message = event.data as FromExtensionMessage; + + if (message.command === 'runnerExecutionResult') { + if (asyncQueue.length === 0) { + throw new Error('No entries in asyncQueue'); + } + const asyncQueueEntryIndex = asyncQueue.findIndex((entry) => entry.id === message.value.historyId); + if (asyncQueueEntryIndex === -1) return; + if (asyncQueueEntryIndex !== 0) { + // eslint-disable-next-line no-console + console.log('Message not in turn, waiting for turn'); + messagesWaitingForTurn.push(message); + return; + } + + deployResult(message); + asyncQueue.shift(); + evaluateMessagesWaitingForTurn(); + } else if (message.command === 'cancelRunnerExecution') { + cancelExecuteExternalHistoryEntry(message.value); + } +}); + +export const addInternalToHistory = function (entry: InternalHistoryEntry): void { + currentState.update((state) => { + const entryWithId: HistoryEntry = { + ...entry, + id: getAndIncrementEntryId(), + }; + const newHistory = [...state.history, entryWithId]; + return { + ...state, + history: newHistory, + }; + }); +}; + +export const executeExternalHistoryEntry = function (entry: ExternalHistoryEntry): void { + currentState.update((state) => { + const entryWithId: HistoryEntry = { + ...entry, + id: getAndIncrementEntryId(), + }; + const newHistory = [...state.history, entryWithId]; + + asyncQueue.push(entryWithId); + executeRunner(state.history, entryWithId); // Is this good in here? Otherwise risk of empty array idk + + return { + ...state, + history: newHistory, + }; + }); +}; + +export const addAndDeployTabHistoryEntry = function (entry: TabHistoryEntry & { id: number }, tab: Tab): void { + // Search if already exists and is up to date + const existingTab = get(currentState).tabs?.find( + (et) => + et.type !== 'empty' && + et.type === tab.type && + et.tabComment === tab.tabComment && + tab.type && + !et.content.outdated && + !et.isInGeneration, + ); + if (existingTab) { + currentTabIndex.set(get(currentState).tabs!.indexOf(existingTab)); + return; + } + + currentState.update((state) => { + const newHistory = [...state.history, entry]; + + return { + ...state, + history: newHistory, + tabs: (state.tabs ?? []).concat([tab]), + }; + }); + currentTabIndex.set(get(currentState).tabs!.indexOf(tab)); +}; + +export const addEmptyTabHistoryEntry = function (): void { + const entry: InteralEmptyTabHistoryEntry & { id: number } = { + action: 'emptyTab', + type: 'internal', + alias: 'New empty tab', + id: getAndIncrementEntryId(), + }; + const tab: EmptyTab = { + type: 'empty', + id: crypto.randomUUID(), + isInGeneration: true, + }; + + currentState.update((state) => { + const newHistory = [...state.history, entry]; + + return { + ...state, + history: newHistory, + tabs: (state.tabs ?? []).concat([tab]), + }; + }); + currentTabIndex.set(get(currentState).tabs!.indexOf(tab)); +}; + +export const cancelExecuteExternalHistoryEntry = function (entry: HistoryEntry): void { + const index = asyncQueue.findIndex((queueEntry) => queueEntry.id === entry.id); + if (index !== -1) { + asyncQueue.splice(index, 1); + if (entry.type === 'external-visualizing' && entry.existingTabId) { + cancelTabIdsWaiting.update((ids) => { + return ids.concat([entry.existingTabId!]); + }); + const tab: RealTab = get(currentState).tabs!.find( + (t) => t.type !== 'empty' && t.id === entry.existingTabId, + )! as RealTab; + unsetTabAsGenerating(tab); + } + } else { + throw new Error('Entry already fully executed'); + } +}; + +export const setTabAsGenerating = function (tab: RealTab): void { + currentState.update((state) => { + const newTabs = state.tabs?.map((t) => { + if (t === tab) { + return { + ...t, + isInGeneration: true, + }; + } else { + return t; + } + }); + + return { + ...state, + tabs: newTabs, + }; + }); +}; + +export const unsetTabAsGenerating = function (tab: RealTab): void { + currentState.update((state) => { + const newTabs = state.tabs?.map((t) => { + if (t === tab) { + return { + ...t, + isInGeneration: false, + }; + } else { + return t; + } + }); + + return { + ...state, + tabs: newTabs, + }; + }); +}; + +const deployResult = function (result: RunnerExecutionResultMessage) { + const resultContent = result.value; + if (resultContent.type === 'tab') { + if (resultContent.content.id) { + const existingTab = get(currentState).tabs?.find((et) => et.id === resultContent.content.id); + if (existingTab) { + const tabIndex = get(currentState).tabs!.indexOf(existingTab); + currentState.update((state) => { + return { + ...state, + tabs: state.tabs?.map((t) => { + if (t.id === resultContent.content.id) { + return resultContent.content; + } else { + return t; + } + }), + }; + }); + currentTabIndex.set(tabIndex); + return; + } + } + const tab = resultContent.content; + tab.id = crypto.randomUUID(); + currentState.update((state) => { + return { + ...state, + tabs: (state.tabs ?? []).concat(tab), + }; + }); + currentTabIndex.set(get(currentState).tabs!.indexOf(tab)); + } +}; + +const evaluateMessagesWaitingForTurn = function () { + const newMessagesWaitingForTurn: RunnerExecutionResultMessage[] = []; + let firstItemQueueChanged = false; + + for (const entry of messagesWaitingForTurn) { + if (asyncQueue[0].id === entry.value.historyId) { + // eslint-disable-next-line no-console + console.log(`Deploying message from waiting queue: ${entry}`); + deployResult(entry); + asyncQueue.shift(); + firstItemQueueChanged = true; + } else if (asyncQueue.findIndex((queueEntry) => queueEntry.id === entry.value.historyId) !== -1) { + newMessagesWaitingForTurn.push(entry); // Only those that still exist in asyncqueue and were not the first item still have to be waited for + } + } + + messagesWaitingForTurn = newMessagesWaitingForTurn; + if (firstItemQueueChanged) evaluateMessagesWaitingForTurn(); // Only if first element was deployed we have to scan again, as this is only deployment condition +}; diff --git a/packages/safe-ds-eda/src/components/NewTabButton.svelte b/packages/safe-ds-eda/src/components/NewTabButton.svelte new file mode 100644 index 000000000..1b8e58acf --- /dev/null +++ b/packages/safe-ds-eda/src/components/NewTabButton.svelte @@ -0,0 +1,25 @@ + + +
+
+ +
+
+ + diff --git a/packages/safe-ds-eda/src/components/Sidebar.svelte b/packages/safe-ds-eda/src/components/Sidebar.svelte index a337c2df8..d5f15e1a2 100644 --- a/packages/safe-ds-eda/src/components/Sidebar.svelte +++ b/packages/safe-ds-eda/src/components/Sidebar.svelte @@ -1,12 +1,19 @@ -
- {#if width > 50} + {#if width > 50} +
{#if width > 200}History{/if} @@ -30,34 +37,35 @@ {#if width > 200}Redo{/if} - {/if} -
+
+ {/if}
{#if width > 50} - + {#if $currentState.tabs} {#each $currentState.tabs as tab, index} - {#if tab.type === 'linePlot'} - - {/if} + {/each} {/if} {/if}
+ {#if width > 50} +
+ +
+ {/if} diff --git a/packages/safe-ds-eda/src/components/tabs/LinePlotTab.svelte b/packages/safe-ds-eda/src/components/tabs/LinePlotTab.svelte deleted file mode 100644 index c0a76d0c6..000000000 --- a/packages/safe-ds-eda/src/components/tabs/LinePlotTab.svelte +++ /dev/null @@ -1,58 +0,0 @@ - - -
-
- - {#if width > 109}Line Plot{/if} -
- {#if width > 300}{tabObject.tabComment}{/if} -
- - diff --git a/packages/safe-ds-eda/src/components/tabs/SidebarTab.svelte b/packages/safe-ds-eda/src/components/tabs/SidebarTab.svelte new file mode 100644 index 000000000..7b306dac5 --- /dev/null +++ b/packages/safe-ds-eda/src/components/tabs/SidebarTab.svelte @@ -0,0 +1,93 @@ + + +
+
+
+ {#if tabObject.isInGeneration} +
+ {:else if tabObject.type === 'linePlot'} + + {:else if tabObject.type === 'boxPlot'} + + {:else if tabObject.type === 'heatmap'} + + {:else if tabObject.type === 'infoPanel'} + + {:else if tabObject.type === 'histogram'} + + {:else if tabObject.type === 'scatterPlot'} + + {/if} +
+ {#if width > 300 || tabObject.isInGeneration || (tabObject.tabComment === '' && width > 109)} + {#if tabObject.isInGeneration} + Generating... + {:else if tabObject.type === 'histogram'} + Histogram + {:else if tabObject.type === 'boxPlot'} + Boxplot + {:else if tabObject.type === 'heatmap'} + Heatmap + {:else if tabObject.type === 'infoPanel'} + Info panel + {:else if tabObject.type === 'linePlot'} + Lineplot + {:else if tabObject.type === 'scatterPlot'} + Scatterplot + {/if} + {/if} +
+ {#if width > 109 && !tabObject.isInGeneration}{tabObject.tabComment}{/if} +
+ + diff --git a/packages/safe-ds-eda/src/components/tabs/TabContent.svelte b/packages/safe-ds-eda/src/components/tabs/TabContent.svelte new file mode 100644 index 000000000..ede378b66 --- /dev/null +++ b/packages/safe-ds-eda/src/components/tabs/TabContent.svelte @@ -0,0 +1,451 @@ + + +
+
+
+
+
+
+ + {#if tab.type !== 'empty' && tab.content.outdated} + Outdated! + {/if} +
+
+
+ {#if $tabInfo.type !== 'empty' && ($tabInfo.columnNumber === 'one' || $tabInfo.columnNumber === 'two')} +
+ {#if $tabInfo.columnNumber === 'one'} + Column +
+ +
+ {:else} + X-Axis +
+ +
+ {/if} +
+ {/if} + {#if $tabInfo.type !== 'empty' && $tabInfo.columnNumber === 'two'} +
+ +
+
+ Y-Axis + +
+ {/if} +
+
+
+ {#if tab.type !== 'empty' && tab.imageTab} +
+ +
+ {/if} + {#if (tab.type === 'empty' || tab.imageTab) && $isInBuildingState} + {#if isLoadingGeneratedTab} +
+

Loading ...

+
+ {:else if $buildATabComplete} +
+ +
+ {:else} +
+

Complete selections

+
+ {/if} + {/if} +
+
+
+
+ + diff --git a/packages/safe-ds-eda/src/components/tabs/content/ImageContent.svelte b/packages/safe-ds-eda/src/components/tabs/content/ImageContent.svelte new file mode 100644 index 000000000..290729e72 --- /dev/null +++ b/packages/safe-ds-eda/src/components/tabs/content/ImageContent.svelte @@ -0,0 +1,25 @@ + + +
+ profiling plot +
+ + diff --git a/packages/safe-ds-eda/src/components/utilities/DropDownButton.svelte b/packages/safe-ds-eda/src/components/utilities/DropDownButton.svelte new file mode 100644 index 000000000..b7e1d6725 --- /dev/null +++ b/packages/safe-ds-eda/src/components/utilities/DropDownButton.svelte @@ -0,0 +1,169 @@ + + +
+ + + {#if isDropdownOpen} + + {/if} +
+ + diff --git a/packages/safe-ds-eda/src/icons/BarPlot.svelte b/packages/safe-ds-eda/src/icons/BarPlot.svelte index f3728e13f..a2af2b7be 100644 --- a/packages/safe-ds-eda/src/icons/BarPlot.svelte +++ b/packages/safe-ds-eda/src/icons/BarPlot.svelte @@ -1,6 +1,25 @@ - - - + + diff --git a/packages/safe-ds-eda/src/icons/BoxPlot.svelte b/packages/safe-ds-eda/src/icons/BoxPlot.svelte new file mode 100644 index 000000000..4db268132 --- /dev/null +++ b/packages/safe-ds-eda/src/icons/BoxPlot.svelte @@ -0,0 +1,18 @@ + + + diff --git a/packages/safe-ds-eda/src/icons/Heatmap.svelte b/packages/safe-ds-eda/src/icons/Heatmap.svelte new file mode 100644 index 000000000..6a72376a3 --- /dev/null +++ b/packages/safe-ds-eda/src/icons/Heatmap.svelte @@ -0,0 +1,15 @@ + + + chart-heatmap-solid + + + + + + + + + diff --git a/packages/safe-ds-eda/src/icons/LinePlot.svelte b/packages/safe-ds-eda/src/icons/LinePlot.svelte index 7bf240b0d..668c5107e 100644 --- a/packages/safe-ds-eda/src/icons/LinePlot.svelte +++ b/packages/safe-ds-eda/src/icons/LinePlot.svelte @@ -1,6 +1,15 @@ - - + + + chart-line-solid + + + + + + + + diff --git a/packages/safe-ds-eda/src/icons/Plus.svelte b/packages/safe-ds-eda/src/icons/Plus.svelte new file mode 100644 index 000000000..c13ab58ff --- /dev/null +++ b/packages/safe-ds-eda/src/icons/Plus.svelte @@ -0,0 +1,16 @@ + + + + + + diff --git a/packages/safe-ds-eda/src/icons/ScatterPlot.svelte b/packages/safe-ds-eda/src/icons/ScatterPlot.svelte new file mode 100644 index 000000000..a84328670 --- /dev/null +++ b/packages/safe-ds-eda/src/icons/ScatterPlot.svelte @@ -0,0 +1,8 @@ + + + diff --git a/packages/safe-ds-eda/src/icons/Swap.svelte b/packages/safe-ds-eda/src/icons/Swap.svelte new file mode 100644 index 000000000..6a06a23f3 --- /dev/null +++ b/packages/safe-ds-eda/src/icons/Swap.svelte @@ -0,0 +1,6 @@ + diff --git a/packages/safe-ds-eda/src/icons/Table.svelte b/packages/safe-ds-eda/src/icons/Table.svelte index 3ff160b6a..4e76656fc 100644 --- a/packages/safe-ds-eda/src/icons/Table.svelte +++ b/packages/safe-ds-eda/src/icons/Table.svelte @@ -1,4 +1,4 @@ - + + + + + diff --git a/packages/safe-ds-eda/src/toggleNonContextMenuEffects.ts b/packages/safe-ds-eda/src/toggleNonContextMenuEffects.ts new file mode 100644 index 000000000..f509e2426 --- /dev/null +++ b/packages/safe-ds-eda/src/toggleNonContextMenuEffects.ts @@ -0,0 +1,47 @@ +const originalHoverStyles = new Map(); +const originalCursorStyles = new Map(); + +export const disableNonContextMenuEffects = function () { + const stylesheets = document.styleSheets; + + for (let i = 0; i < stylesheets.length; i++) { + const rules = stylesheets[i].cssRules; + const ownerNode = stylesheets[i].ownerNode; + if ( + !(ownerNode instanceof Element) || + (ownerNode instanceof Element && ownerNode.id && !ownerNode.id.includes('svelte')) + ) { + // We only care for stylesheets that are svlete generated + continue; + } + + for (let j = 0; j < rules.length; j++) { + // Remove all hover styles and cursor pointer styles from non context menu elements + const rule = rules[j] as CSSStyleRule; + if (rule.selectorText?.includes(':hover') && !rule.selectorText?.includes('contextMenu')) { + // Store the original hover style + originalHoverStyles.set(rule, rule.style.cssText); + // Disable the hover style + rule.style.cssText = ''; + } + if (rule.style?.cursor === 'pointer' && !rule.selectorText?.includes('contextMenu')) { + // Store the original pointer style + originalCursorStyles.set(rule, rule.style.cssText); + // Disable the cursor pointer + rule.style.cursor = 'auto'; + } + } + } +}; + +export const restoreNonContextMenuEffects = function () { + originalHoverStyles.forEach((style, rule) => { + rule.style.cssText = style; + }); + originalHoverStyles.clear(); + + originalCursorStyles.forEach((style, rule) => { + rule.style.cssText = style; + }); + originalCursorStyles.clear(); +}; diff --git a/packages/safe-ds-eda/src/webviewState.ts b/packages/safe-ds-eda/src/webviewState.ts index a19d1459d..24fbf0fc7 100644 --- a/packages/safe-ds-eda/src/webviewState.ts +++ b/packages/safe-ds-eda/src/webviewState.ts @@ -1,21 +1,15 @@ import type { FromExtensionMessage } from '../types/messaging'; import type { State } from '../types/state'; -import * as extensionApi from './apis/extensionApi'; import { get, writable } from 'svelte/store'; -let currentTabIndex = writable(0); +let currentTabIndex = writable(undefined); let preventClicks = writable(false); -// Define the stores, current state to default in case the extension never calls setWebviewState( Shouldn't happen) -let currentState = writable({ tableIdentifier: undefined, history: [], defaultState: true }); +let cancelTabIdsWaiting = writable([]); -// Set Global states whenever updatedAllStates changes -currentState.subscribe(($currentState) => { - if (!$currentState.defaultState) { - extensionApi.setCurrentGlobalState($currentState); - } -}); +// Define the stores, current state to default in case the extension never calls setWebviewState( Shouldn't happen) +const currentState = writable({ tableIdentifier: undefined, history: [], defaultState: true, tabs: [] }); window.addEventListener('message', (event) => { const message = event.data as FromExtensionMessage; @@ -55,4 +49,4 @@ window.addEventListener('message', (event) => { } }); -export { currentState, currentTabIndex, preventClicks }; +export { currentState, currentTabIndex, preventClicks, cancelTabIdsWaiting }; diff --git a/packages/safe-ds-eda/tsconfig.json b/packages/safe-ds-eda/tsconfig.json index 6deff8f67..15ac19ba8 100644 --- a/packages/safe-ds-eda/tsconfig.json +++ b/packages/safe-ds-eda/tsconfig.json @@ -4,5 +4,5 @@ "rootDir": ".", "noEmit": true }, - "include": ["src/**/*", "types/**/*"] + "include": ["src/**/*", "types/**/*", "./*.ts"] } diff --git a/packages/safe-ds-eda/types/messaging.d.ts b/packages/safe-ds-eda/types/messaging.d.ts deleted file mode 100644 index 438c7081e..000000000 --- a/packages/safe-ds-eda/types/messaging.d.ts +++ /dev/null @@ -1,54 +0,0 @@ -import * as defaultTypes from './state'; - -// To extension -type ToExtensionCommand = 'setGlobalState' | 'setInfo' | 'setError'; - -interface ToExtensionCommandMessage { - command: ToExtensionCommand; - value: any; -} -interface ToExtensionSetStateMessage extends ToExtensionCommandMessage { - command: 'setCurrentGlobalState'; - value: defaultTypes.State; -} - -interface ToExtensionResetStateMessage extends ToExtensionCommandMessage { - command: 'resetGlobalState'; - value: null; -} - -// Just example -interface ToExtensionSetInfoMessage extends ToExtensionCommandMessage { - command: 'setInfo'; - value: string; -} - -interface ToExtensionSetErrorMessage extends ToExtensionCommandMessage { - command: 'setError'; - value: string; -} - -export type ToExtensionMessage = - | ToExtensionSetInfoMessage - | ToExtensionSetStateMessage - | ToExtensionResetStateMessage - | ToExtensionSetErrorMessage; - -// From extension -type FromExtensionCommand = 'setWebviewState'; - -interface FromExtensionCommandMessage { - command: FromExtensionCommand; - value: any; -} -interface FromExtensionSetStateMessage extends FromExtensionCommandMessage { - command: 'setWebviewState'; - value: defaultTypes.State; -} - -interface FromExtensionSetProfilingMessage extends FromExtensionCommandMessage { - command: 'setProfiling'; - value: { columnName: string; profiling: defaultTypes.Profiling }[]; -} - -export type FromExtensionMessage = FromExtensionSetStateMessage | FromExtensionSetProfilingMessage; diff --git a/packages/safe-ds-eda/types/messaging.ts b/packages/safe-ds-eda/types/messaging.ts new file mode 100644 index 000000000..7a009310b --- /dev/null +++ b/packages/safe-ds-eda/types/messaging.ts @@ -0,0 +1,85 @@ +import * as defaultTypes from './state.js'; + +// To extension +type ToExtensionCommand = 'setCurrentGlobalState' | 'resetGlobalState' | 'setInfo' | 'setError' | 'executeRunner'; + +interface ToExtensionCommandMessage { + command: ToExtensionCommand; + value: any; +} + +interface ToExtensionSetInfoMessage extends ToExtensionCommandMessage { + command: 'setInfo'; + value: string; +} + +interface ToExtensionSetErrorMessage extends ToExtensionCommandMessage { + command: 'setError'; + value: string; +} + +interface ToExtensionExecuteRunnerMessage extends ToExtensionCommandMessage { + command: 'executeRunner'; + value: { + pastEntries: defaultTypes.HistoryEntry[]; + newEntry: defaultTypes.HistoryEntry; + }; +} + +export type ToExtensionMessage = + | ToExtensionSetInfoMessage + | ToExtensionSetErrorMessage + | ToExtensionExecuteRunnerMessage; + +// From extension +type FromExtensionCommand = 'setWebviewState' | 'setProfiling' | 'runnerExecutionResult' | 'cancelRunnerExecution'; + +interface FromExtensionCommandMessage { + command: FromExtensionCommand; + value: any; +} +interface FromExtensionSetStateMessage extends FromExtensionCommandMessage { + command: 'setWebviewState'; + value: defaultTypes.State; +} + +interface FromExtensionSetProfilingMessage extends FromExtensionCommandMessage { + command: 'setProfiling'; + value: { columnName: string; profiling: defaultTypes.Profiling }[]; +} + +interface RunnerExecutionResultBase { + type: 'tab' | 'table' | 'profiling'; + historyId: number; +} + +interface RunnerExecutionResultTab extends RunnerExecutionResultBase { + type: 'tab'; + content: defaultTypes.Tab; +} + +interface RunnerExecutionResultTable extends RunnerExecutionResultBase { + type: 'table'; + content: defaultTypes.Table; +} + +interface RunnerExecutionResultProfiling extends RunnerExecutionResultBase { + type: 'profiling'; + content: defaultTypes.Profiling; +} + +export interface RunnerExecutionResultMessage extends FromExtensionCommandMessage { + command: 'runnerExecutionResult'; + value: RunnerExecutionResultTab | RunnerExecutionResultTable | RunnerExecutionResultProfiling; +} + +export interface CancelRunnerExecutionMessage extends FromExtensionCommandMessage { + command: 'cancelRunnerExecution'; + value: defaultTypes.HistoryEntry; +} + +export type FromExtensionMessage = + | FromExtensionSetStateMessage + | FromExtensionSetProfilingMessage + | RunnerExecutionResultMessage + | CancelRunnerExecutionMessage; diff --git a/packages/safe-ds-eda/types/state.d.ts b/packages/safe-ds-eda/types/state.d.ts deleted file mode 100644 index 4c42b8907..000000000 --- a/packages/safe-ds-eda/types/state.d.ts +++ /dev/null @@ -1,206 +0,0 @@ -export interface State { - tableIdentifier?: string; - table?: Table; - tabs?: Tab[]; - defaultState?: boolean; - history: HistoryEntry[]; - settings?: UserSettings; -} - -export interface HistoryEntry { - alias?: string; - action: string; - executedSdsCode: string; -} - -// ------------------ Types for the Tabs ------------------ -type TabType = 'linePlot' | 'barPlot' | 'heatmap' | 'scatterPlot' | 'infoPanel'; - -export interface TabObject { - type: TabType; - tabComment: string; - content: Object; -} - -export interface DefaultPlotTab extends TabObject { - content: { - xAxis: string; - yAxis: string; - outdated: boolean; - encodedImage: string; - }; -} -export interface LinePlotTab extends DefaultPlotTab { - type: 'linePlot'; -} - -export interface BarPlotTab extends DefaultPlotTab { - type: 'barPlot'; -} -export interface ScatterPlotTab extends DefaultPlotTab { - type: 'scatterPlot'; -} - -export interface HeatmapTab extends TabObject { - type: 'heatmap'; - content: { - outdated: boolean; - encodedImage: string; - }; -} - -export interface InfoPanelTab extends TabObject { - type: 'infoPanel'; - content: { - correlations: { columnName: string; correlation: number }[]; - outdated: boolean; - statistics: { statName: string; statValue: number }[]; - }; -} - -export type Tab = LinePlotTab | BarPlotTab | HeatmapTab | ScatterPlotTab | InfoPanelTab; - -// ------------------ Types for the Table ------------------ -export interface Table { - columns: [number, Column][]; - visibleRows?: number; - totalRows: number; - name: string; - appliedFilters: TableFilter[]; -} - -// ------------ Types for the Profiling ----------- -export interface Profiling { - validRatio: ProfilingDetailStatistical; - missingRatio: ProfilingDetailStatistical; - other: ProfilingDetail[]; -} - -export interface ProfilingDetailBase { - type: 'numerical' | 'image' | 'name'; - value: string; -} - -interface ProfilingDetailText extends ProfilingDetailBase { - interpretation: 'warn' | 'error' | 'default' | 'important' | 'good'; -} - -export interface ProfilingDetailStatistical extends ProfilingDetailText { - type: 'numerical'; - name: string; - value: string; - interpretation: ProfilingDetailText['interpretation'] | 'category'; // 'category' needed for filters, to show distinct values -} - -export interface ProfilingDetailImage extends ProfilingDetailBase { - type: 'image'; - value: Base64Image; -} - -export interface ProfilingDetailName extends ProfilingDetailText { - type: 'text'; - value: string; -} - -export type ProfilingDetail = ProfilingDetailStatistical | ProfilingDetailImage | ProfilingDetailName; - -// ------------ Types for the Columns ----------- -export interface ColumnBase { - type: 'numerical' | 'categorical'; - name: string; - values: any; - hidden: boolean; - highlighted: boolean; - appliedSort: 'asc' | 'desc' | null; - profiling?: Profiling; -} - -export interface NumericalColumn extends ColumnBase { - type: 'numerical'; - appliedFilters: NumericalFilter[]; - coloredHighLow: boolean; -} - -export interface CategoricalColumn extends ColumnBase { - type: 'categorical'; - appliedFilters: CategoricalFilter[]; -} - -export type Column = NumericalColumn | CategoricalColumn; - -// ------------ Types for the Filters ----------- -export interface FilterBase { - type: string; -} - -export interface ColumnFilterBase extends FilterBase { - type: 'valueRange' | 'specificValue' | 'searchString'; - columnName: string; -} - -export interface PossibleSearchStringFilter extends ColumnFilterBase { - type: 'searchString'; -} - -export interface SearchStringFilter extends PossibleSearchStringFilter { - searchString: string; -} - -export interface PossibleValueRangeFilter extends ColumnFilterBase { - type: 'valueRange'; - min: number; - max: number; -} - -export interface ValueRangeFilter extends PossibleValueRangeFilter { - currentMin: number; - currentMax: number; -} - -export interface PossibleSpecificValueFilter extends ColumnFilterBase { - type: 'specificValue'; - values: string[]; -} - -export interface SpecificValueFilter extends ColumnFilterBase { - type: 'specificValue'; - value: string; -} - -export type NumericalFilter = ValueRangeFilter; -export type CategoricalFilter = SearchStringFilter | SpecificValueFilter; - -export type PossibleColumnFilter = PossibleValueRangeFilter | PossibleSearchStringFilter | PossibleSpecificValueFilter; - -export interface TableFilter extends FilterBase { - type: 'hideMissingValueColumns' | 'hideNonNumericalColumns' | 'hideDuplicateRows' | 'hideRowsWithOutliers'; -} - -// ------------ Types for the Settings ----------- -export interface UserSettings { - profiling: ProfilingSettings; -} - -export interface ProfilingSettingsBase { - [key: string]: boolean; -} - -export interface ProfilingSettings extends ProfilingSettingsBase { - idNess: boolean; - maximum: boolean; - minimum: boolean; - mean: boolean; - median: boolean; - mode: boolean; - stability: boolean; - standardDeviation: boolean; - sum: boolean; - variance: boolean; -} - -// ------------ Types for general objects ----------- - -export interface Base64Image { - format: string; - bytes: string; -} diff --git a/packages/safe-ds-eda/types/state.ts b/packages/safe-ds-eda/types/state.ts new file mode 100644 index 000000000..c61cc4f6b --- /dev/null +++ b/packages/safe-ds-eda/types/state.ts @@ -0,0 +1,348 @@ +export interface State { + tableIdentifier?: string; + table?: Table; + tabs: Tab[]; + defaultState?: boolean; + history: HistoryEntry[]; + settings?: UserSettings; +} + +type InternalAction = 'reorderColumns' | 'resizeColumn' | 'hideColumn' | 'highlightColumn' | 'emptyTab'; +type ExternalManipulatingAction = 'filterColumn' | 'sortColumn' | TableFilterTypes; +type ExternalVisualizingAction = TabType | 'refreshTab'; +type Action = InternalAction | ExternalManipulatingAction | ExternalVisualizingAction; + +interface HistoryEntryBase { + type: 'internal' | 'external-manipulating' | 'external-visualizing'; + alias: string; + action: Action; +} + +export interface InternalHistoryEntryBase extends HistoryEntryBase { + type: 'internal'; + action: InternalAction; +} + +interface ExternalManipulatingHistoryEntryBase extends HistoryEntryBase { + type: 'external-manipulating'; + action: ExternalManipulatingAction; +} + +interface ExternalVisualizingHistoryEntryBase extends HistoryEntryBase { + type: 'external-visualizing'; + action: ExternalVisualizingAction; + columnNumber: 'one' | 'two' | 'none'; + existingTabId?: string; +} + +export interface InternalColumnWithValueHistoryEntry extends InternalHistoryEntryBase { + action: 'reorderColumns' | 'resizeColumn'; + columnName: string; + value: number; +} + +export interface InternalColumnHistoryEntry extends InternalHistoryEntryBase { + action: 'hideColumn' | 'highlightColumn'; + columnName: string; +} + +export interface InteralEmptyTabHistoryEntry extends InternalHistoryEntryBase { + action: 'emptyTab'; +} + +export interface ExternalManipulatingColumnFilterHistoryEntry extends ExternalManipulatingHistoryEntryBase { + action: 'filterColumn'; + columnName: string; + filter: NumericalFilter | CategoricalFilter; +} + +export interface ExternalManipulatingTableFilterHistoryEntry extends ExternalManipulatingHistoryEntryBase { + action: TableFilterTypes; +} + +export interface ExternalManipulatingColumnSortHistoryEntry extends ExternalManipulatingHistoryEntryBase { + action: 'sortColumn'; + columnName: string; + sort: PossibleSorts; +} + +export interface ExternalVisualizingNoColumnHistoryEntry extends ExternalVisualizingHistoryEntryBase { + action: NoColumnTabTypes; + columnNumber: 'none'; +} + +export interface ExternalVisualizingOneColumnHistoryEntry extends ExternalVisualizingHistoryEntryBase { + action: OneColumnTabTypes; + columnName: string; + columnNumber: 'one'; +} + +export interface ExternalVisualizingTwoColumnHistoryEntry extends ExternalVisualizingHistoryEntryBase { + action: TwoColumnTabTypes; + xAxisColumnName: string; + yAxisColumnName: string; + columnNumber: 'two'; +} + +export interface ExternalVisualizingRefreshHistoryEntry extends ExternalVisualizingHistoryEntryBase { + action: 'refreshTab'; +} + +export type TabHistoryEntry = + | ExternalVisualizingNoColumnHistoryEntry + | ExternalVisualizingOneColumnHistoryEntry + | ExternalVisualizingTwoColumnHistoryEntry; +export type InternalHistoryEntry = + | InternalColumnWithValueHistoryEntry + | InternalColumnHistoryEntry + | InteralEmptyTabHistoryEntry; +export type ExternalManipulatingHistoryEntry = + | ExternalManipulatingColumnFilterHistoryEntry + | ExternalManipulatingTableFilterHistoryEntry + | ExternalManipulatingColumnSortHistoryEntry; +export type ExternalVisualizingHistoryEntry = + | ExternalVisualizingNoColumnHistoryEntry + | ExternalVisualizingOneColumnHistoryEntry + | ExternalVisualizingTwoColumnHistoryEntry + | ExternalVisualizingRefreshHistoryEntry; + +export type ExternalHistoryEntry = ExternalManipulatingHistoryEntry | ExternalVisualizingHistoryEntry; + +export type HistoryEntry = (InternalHistoryEntry | ExternalHistoryEntry) & { + id: number; +}; + +// ------------------ Types for the Tabs ------------------ +export type TwoColumnTabTypes = 'linePlot' | 'scatterPlot'; +export type OneColumnTabTypes = 'histogram' | 'boxPlot' | 'infoPanel'; +export type NoColumnTabTypes = 'heatmap'; +type TabType = TwoColumnTabTypes | OneColumnTabTypes | NoColumnTabTypes; + +interface TabObject { + id?: string; + type: TabType; + tabComment: string; + content: Object; + imageTab: boolean; + columnNumber: 'one' | 'two' | 'none'; + isInGeneration: boolean; +} + +interface ImageTabObject extends TabObject { + imageTab: true; + content: { + outdated: boolean; + encodedImage: Base64Image; + }; +} + +interface OneColumnTabContent { + columnName: string; + outdated: boolean; + encodedImage: Base64Image; +} + +export interface OneColumnTab extends ImageTabObject { + type: Exclude; + columnNumber: 'one'; + content: OneColumnTabContent; +} + +interface InfoPanelTabContent { + correlations: { columnName: string; correlation: number }[]; + outdated: boolean; + statistics: { statName: string; statValue: number }[]; +} + +export interface InfoPanelTab extends TabObject { + imageTab: false; + type: 'infoPanel'; + columnNumber: 'none'; + content: InfoPanelTabContent; +} + +interface TwoColumnTabContent { + xAxisColumnName: string; + yAxisColumnName: string; + outdated: boolean; + encodedImage: Base64Image; +} + +export interface TwoColumnTab extends ImageTabObject { + type: TwoColumnTabTypes; + columnNumber: 'two'; + content: TwoColumnTabContent; +} + +interface NoColumnTabContent { + outdated: boolean; + encodedImage: Base64Image; +} + +export interface NoColumnTab extends ImageTabObject { + type: NoColumnTabTypes; + columnNumber: 'none'; + content: NoColumnTabContent; +} + +export interface EmptyTab { + type: 'empty'; + id: string; + isInGeneration: true; +} + +export type Tab = OneColumnTab | InfoPanelTab | TwoColumnTab | NoColumnTab | EmptyTab; +export type RealTab = Exclude; +export type PlotTab = OneColumnTab | TwoColumnTab | NoColumnTab; + +// ------------------ Types for the Table ------------------ +export interface Table { + columns: [number, Column][]; + visibleRows?: number; + totalRows: number; + name: string; + appliedFilters: TableFilter[]; +} + +// ------------ Types for the Profiling ----------- +export interface Profiling { + validRatio: ProfilingDetailStatistical; + missingRatio: ProfilingDetailStatistical; + other: ProfilingDetail[]; +} + +type BaseInterpretation = 'warn' | 'error' | 'default' | 'important' | 'good'; + +interface ProfilingDetailBase { + type: 'numerical' | 'image' | 'text'; + value: string | Base64Image; +} + +export interface ProfilingDetailStatistical extends ProfilingDetailBase { + type: 'numerical'; + name: string; + value: string; + interpretation: BaseInterpretation | 'category'; // 'category' needed for filters, to show distinct values +} + +export interface ProfilingDetailImage extends ProfilingDetailBase { + type: 'image'; + value: Base64Image; +} + +export interface ProfilingDetailName extends ProfilingDetailBase { + type: 'text'; + value: string; + interpretation: BaseInterpretation; +} + +export type ProfilingDetail = ProfilingDetailStatistical | ProfilingDetailImage | ProfilingDetailName; + +// ------------ Types for the Columns ----------- +type PossibleSorts = 'asc' | 'desc' | null; + +interface ColumnBase { + type: 'numerical' | 'categorical'; + name: string; + values: any; + hidden: boolean; + highlighted: boolean; + appliedSort: PossibleSorts; + profiling?: Profiling; +} + +export interface NumericalColumn extends ColumnBase { + type: 'numerical'; + appliedFilters: NumericalFilter[]; + coloredHighLow: boolean; +} + +export interface CategoricalColumn extends ColumnBase { + type: 'categorical'; + appliedFilters: CategoricalFilter[]; +} + +export type Column = NumericalColumn | CategoricalColumn; + +// ------------ Types for the Filters ----------- +interface FilterBase { + type: string; +} + +interface ColumnFilterBase extends FilterBase { + type: 'valueRange' | 'specificValue' | 'searchString'; + columnName: string; +} + +export interface PossibleSearchStringFilter extends ColumnFilterBase { + type: 'searchString'; +} + +export interface SearchStringFilter extends PossibleSearchStringFilter { + searchString: string; +} + +export interface PossibleValueRangeFilter extends ColumnFilterBase { + type: 'valueRange'; + min: number; + max: number; +} + +export interface ValueRangeFilter extends PossibleValueRangeFilter { + currentMin: number; + currentMax: number; +} + +export interface PossibleSpecificValueFilter extends ColumnFilterBase { + type: 'specificValue'; + values: string[]; +} + +export interface SpecificValueFilter extends ColumnFilterBase { + type: 'specificValue'; + value: string; +} + +export type NumericalFilter = ValueRangeFilter; +export type CategoricalFilter = SearchStringFilter | SpecificValueFilter; + +export type PossibleColumnFilter = PossibleValueRangeFilter | PossibleSearchStringFilter | PossibleSpecificValueFilter; + +type TableFilterTypes = + | 'hideMissingValueColumns' + | 'hideNonNumericalColumns' + | 'hideDuplicateRows' + | 'hideRowsWithOutliers'; + +export interface TableFilter extends FilterBase { + type: TableFilterTypes; +} + +// ------------ Types for the Settings ----------- +export interface UserSettings { + profiling: ProfilingSettings; +} + +interface ProfilingSettingsBase { + [key: string]: boolean; +} + +export interface ProfilingSettings extends ProfilingSettingsBase { + idNess: boolean; + maximum: boolean; + minimum: boolean; + mean: boolean; + median: boolean; + mode: boolean; + stability: boolean; + standardDeviation: boolean; + sum: boolean; + variance: boolean; +} + +// ------------ Types for general objects ----------- + +export interface Base64Image { + format: string; + bytes: string; +} diff --git a/packages/safe-ds-vscode/media/styles.css b/packages/safe-ds-vscode/media/styles.css index 61b357140..bb20002ed 100644 --- a/packages/safe-ds-vscode/media/styles.css +++ b/packages/safe-ds-vscode/media/styles.css @@ -5,6 +5,7 @@ --warn-color: #a08b14; --bg-bright: white; --bg-dark: #f2f2f2; + --bg-most-dark: #ccc; --bg-medium: #f9f9f9; --font-dark: #292929; --font-light: #6d6d6d; @@ -31,3 +32,18 @@ ::-webkit-scrollbar-corner { background-color: var(--bg-dark); } + +button { + background: none; + border: none; + cursor: pointer; + color: var(--font-dark); +} + +button:focus { + outline-color: transparent !important; +} + +button:hover { + background: none; +} diff --git a/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts b/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts index a15ac4dd7..93d0d2f72 100644 --- a/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts +++ b/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts @@ -1,16 +1,26 @@ -import { Base64Image, Column, Profiling, ProfilingDetailStatistical, Table } from '@safe-ds/eda/types/state.js'; +import { + Base64Image, + Column, + ExternalHistoryEntry, + HistoryEntry, + Profiling, + ProfilingDetailStatistical, + Table, +} from '@safe-ds/eda/types/state.js'; import { ast, CODEGEN_PREFIX, messages, SafeDsServices } from '@safe-ds/lang'; import { LangiumDocument } from 'langium'; import * as vscode from 'vscode'; import crypto from 'crypto'; import { getPipelineDocument } from '../../mainClient.ts'; import { safeDsLogger } from '../../helpers/logging.js'; +import { RunnerExecutionResultMessage } from '@safe-ds/eda/types/messaging.ts'; export class RunnerApi { services: SafeDsServices; pipelinePath: vscode.Uri; pipelineName: string; pipelineNode: ast.SdsPipeline; + tablePlaceholder: string; baseDocument: LangiumDocument | undefined; placeholderCounter = 0; @@ -19,11 +29,13 @@ export class RunnerApi { pipelinePath: vscode.Uri, pipelineName: string, pipelineNode: ast.SdsPipeline, + tablePlaceholder: string, ) { this.services = services; this.pipelinePath = pipelinePath; this.pipelineName = pipelineName; this.pipelineNode = pipelineNode; + this.tablePlaceholder = tablePlaceholder; getPipelineDocument(this.pipelinePath).then((doc) => { // Get here to avoid issues because of chanigng file // Make sure to create new instance of RunnerApi if pipeline execution of fresh pipeline is needed @@ -32,7 +44,12 @@ export class RunnerApi { }); } - private async addToAndExecutePipeline(pipelineExecutionId: string, addedLines: string): Promise { + //#region Pipeline execution + private async addToAndExecutePipeline( + pipelineExecutionId: string, + addedLines: string, + placeholderNames?: string[], + ): Promise { return new Promise(async (resolve, reject) => { if (!this.baseDocument) { reject('Document not found'); @@ -47,18 +64,27 @@ export class RunnerApi { return; } + let newDocumentText; + + this.services.shared.workspace.LangiumDocuments.deleteDocument(this.pipelinePath); + const beforePipelineEnd = documentText.substring(0, endOfPipeline - 1); const afterPipelineEnd = documentText.substring(endOfPipeline - 1); - const newDocumentText = beforePipelineEnd + addedLines + afterPipelineEnd; + newDocumentText = beforePipelineEnd + addedLines + afterPipelineEnd; - this.services.shared.workspace.LangiumDocuments.deleteDocument(this.pipelinePath); - const newDoc = this.services.shared.workspace.LangiumDocuments.createDocument( - this.pipelinePath, + const newDoc = this.services.shared.workspace.LangiumDocumentFactory.fromString( newDocumentText, + this.pipelinePath, ); await this.services.shared.workspace.DocumentBuilder.build([newDoc]); - await this.services.runtime.Runner.executePipeline(pipelineExecutionId, newDoc, this.pipelineName); + safeDsLogger.debug(`Executing pipeline ${this.pipelineName} with added lines`); + await this.services.runtime.Runner.executePipeline( + pipelineExecutionId, + newDoc, + this.pipelineName, + placeholderNames, + ); this.services.shared.workspace.LangiumDocuments.deleteDocument(this.pipelinePath); this.services.shared.workspace.LangiumDocuments.addDocument(this.baseDocument); @@ -68,6 +94,7 @@ export class RunnerApi { return; } if (message.data === 'done') { + safeDsLogger.debug(`Pipeline execution ${this.pipelineName} done`); this.services.runtime.PythonServer.removeMessageCallback('runtime_progress', runtimeCallback); this.services.runtime.PythonServer.removeMessageCallback('runtime_error', errorCallback); resolve(); @@ -77,6 +104,7 @@ export class RunnerApi { if (message.id !== pipelineExecutionId) { return; } + safeDsLogger.error(`Pipeline execution ${this.pipelineName} ran into error: ${message.data}`); this.services.runtime.PythonServer.removeMessageCallback('runtime_progress', runtimeCallback); this.services.runtime.PythonServer.removeMessageCallback('runtime_error', errorCallback); reject(message.data); @@ -89,8 +117,62 @@ export class RunnerApi { }, 3000000); }); } - - // --- SDS code generation --- + //#endregion + + //#region SDS code generation + private sdsStringForHistoryEntry(historyEntry: ExternalHistoryEntry): { + sdsString: string; + placeholderNames: string[]; + } { + const newPlaceholderName = this.genPlaceholderName(); + switch (historyEntry.action) { + case 'histogram': + return { + sdsString: this.sdsStringForHistogramByColumnName( + historyEntry.columnName, + this.tablePlaceholder, + newPlaceholderName, + ), + placeholderNames: [newPlaceholderName], + }; + case 'boxPlot': + return { + sdsString: this.sdsStringForBoxplotByColumnName( + historyEntry.columnName, + this.tablePlaceholder, + newPlaceholderName, + ), + placeholderNames: [newPlaceholderName], + }; + case 'linePlot': + return { + sdsString: this.sdsStringForLinePlotByColumnNames( + historyEntry.xAxisColumnName, + historyEntry.yAxisColumnName, + this.tablePlaceholder, + newPlaceholderName, + ), + placeholderNames: [newPlaceholderName], + }; + case 'scatterPlot': + return { + sdsString: this.sdsStringForScatterPlotByColumnNames( + historyEntry.xAxisColumnName, + historyEntry.yAxisColumnName, + this.tablePlaceholder, + newPlaceholderName, + ), + placeholderNames: [newPlaceholderName], + }; + case 'heatmap': + return { + sdsString: this.sdsStringForCorrelationHeatmap(this.tablePlaceholder, newPlaceholderName), + placeholderNames: [newPlaceholderName], + }; + default: + throw new Error('Unknown history entry action: ' + historyEntry.action); + } + } private sdsStringForMissingValueRatioByColumnName( columnName: string, @@ -128,10 +210,66 @@ export class RunnerApi { ); } - // --- Placeholder handling --- + private sdsStringForBoxplotByColumnName(columnName: string, tablePlaceholder: string, newPlaceholderName: string) { + return ( + 'val ' + + newPlaceholderName + + ' = ' + + tablePlaceholder + + '.getColumn("' + + columnName + + '").plotBoxplot(); \n' + ); + } - private genPlaceholderName(): string { - return CODEGEN_PREFIX + this.placeholderCounter++; + private sdsStringForLinePlotByColumnNames( + xAxisColumnName: string, + yAxisColumnName: string, + tablePlaceholder: string, + newPlaceholderName: string, + ) { + return ( + 'val ' + + newPlaceholderName + + ' = ' + + tablePlaceholder + + '.plotLineplot(xColumnName="' + + xAxisColumnName + + '", yColumnName="' + + yAxisColumnName + + '"); \n' + ); + } + + private sdsStringForScatterPlotByColumnNames( + xAxisColumnName: string, + yAxisColumnName: string, + tablePlaceholder: string, + newPlaceholderName: string, + ) { + return ( + 'val ' + + newPlaceholderName + + ' = ' + + tablePlaceholder + + '.plotScatterplot(xColumnName="' + + xAxisColumnName + + '", yColumnName="' + + yAxisColumnName + + '"); \n' + ); + } + + private sdsStringForCorrelationHeatmap(tablePlaceholder: string, newPlaceholderName: string) { + return 'val ' + newPlaceholderName + ' = ' + tablePlaceholder + '.plotCorrelationHeatmap(); \n'; + } + //#endregion + + //#region Placeholder handling + private genPlaceholderName(suffix?: string): string { + // Filter out non-alphanumeric characters (allowing underscores), considering Unicode characters + const cleanedSuffix = suffix ? suffix.replace(/[^a-zA-Z0-9_]/gu, '') : undefined; + return CODEGEN_PREFIX + this.placeholderCounter++ + (cleanedSuffix ? '_' + cleanedSuffix : ''); } private async getPlaceholderValue(placeholder: string, pipelineExecutionId: string): Promise { @@ -149,7 +287,8 @@ export class RunnerApi { }; this.services.runtime.PythonServer.addMessageCallback('placeholder_value', placeholderValueCallback); - safeDsLogger.info('Getting placeholder from Runner ...'); + + safeDsLogger.debug('Requesting placeholder: ' + placeholder); this.services.runtime.PythonServer.sendMessageToPythonServer( messages.createPlaceholderQueryMessage(pipelineExecutionId, placeholder), ); @@ -160,9 +299,11 @@ export class RunnerApi { }); } - // --- Public API --- + //#region Public API + //#region Table fetching public async getTableByPlaceholder(tableName: string, pipelineExecutionId: string): Promise { + safeDsLogger.debug('Getting table by placeholder: ' + tableName); const pythonTableColumns = await this.getPlaceholderValue(tableName, pipelineExecutionId); if (pythonTableColumns) { const table: Table = { @@ -205,24 +346,30 @@ export class RunnerApi { return undefined; } } + //#endregion + //#region Profiling public async getProfiling(table: Table): Promise<{ columnName: string; profiling: Profiling }[]> { + safeDsLogger.debug('Getting profiling for table: ' + table.name); + const columns = table.columns; let sdsStrings = ''; + let placeholderNames: string[] = []; + const columnNameToPlaceholderMVNameMap = new Map(); // Mapping random placeholder name for missing value ratio back to column name const missingValueRatioMap = new Map(); // Saved by random placeholder name - const columnNameToPlaceholderIDnessNameMap = new Map(); // Mapping random placeholder name for IDness back to column name - const idnessMap = new Map(); // Saved by random placeholder name - const columnNameToPlaceholderHistogramNameMap = new Map(); // Mapping random placeholder name for histogram back to column name const histogramMap = new Map(); // Saved by random placeholder name + const uniqueValuesMap = new Map>(); + // Generate SDS code to get missing value ratio for each column - outer: for (const column of columns) { - const newMvPlaceholderName = this.genPlaceholderName(); + for (const column of columns) { + const newMvPlaceholderName = this.genPlaceholderName(column[1].name + '_mv'); + placeholderNames.push(newMvPlaceholderName); columnNameToPlaceholderMVNameMap.set(column[1].name, newMvPlaceholderName); sdsStrings += this.sdsStringForMissingValueRatioByColumnName( column[1].name, @@ -230,29 +377,31 @@ export class RunnerApi { newMvPlaceholderName, ); - // Only need to check IDness for non-numerical columns + // Find unique values + // TODO reevaluate when image stuck problem fixed + let uniqueValues = new Set(); + for (let j = 0; j < column[1].values.length; j++) { + uniqueValues.add(column[1].values[j]); + } + uniqueValuesMap.set(column[1].name, uniqueValues); + + // Different histogram conditions for numerical and categorical columns if (column[1].type !== 'numerical') { - const newIDnessPlaceholderName = this.genPlaceholderName(); - columnNameToPlaceholderIDnessNameMap.set(column[1].name, newIDnessPlaceholderName); - sdsStrings += this.sdsStringForIDnessByColumnName(column[1].name, table.name, newIDnessPlaceholderName); - - // Find unique values - // TODO reevaluate when image stuck problem fixed - let uniqueValues = new Set(); - for (let j = 0; j < column[1].values.length; j++) { - uniqueValues.add(column[1].values[j]); - if (uniqueValues.size > 10) { - continue outer; - } - } if (uniqueValues.size <= 3 || uniqueValues.size > 10) { - // Must match conidtions below that choose to display histogram + // Must match conidtions below that choose to display histogram for categorical columns continue; // This historam only generated if between 4-10 categorigal uniques or numerical type } + } else { + if (uniqueValues.size > column[1].values.length * 0.9) { + // Must match conidtions below that choose to display histogram for numerical columns + // If 90% of values are unique, it's not a good idea to display histogram + continue; + } } // Histogram for numerical columns or categorical columns with 4-10 unique values - const newHistogramPlaceholderName = this.genPlaceholderName(); + const newHistogramPlaceholderName = this.genPlaceholderName(column[1].name + '_hist'); + placeholderNames.push(newHistogramPlaceholderName); columnNameToPlaceholderHistogramNameMap.set(column[1].name, newHistogramPlaceholderName); sdsStrings += this.sdsStringForHistogramByColumnName( column[1].name, @@ -264,9 +413,8 @@ export class RunnerApi { // Execute with generated SDS code const pipelineExecutionId = crypto.randomUUID(); try { - await this.addToAndExecutePipeline(pipelineExecutionId, sdsStrings); + await this.addToAndExecutePipeline(pipelineExecutionId, sdsStrings, placeholderNames); } catch (e) { - safeDsLogger.info('Error during pipeline execution: ' + e); throw e; } @@ -278,14 +426,6 @@ export class RunnerApi { } } - // Get IDness for each column - for (const [, placeholderName] of columnNameToPlaceholderIDnessNameMap) { - const idness = await this.getPlaceholderValue(placeholderName, pipelineExecutionId); - if (idness) { - idnessMap.set(placeholderName, idness as number); - } - } - // Get histogram for each column for (const [, placeholderName] of columnNameToPlaceholderHistogramNameMap) { const histogram = await this.getPlaceholderValue(placeholderName, pipelineExecutionId); @@ -315,17 +455,15 @@ export class RunnerApi { interpretation: missingValuesRatio > 0 ? 'error' : 'default', }; + const uniqueValues = uniqueValuesMap.get(column[1].name)!.size; // If not numerical, add proper profilings according to idness results if (column[1].type !== 'numerical') { - const idness = idnessMap.get(columnNameToPlaceholderIDnessNameMap.get(column[1].name)!)!; - const uniqueValues = idness * column[1].values.length; - if (uniqueValues <= 3) { // Can display each separate percentages of unique values // Find all unique values and count them const uniqueValueCounts = new Map(); for (let i = 0; i < column[1].values.length; i++) { - if (column[1].values[i]) + if (column[1].values[i] !== undefined && column[1].values[i] !== null) uniqueValueCounts.set( column[1].values[i], (uniqueValueCounts.get(column[1].values[i]) || 0) + 1, @@ -399,23 +537,155 @@ export class RunnerApi { }); } } else { - // Display histogram for numerical columns - const histogram = histogramMap.get(columnNameToPlaceholderHistogramNameMap.get(column[1].name)!)!; - - profiling.push({ - columnName: column[1].name, - profiling: { - validRatio, - missingRatio, - other: [ - { type: 'text', value: 'Numerical', interpretation: 'important' }, - { type: 'image', value: histogram }, - ], - }, - }); + if (uniqueValues > column[1].values.length * 0.9) { + profiling.push({ + columnName: column[1].name, + profiling: { + validRatio, + missingRatio, + other: [ + { type: 'text', value: 'Categorical', interpretation: 'important' }, + { + type: 'text', + value: uniqueValues + ' Distincts', + interpretation: 'default', + }, + { + type: 'text', + value: + Math.round( + column[1].values.length * + (1 - + (missingValueRatioMap.get( + columnNameToPlaceholderMVNameMap.get(column[1].name)!, + ) || 0)), + ) + ' Total Valids', + interpretation: 'default', + }, + ], + }, + }); + } else { + const histogram = histogramMap.get(columnNameToPlaceholderHistogramNameMap.get(column[1].name)!)!; + + profiling.push({ + columnName: column[1].name, + profiling: { + validRatio, + missingRatio, + other: [ + { type: 'text', value: 'Numerical', interpretation: 'important' }, + { type: 'image', value: histogram }, + ], + }, + }); + } } } return profiling; } + //#endregion + + //#region History + public async executeHistoryAndReturnNewResult( + pastEntries: HistoryEntry[], + newEntry: HistoryEntry, + ): Promise { + let sdsLines = ''; + let placeholderNames: string[] = []; + for (const entry of pastEntries) { + if (entry.type === 'external-manipulating') { + // Only manipulating actions have to be repeated before last entry that is of interest, others do not influence that end result + const sdsString = this.sdsStringForHistoryEntry(entry).sdsString; + if (sdsString) { + sdsLines += sdsString + '\n'; + } + safeDsLogger.debug(`Running old entry ${entry.id} with action ${entry.action}`); + } + } + + if (newEntry.type === 'external-visualizing') { + if (newEntry.action === 'infoPanel' || newEntry.action === 'refreshTab') throw new Error('Not implemented'); + + const sdsStringObj = this.sdsStringForHistoryEntry(newEntry); + sdsLines += sdsStringObj.sdsString + '\n'; + placeholderNames = sdsStringObj.placeholderNames; + + safeDsLogger.debug(`Running new entry ${newEntry.id} with action ${newEntry.action}`); + } else if (newEntry.type === 'external-manipulating') { + throw new Error('Not implemented'); + } else if (newEntry.type === 'internal') { + throw new Error('Cannot execute internal history entry in Runner'); + } + + const pipelineExecutionId = crypto.randomUUID(); + try { + await this.addToAndExecutePipeline(pipelineExecutionId, sdsLines, placeholderNames); + } catch (e) { + throw e; + } + + if ( + newEntry.type === 'external-visualizing' && + newEntry.action !== 'infoPanel' && + placeholderNames.length > 0 + ) { + const result = await this.getPlaceholderValue(placeholderNames[0]!, pipelineExecutionId); + const image = result as Base64Image; + + if (newEntry.columnNumber === 'none') { + return { + type: 'tab', + historyId: newEntry.id, + content: { + tabComment: '', + type: newEntry.action, + columnNumber: newEntry.columnNumber, + imageTab: true, + isInGeneration: false, + id: newEntry.existingTabId, + content: { outdated: false, encodedImage: image }, + }, + }; + } else if (newEntry.columnNumber === 'two') { + return { + type: 'tab', + historyId: newEntry.id, + content: { + tabComment: newEntry.xAxisColumnName + ' x ' + newEntry.yAxisColumnName, + type: newEntry.action, + columnNumber: newEntry.columnNumber, + imageTab: true, + isInGeneration: false, + id: newEntry.existingTabId, + content: { + outdated: false, + encodedImage: image, + xAxisColumnName: newEntry.xAxisColumnName, + yAxisColumnName: newEntry.yAxisColumnName, + }, + }, + }; + } else { + return { + type: 'tab', + historyId: newEntry.id, + content: { + tabComment: newEntry.columnName, + type: newEntry.action, + columnNumber: newEntry.columnNumber, + imageTab: true, + isInGeneration: false, + id: newEntry.existingTabId, + content: { outdated: false, encodedImage: image, columnName: newEntry.columnName }, + }, + }; + } + } else { + throw new Error('Not implemented'); + } + } + //#endregion + //#endregion // Public API } diff --git a/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts b/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts index 1ca5847ab..bb7ee5ecd 100644 --- a/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts +++ b/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts @@ -1,8 +1,8 @@ import * as vscode from 'vscode'; import { ToExtensionMessage } from '@safe-ds/eda/types/messaging.js'; import * as webviewApi from './apis/webviewApi.ts'; -import { State } from '@safe-ds/eda/types/state.js'; -import { ast, SafeDsServices } from '@safe-ds/lang'; +import { State } from '@safe-ds/eda/types/state.ts'; +import { SafeDsServices, ast } from '@safe-ds/lang'; import { RunnerApi } from './apis/runnerApi.ts'; import { safeDsLogger } from '../helpers/logging.js'; @@ -26,6 +26,7 @@ export class EDAPanel { private startPipelineExecutionId: string; private runnerApi: RunnerApi; + //#region Creation private constructor( panel: vscode.WebviewPanel, extensionUri: vscode.Uri, @@ -39,7 +40,7 @@ export class EDAPanel { this.panel = panel; this.extensionUri = extensionUri; this.startPipelineExecutionId = startPipelineExecutionId; - this.runnerApi = new RunnerApi(EDAPanel.services, pipelinePath, pipelineName, pipelineNode); + this.runnerApi = new RunnerApi(EDAPanel.services, pipelinePath, pipelineName, pipelineNode, tableName); this.tableName = tableName; // Set the webview's initial html content @@ -56,13 +57,14 @@ export class EDAPanel { if (updatedPanel.visible) { this.column = updatedPanel.viewColumn; } + // TODO handle floating panels if at some point possible }); this.disposables.push(this.viewStateChangeListener); // Handle messages from the webview const webview = this.panel.webview; this.webviewListener = webview.onDidReceiveMessage(async (data: ToExtensionMessage) => { - safeDsLogger.info(data.command + ' called'); + safeDsLogger.debug(data.command + ' called'); switch (data.command) { case 'setInfo': { if (!data.value) { @@ -78,24 +80,56 @@ export class EDAPanel { vscode.window.showErrorMessage(data.value); break; } - case 'setCurrentGlobalState': { - // if (!data.value) { - // return; - // } - // const existingStates = (EDAPanel.context.globalState.get('webviewState') ?? []) as State[]; - // const stateExists = existingStates.some((s) => s.tableIdentifier === data.value.tableIdentifier); - - // const newWebviewState = stateExists - // ? (existingStates.map((s) => - // s.tableIdentifier === data.value.tableIdentifier ? data.value : s, - // ) as State[]) - // : existingStates.concat(data.value); - - // EDAPanel.context.globalState.update('webviewState', newWebviewState); - break; - } - case 'resetGlobalState': { - EDAPanel.context.globalState.update('webviewState', []); + case 'executeRunner': { + if (!data.value) { + return; + } + + let alreadyComplete = false; + + // Execute the runner + const resultPromise = this.runnerApi.executeHistoryAndReturnNewResult( + data.value.pastEntries, + data.value.newEntry, + ); + + // Check if execution takes longer than 1s to show progress indicator + setTimeout(() => { + if (!alreadyComplete) { + vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: 'Executing action ...', + cancellable: true, + }, + async (progress, token) => { + token.onCancellationRequested(() => { + if (data.value.newEntry) { + safeDsLogger.info('User canceled execution.'); + webviewApi.postMessage(this.panel.webview, { + command: 'cancelRunnerExecution', + value: data.value.newEntry, + }); + } else { + throw new Error('No history entry to cancel'); + } + }); + + // Wait for the result to finish in case it's still running + await resultPromise; + alreadyComplete = true; // Mark completion to prevent multiple indicators + }, + ); + } + }, 1000); + + const result = await resultPromise; + alreadyComplete = true; + + webviewApi.postMessage(this.panel.webview, { + command: 'runnerExecutionResult', + value: result, + }); break; } } @@ -127,7 +161,7 @@ export class EDAPanel { panel.panel.reveal(panel.column); panel.tableIdentifier = tableIdentifier; panel.startPipelineExecutionId = startPipelineExecutionId; - panel.runnerApi = new RunnerApi(services, pipelinePath, pipelineName, pipelineNode); + panel.runnerApi = new RunnerApi(services, pipelinePath, pipelineName, pipelineNode, tableName); panel.tableName = tableName; EDAPanel.panelsMap.set(tableIdentifier, panel); @@ -191,7 +225,9 @@ export class EDAPanel { } } } + //#endregion + //#region Disposal public static kill(tableIdentifier: string) { safeDsLogger.info('kill ' + tableIdentifier); let panel = EDAPanel.panelsMap.get(tableIdentifier); @@ -202,6 +238,7 @@ export class EDAPanel { } public dispose() { + safeDsLogger.info('dispose ' + this.tableIdentifier); EDAPanel.panelsMap.delete(this.tableIdentifier); // Clean up our panel @@ -215,7 +252,34 @@ export class EDAPanel { } } } + //#endregion + //#region State handling + private async constructCurrentState(): Promise<{ state: State; fromExisting: boolean }> { + const panel = EDAPanel.panelsMap.get(this.tableIdentifier); + if (!panel) { + throw new Error('Panel not found.'); + } else { + const table = await panel.runnerApi.getTableByPlaceholder(this.tableName, this.startPipelineExecutionId); + if (!table) { + throw new Error('Timeout waiting for placeholder value'); + } else { + return { + state: { + tableIdentifier: panel.tableIdentifier, + history: [], + defaultState: false, + table, + tabs: [], + }, + fromExisting: false, + }; + } + } + } + //#endregion + + //#region Html updating private async _update() { const webview = this.panel.webview; this.panel.webview.html = await this._getHtmlForWebview(webview); @@ -239,39 +303,6 @@ export class EDAPanel { }); }; - // private findCurrentState(): State | undefined { - // const existingStates = (EDAPanel.context.globalState.get('webviewState') ?? []) as State[]; - // return existingStates.find((s) => s.tableIdentifier === this.tableIdentifier); - // } - - private async constructCurrentState(): Promise<{ state: State; fromExisting: boolean }> { - // const existingCurrentState = this.findCurrentState(); - // if (existingCurrentState) { - // printOutputMessage('Found current State.'); - // return { state: existingCurrentState, fromExisting: true }; - // } - // - const panel = EDAPanel.panelsMap.get(this.tableIdentifier); - if (!panel) { - throw new Error('RunnerApi panel not found.'); - } else { - const table = await panel.runnerApi.getTableByPlaceholder(this.tableName, this.startPipelineExecutionId); - if (!table) { - throw new Error('Timeout waiting for placeholder value'); - } else { - return { - state: { - tableIdentifier: panel.tableIdentifier, - history: [], - defaultState: false, - table, - }, - fromExisting: false, - }; - } - } - } - private async _getHtmlForWebview(webview: vscode.Webview) { // The uri we use to load this script in the webview let scriptUri; @@ -327,4 +358,5 @@ export class EDAPanel { } return text; } + //#endregion }