feat(editor): Hover actions on the logs overview (#14386)

This commit is contained in:
Suguru Inoue
2025-04-07 10:35:29 +02:00
committed by GitHub
parent 89d2eb7aa3
commit 8f9ea23019
22 changed files with 317 additions and 130 deletions

View File

@@ -175,7 +175,7 @@ describe('CanvasChat', () => {
return matchedNode;
});
workflowsStore.chatPanelState = LOGS_PANEL_STATE.ATTACHED;
workflowsStore.logsPanelState = LOGS_PANEL_STATE.ATTACHED;
workflowsStore.isLogsPanelOpen = true;
workflowsStore.getWorkflowExecution = mockWorkflowExecution as unknown as IExecutionResponse;
workflowsStore.getPastChatMessages = ['Previous message 1', 'Previous message 2'];
@@ -198,7 +198,7 @@ describe('CanvasChat', () => {
});
it('should not render chat when panel is closed', async () => {
workflowsStore.chatPanelState = LOGS_PANEL_STATE.CLOSED;
workflowsStore.logsPanelState = LOGS_PANEL_STATE.CLOSED;
const { queryByTestId } = renderComponent();
await waitFor(() => {
expect(queryByTestId('canvas-chat')).not.toBeInTheDocument();
@@ -390,7 +390,7 @@ describe('CanvasChat', () => {
isLoading: computed(() => false),
});
workflowsStore.chatPanelState = LOGS_PANEL_STATE.ATTACHED;
workflowsStore.logsPanelState = LOGS_PANEL_STATE.ATTACHED;
workflowsStore.allowFileUploads = true;
});
@@ -555,7 +555,7 @@ describe('CanvasChat', () => {
});
// Close chat panel
workflowsStore.chatPanelState = LOGS_PANEL_STATE.CLOSED;
workflowsStore.logsPanelState = LOGS_PANEL_STATE.CLOSED;
await waitFor(() => {
expect(canvasStore.setPanelHeight).toHaveBeenCalledWith(0);
});
@@ -565,14 +565,14 @@ describe('CanvasChat', () => {
const { unmount, rerender } = renderComponent();
// Set initial state
workflowsStore.chatPanelState = LOGS_PANEL_STATE.ATTACHED;
workflowsStore.logsPanelState = LOGS_PANEL_STATE.ATTACHED;
workflowsStore.isLogsPanelOpen = true;
// Unmount and remount
unmount();
await rerender({});
expect(workflowsStore.chatPanelState).toBe(LOGS_PANEL_STATE.ATTACHED);
expect(workflowsStore.logsPanelState).toBe(LOGS_PANEL_STATE.ATTACHED);
expect(workflowsStore.isLogsPanelOpen).toBe(true);
});
});

View File

@@ -29,7 +29,7 @@ const pipContent = useTemplateRef('pipContent');
// Computed properties
const workflow = computed(() => workflowsStore.getCurrentWorkflow());
const chatPanelState = computed(() => workflowsStore.chatPanelState);
const chatPanelState = computed(() => workflowsStore.logsPanelState);
const previousChatMessages = computed(() => workflowsStore.getPastChatMessages);
const resultData = computed(() => workflowsStore.getWorkflowRunData);
@@ -57,7 +57,7 @@ const { canPopOut, isPoppedOut, pipWindow } = usePiPWindow({
}
telemetry.track('User toggled log view', { new_state: 'attached' });
workflowsStore.setPanelState(LOGS_PANEL_STATE.ATTACHED);
workflowsStore.setPreferPoppedOutLogsView(false);
},
});
@@ -80,12 +80,13 @@ defineExpose({
});
const closePanel = () => {
workflowsStore.setPanelState(LOGS_PANEL_STATE.CLOSED);
workflowsStore.toggleLogsPanelOpen(false);
};
function onPopOut() {
telemetry.track('User toggled log view', { new_state: 'floating' });
workflowsStore.setPanelState(LOGS_PANEL_STATE.FLOATING);
workflowsStore.toggleLogsPanelOpen(true);
workflowsStore.setPreferPoppedOutLogsView(true);
}
// Watchers

View File

@@ -36,9 +36,16 @@ export const manualTriggerNode = createTestNode({ name: 'Manual' });
export const aiAgentNode = createTestNode({ name: 'AI Agent', type: AGENT_NODE_TYPE });
export const aiModelNode = createTestNode({ name: 'AI Model' });
export const simpleWorkflow = createTestWorkflow({
nodes: [manualTriggerNode],
connections: {},
export const aiManualWorkflow = createTestWorkflow({
nodes: [manualTriggerNode, aiAgentNode, aiModelNode],
connections: {
Manual: {
main: [[{ node: 'AI Agent', index: 0, type: 'main' }]],
},
'AI Model': {
ai_languageModel: [[{ node: 'AI Agent', index: 0, type: 'ai_languageModel' }]],
},
},
});
export const aiChatWorkflow = createTestWorkflow({
@@ -53,7 +60,7 @@ export const aiChatWorkflow = createTestWorkflow({
},
});
export const executionResponse: IExecutionResponse = {
export const aiChatExecutionResponse: IExecutionResponse = {
id: 'test-exec-id',
finished: true,
mode: 'manual',
@@ -102,3 +109,52 @@ export const executionResponse: IExecutionResponse = {
startedAt: new Date('2025-03-26T00:00:00.001Z'),
stoppedAt: new Date('2025-03-26T00:00:02.000Z'),
};
export const aiManualExecutionResponse: IExecutionResponse = {
id: 'test-exec-id-2',
finished: true,
mode: 'manual',
status: 'success',
data: {
resultData: {
runData: {
'AI Agent': [
{
executionStatus: 'success',
startTime: +new Date('2025-03-30T00:00:00.002Z'),
executionTime: 12,
source: [],
data: {},
},
],
'AI Model': [
{
executionStatus: 'success',
startTime: +new Date('2025-03-30T00:00:00.003Z'),
executionTime: 3456,
source: [],
data: {
ai_languageModel: [
[
{
json: {
tokenUsage: {
completionTokens: 4,
promptTokens: 5,
totalTokens: 6,
},
},
},
],
],
},
},
],
},
},
},
workflowData: aiManualWorkflow,
createdAt: new Date('2025-03-30T00:00:00.000Z'),
startedAt: new Date('2025-03-30T00:00:00.001Z'),
stoppedAt: new Date('2025-03-30T00:00:02.000Z'),
};

View File

@@ -42,7 +42,7 @@ export function useChatState(isDisabled: Ref<boolean>, onWindowResize: () => voi
const canvasNodes = computed(() => workflowsStore.allNodes);
const allConnections = computed(() => workflowsStore.allConnections);
const chatPanelState = computed(() => workflowsStore.chatPanelState);
const logsPanelState = computed(() => workflowsStore.logsPanelState);
const workflow = computed(() => workflowsStore.getCurrentWorkflow());
// Initialize features with injected dependencies
@@ -125,7 +125,7 @@ export function useChatState(isDisabled: Ref<boolean>, onWindowResize: () => voi
// Watchers
watch(
() => chatPanelState.value,
() => logsPanelState.value,
(state) => {
if (state !== LOGS_PANEL_STATE.CLOSED) {
setChatTriggerNode();

View File

@@ -8,7 +8,12 @@ import { setActivePinia } from 'pinia';
import { createRouter, createWebHistory } from 'vue-router';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { h } from 'vue';
import { executionResponse, aiChatWorkflow, simpleWorkflow, nodeTypes } from '../__test__/data';
import {
aiChatExecutionResponse,
aiChatWorkflow,
aiManualWorkflow,
nodeTypes,
} from '../__test__/data';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
describe('LogsPanel', () => {
@@ -47,16 +52,14 @@ describe('LogsPanel', () => {
});
it('should render collapsed panel by default', async () => {
workflowsStore.setWorkflow(simpleWorkflow);
const rendered = render();
expect(await rendered.findByTestId('logs-overview-header')).toBeInTheDocument();
expect(rendered.queryByTestId('logs-overview-empty')).not.toBeInTheDocument();
});
it('should render logs panel only if the workflow has no chat trigger', async () => {
workflowsStore.setWorkflow(simpleWorkflow);
it('should only render logs panel if the workflow has no chat trigger', async () => {
workflowsStore.setWorkflow(aiManualWorkflow);
const rendered = render();
@@ -99,7 +102,7 @@ describe('LogsPanel', () => {
it('should open log details panel when a log entry is clicked in the logs overview panel', async () => {
workflowsStore.setWorkflow(aiChatWorkflow);
workflowsStore.setWorkflowExecutionData(executionResponse);
workflowsStore.setWorkflowExecutionData(aiChatExecutionResponse);
const rendered = render();
@@ -114,7 +117,7 @@ describe('LogsPanel', () => {
it("should show the button to toggle panel in the header of log details panel when it's opened", async () => {
workflowsStore.setWorkflow(aiChatWorkflow);
workflowsStore.setWorkflowExecutionData(executionResponse);
workflowsStore.setWorkflowExecutionData(aiChatExecutionResponse);
const rendered = render();

View File

@@ -16,7 +16,7 @@ import LogsPanelActions from '@/components/CanvasChat/future/components/LogsPane
const workflowsStore = useWorkflowsStore();
const canvasStore = useCanvasStore();
const panelState = computed(() => workflowsStore.chatPanelState);
const panelState = computed(() => workflowsStore.logsPanelState);
const container = ref<HTMLElement>();
const selectedLogEntry = ref<LogEntryIdentity | undefined>(undefined);
const pipContainer = useTemplateRef('pipContainer');
@@ -49,7 +49,7 @@ const { canPopOut, isPoppedOut, pipWindow } = usePiPWindow({
}
telemetry.track('User toggled log view', { new_state: 'attached' });
workflowsStore.setPanelState(LOGS_PANEL_STATE.ATTACHED);
workflowsStore.setPreferPoppedOutLogsView(false);
},
});
const logsPanelActionsProps = computed<InstanceType<typeof LogsPanelActions>['$props']>(() => ({
@@ -60,19 +60,17 @@ const logsPanelActionsProps = computed<InstanceType<typeof LogsPanelActions>['$p
}));
function onToggleOpen() {
if (panelState.value === LOGS_PANEL_STATE.CLOSED) {
telemetry.track('User toggled log view', { new_state: 'attached' });
workflowsStore.setPanelState(LOGS_PANEL_STATE.ATTACHED);
} else {
telemetry.track('User toggled log view', { new_state: 'collapsed' });
workflowsStore.setPanelState(LOGS_PANEL_STATE.CLOSED);
}
workflowsStore.toggleLogsPanelOpen();
telemetry.track('User toggled log view', {
new_state: panelState.value === LOGS_PANEL_STATE.CLOSED ? 'attached' : 'collapsed',
});
}
function handleClickHeader() {
if (panelState.value === LOGS_PANEL_STATE.CLOSED) {
telemetry.track('User toggled log view', { new_state: 'attached' });
workflowsStore.setPanelState(LOGS_PANEL_STATE.ATTACHED);
workflowsStore.toggleLogsPanelOpen(true);
}
}
@@ -82,7 +80,8 @@ function handleSelectLogEntry(selected: LogEntryIdentity | undefined) {
function onPopOut() {
telemetry.track('User toggled log view', { new_state: 'floating' });
workflowsStore.setPanelState(LOGS_PANEL_STATE.FLOATING);
workflowsStore.toggleLogsPanelOpen(true);
workflowsStore.setPreferPoppedOutLogsView(true);
}
watch([panelState, height], ([state, h]) => {

View File

@@ -7,11 +7,21 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
import { createRouter, createWebHistory } from 'vue-router';
import { h, type ExtractPropTypes } from 'vue';
import { fireEvent, waitFor, within } from '@testing-library/vue';
import { aiAgentNode, executionResponse, aiChatWorkflow } from '../../__test__/data';
import {
aiAgentNode,
aiChatExecutionResponse,
aiChatWorkflow,
aiManualExecutionResponse,
aiManualWorkflow,
} from '../../__test__/data';
import { usePushConnectionStore } from '@/stores/pushConnection.store';
import { useNDVStore } from '@/stores/ndv.store';
describe('LogsOverviewPanel', () => {
let pinia: TestingPinia;
let workflowsStore: ReturnType<typeof mockedStore<typeof useWorkflowsStore>>;
let pushConnectionStore: ReturnType<typeof mockedStore<typeof usePushConnectionStore>>;
let ndvStore: ReturnType<typeof mockedStore<typeof useNDVStore>>;
function render(props: ExtractPropTypes<typeof LogsOverviewPanel>) {
return renderComponent(LogsOverviewPanel, {
@@ -36,6 +46,11 @@ describe('LogsOverviewPanel', () => {
workflowsStore = mockedStore(useWorkflowsStore);
workflowsStore.setWorkflow(aiChatWorkflow);
workflowsStore.setWorkflowExecutionData(null);
pushConnectionStore = mockedStore(usePushConnectionStore);
pushConnectionStore.isConnected = true;
ndvStore = mockedStore(useNDVStore);
});
it('should not render body if the panel is not open', () => {
@@ -51,7 +66,7 @@ describe('LogsOverviewPanel', () => {
});
it('should render summary text and executed nodes if there is an execution', async () => {
workflowsStore.setWorkflowExecutionData(executionResponse);
workflowsStore.setWorkflowExecutionData(aiChatExecutionResponse);
const rendered = render({ isOpen: true, node: aiAgentNode });
const summary = within(rendered.container.querySelector('.summary')!);
@@ -79,7 +94,34 @@ describe('LogsOverviewPanel', () => {
expect(row2.queryByText('555 Tokens')).toBeInTheDocument();
// collapse tree
await fireEvent.click(row1.getByRole('button'));
await fireEvent.click(row1.getAllByLabelText('Toggle row')[0]);
await waitFor(() => expect(tree.queryAllByRole('treeitem')).toHaveLength(1));
});
it('should open NDV if the button is clicked', async () => {
workflowsStore.setWorkflowExecutionData(aiChatExecutionResponse);
const rendered = render({ isOpen: true, node: aiAgentNode });
const aiAgentRow = rendered.getAllByRole('treeitem')[0];
await fireEvent.click(within(aiAgentRow).getAllByLabelText('Open...')[0]);
await waitFor(() => expect(ndvStore.activeNodeName).toBe('AI Agent'));
});
it('should trigger partial execution if the button is clicked', async () => {
workflowsStore.setWorkflow(aiManualWorkflow);
workflowsStore.setWorkflowExecutionData(aiManualExecutionResponse);
const spyRun = vi.spyOn(workflowsStore, 'runWorkflow');
workflowsStore.setWorkflowExecutionData(aiChatExecutionResponse);
const rendered = render({ isOpen: true, node: aiAgentNode });
const aiAgentRow = rendered.getAllByRole('treeitem')[0];
await fireEvent.click(within(aiAgentRow).getAllByLabelText('Test step')[0]);
await waitFor(() =>
expect(spyRun).toHaveBeenCalledWith(expect.objectContaining({ destinationNode: 'AI Agent' })),
);
});
});

View File

@@ -5,7 +5,7 @@ import { useI18n } from '@/composables/useI18n';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { N8nButton, N8nRadioButtons, N8nText, N8nTooltip } from '@n8n/design-system';
import { computed } from 'vue';
import { computed, nextTick } from 'vue';
import { ElTree, type TreeNode as ElTreeNode } from 'element-plus';
import {
createAiData,
@@ -20,6 +20,9 @@ import { useTelemetry } from '@/composables/useTelemetry';
import ConsumedTokenCountText from '@/components/CanvasChat/future/components/ConsumedTokenCountText.vue';
import { type LogEntryIdentity } from '@/components/CanvasChat/types/logs';
import LogsOverviewRow from '@/components/CanvasChat/future/components/LogsOverviewRow.vue';
import { useRunWorkflow } from '@/composables/useRunWorkflow';
import { useNDVStore } from '@/stores/ndv.store';
import { useRouter } from 'vue-router';
const { node, isOpen, selected } = defineProps<{
isOpen: boolean;
@@ -34,6 +37,9 @@ defineSlots<{ actions: {} }>();
const locale = useI18n();
const telemetry = useTelemetry();
const workflowsStore = useWorkflowsStore();
const router = useRouter();
const runWorkflow = useRunWorkflow({ router });
const ndvStore = useNDVStore();
const nodeHelpers = useNodeHelpers();
const isClearExecutionButtonVisible = useClearExecutionButtonVisible();
const workflow = computed(() => workflowsStore.getCurrentWorkflow());
@@ -106,6 +112,17 @@ function handleSwitchView(value: 'overview' | 'details') {
function handleToggleExpanded(treeNode: ElTreeNode) {
treeNode.expanded = !treeNode.expanded;
}
async function handleOpenNdv(treeNode: TreeNode) {
ndvStore.setActiveNodeName(treeNode.node);
// HACK: defer setting the output run index to not be overridden by other effects
await nextTick(() => ndvStore.setOutputRunIndex(treeNode.runIndex));
}
async function handleTriggerPartialExecution(treeNode: TreeNode) {
await runWorkflow.runWorkflow({ destinationNode: treeNode.node });
}
</script>
<template>
@@ -179,6 +196,8 @@ function handleToggleExpanded(treeNode: ElTreeNode) {
:is-compact="selected !== undefined"
:should-show-consumed-tokens="consumedTokens.totalTokens > 0"
@toggle-expanded="handleToggleExpanded"
@open-ndv="handleOpenNdv"
@trigger-partial-execution="handleTriggerPartialExecution"
/>
</template>
</ElTree>

View File

@@ -4,7 +4,7 @@ import { getSubtreeTotalConsumedTokens, type TreeNode } from '@/components/RunDa
import { useWorkflowsStore } from '@/stores/workflows.store';
import { computed } from 'vue';
import { type INodeUi } from '@/Interface';
import { N8nIcon, N8nIconButton, N8nText } from '@n8n/design-system';
import { N8nButton, N8nIcon, N8nIconButton, N8nText } from '@n8n/design-system';
import { type ITaskData } from 'n8n-workflow';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { upperFirst } from 'lodash-es';
@@ -21,7 +21,11 @@ const props = defineProps<{
isCompact: boolean;
}>();
const emit = defineEmits<{ toggleExpanded: [node: ElTreeNode] }>();
const emit = defineEmits<{
toggleExpanded: [node: ElTreeNode];
triggerPartialExecution: [node: TreeNode];
openNdv: [node: TreeNode];
}>();
const locale = useI18n();
const workflowsStore = useWorkflowsStore();
@@ -140,43 +144,67 @@ function isLastChild(level: number) {
:class="$style.compactErrorIcon"
/>
<N8nIconButton
type="secondary"
size="small"
icon="play"
style="color: var(--color-text-base)"
:aria-label="locale.baseText('logs.overview.body.run')"
:class="[$style.partialExecutionButton, depth > 0 ? $style.unavailable : '']"
@click.stop="emit('triggerPartialExecution', props.data)"
/>
<N8nIconButton
type="secondary"
size="small"
icon="external-link-alt"
style="color: var(--color-text-base)"
:class="$style.openNdvButton"
:aria-label="locale.baseText('logs.overview.body.open')"
@click.stop="emit('openNdv', props.data)"
/>
<N8nButton
v-if="!isCompact || props.data.children.length > 0"
type="secondary"
size="medium"
:icon="props.node.expanded ? 'chevron-down' : 'chevron-up'"
size="small"
:square="true"
:style="{
visibility: props.data.children.length === 0 ? 'hidden' : '',
color: 'var(--color-text-base)', // give higher specificity than the style from the component itself
}"
:class="$style.toggleButton"
:aria-label="locale.baseText('logs.overview.body.toggleRow')"
@click.stop="emit('toggleExpanded', props.node)"
/>
>
<N8nIcon size="medium" :icon="props.node.expanded ? 'chevron-down' : 'chevron-up'" />
</N8nButton>
</div>
</template>
<style lang="scss" module>
.container {
display: flex;
align-items: stretch;
align-items: center;
justify-content: stretch;
overflow: hidden;
position: relative;
z-index: 1;
--row-gap-thickness: 1px;
& > * {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding: var(--spacing-2xs);
margin-bottom: var(--row-gap-thickness);
}
}
.background {
position: absolute;
left: calc(var(--indent-depth) * 32px);
left: calc(var(--row-gap-thickness) + var(--indent-depth) * 32px);
top: 0;
width: calc(100% - var(--indent-depth) * 32px);
height: 100%;
width: calc(100% - var(--indent-depth) * 32px - var(--row-gap-thickness));
height: calc(100% - var(--row-gap-thickness));
border-radius: var(--border-radius-base);
z-index: -1;
@@ -197,6 +225,7 @@ function isLastChild(level: number) {
align-self: stretch;
position: relative;
overflow: hidden;
margin-bottom: 0;
&.connectorCurved:before {
content: '';
@@ -220,6 +249,7 @@ function isLastChild(level: number) {
}
.icon {
margin-left: var(--row-gap-thickness);
flex-grow: 0;
flex-shrink: 0;
}
@@ -283,6 +313,26 @@ function isLastChild(level: number) {
}
}
.partialExecutionButton,
.openNdvButton {
transition: none;
/* By default, take space but keep invisible */
visibility: hidden;
.container.compact & {
/* When compact, collapse to save space */
display: none;
}
.container:hover &:not(.unavailable) {
visibility: visible;
display: inline-flex;
}
}
.partialExecutionButton,
.openNdvButton,
.toggleButton {
flex-grow: 0;
flex-shrink: 0;
@@ -290,9 +340,15 @@ function isLastChild(level: number) {
background: transparent;
margin-inline-end: var(--spacing-5xs);
color: var(--color-text-base);
align-items: center;
justify-content: center;
&:hover {
background: transparent;
}
}
.toggleButton {
display: inline-flex;
}
</style>

View File

@@ -34,7 +34,7 @@ const toggleButtonText = computed(() =>
size="small"
icon-size="medium"
:aria-label="popOutButtonText"
@click="emit('popOut')"
@click.stop="emit('popOut')"
/>
</N8nTooltip>
<N8nTooltip

View File

@@ -78,9 +78,6 @@ const { APP_Z_INDEXES } = useStyles();
const settingsEventBus = createEventBus();
const redrawRequired = ref(false);
const runInputIndex = ref(-1);
const runOutputIndex = ref(-1);
const isLinkingEnabled = ref(true);
const selectedInput = ref<string | undefined>();
const triggerWaitingWarningEnabled = ref(false);
const isDragging = ref(false);
@@ -248,12 +245,6 @@ const maxOutputRun = computed(() => {
return 0;
});
const outputRun = computed(() =>
runOutputIndex.value === -1
? maxOutputRun.value
: Math.min(runOutputIndex.value, maxOutputRun.value),
);
const maxInputRun = computed(() => {
if (inputNode.value === null || activeNode.value === null) {
return 0;
@@ -290,15 +281,23 @@ const maxInputRun = computed(() => {
return 0;
});
const isLinkingEnabled = computed(() => ndvStore.isRunIndexLinkingEnabled);
const outputRun = computed(() =>
ndvStore.output.run === -1
? maxOutputRun.value
: Math.min(ndvStore.output.run, maxOutputRun.value),
);
const inputRun = computed(() => {
if (isLinkingEnabled.value && maxOutputRun.value === maxInputRun.value) {
return outputRun.value;
}
if (runInputIndex.value === -1) {
if (ndvStore.input.run === -1) {
return maxInputRun.value;
}
return Math.min(runInputIndex.value, maxInputRun.value);
return Math.min(ndvStore.input.run, maxInputRun.value);
});
const canLinkRuns = computed(
@@ -443,13 +442,13 @@ const onPanelsInit = (e: { position: number }) => {
};
const onLinkRunToOutput = () => {
isLinkingEnabled.value = true;
ndvStore.setRunIndexLinkingEnabled(true);
trackLinking('output');
};
const onUnlinkRun = (pane: string) => {
runInputIndex.value = runOutputIndex.value;
isLinkingEnabled.value = false;
ndvStore.setInputRunIndex(outputRun.value);
ndvStore.setRunIndexLinkingEnabled(false);
trackLinking(pane);
};
@@ -476,8 +475,8 @@ const trackLinking = (pane: string) => {
};
const onLinkRunToInput = () => {
runOutputIndex.value = runInputIndex.value;
isLinkingEnabled.value = true;
ndvStore.setOutputRunIndex(inputRun.value);
ndvStore.setRunIndexLinkingEnabled(true);
trackLinking('input');
};
@@ -553,15 +552,12 @@ const trackRunChange = (run: number, pane: string) => {
};
const onRunOutputIndexChange = (run: number) => {
runOutputIndex.value = run;
ndvStore.setOutputRunIndex(run);
trackRunChange(run, 'output');
};
const onRunInputIndexChange = (run: number) => {
runInputIndex.value = run;
if (linked.value) {
runOutputIndex.value = run;
}
ndvStore.setInputRunIndex(run);
trackRunChange(run, 'input');
};
@@ -570,8 +566,8 @@ const onOutputTableMounted = (e: { avgRowHeight: number }) => {
};
const onInputNodeChange = (value: string, index: number) => {
runInputIndex.value = -1;
isLinkingEnabled.value = true;
ndvStore.setInputRunIndex(-1);
ndvStore.setRunIndexLinkingEnabled(true);
selectedInput.value = value;
telemetry.track('User changed ndv input dropdown', {
@@ -621,9 +617,6 @@ watch(
}
if (node && node.name !== oldNode?.name && !isActiveStickyNode.value) {
runInputIndex.value = -1;
runOutputIndex.value = -1;
isLinkingEnabled.value = true;
selectedInput.value = undefined;
triggerWaitingWarningEnabled.value = false;
avgOutputRowHeight.value = 0;
@@ -674,26 +667,12 @@ watch(
{ immediate: true },
);
watch(maxOutputRun, () => {
runOutputIndex.value = -1;
});
watch(maxInputRun, () => {
runInputIndex.value = -1;
});
watch(inputNodeName, (nodeName) => {
setTimeout(() => {
ndvStore.setInputNodeName(nodeName);
}, 0);
});
watch(inputRun, (inputRun) => {
setTimeout(() => {
ndvStore.setInputRunIndex(inputRun);
}, 0);
});
onMounted(() => {
dataPinningEventBus.on('data-pinning-discovery', setIsTooltipVisible);
});

View File

@@ -40,7 +40,7 @@ const uiStore = useUIStore();
const { runEntireWorkflow } = useRunWorkflow({ router });
const { toggleChatOpen } = useCanvasOperations({ router });
const isChatOpen = computed(() => workflowsStore.chatPanelState !== LOGS_PANEL_STATE.CLOSED);
const isChatOpen = computed(() => workflowsStore.logsPanelState !== LOGS_PANEL_STATE.CLOSED);
const isExecuting = computed(() => uiStore.isActionActive.workflowRunning);
const testId = computed(() => `execute-workflow-button-${name}`);
</script>