+
diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/composables/useChatState.ts b/packages/frontend/editor-ui/src/components/CanvasChat/composables/useChatState.ts
index a353ea11d6..dce6a9d810 100644
--- a/packages/frontend/editor-ui/src/components/CanvasChat/composables/useChatState.ts
+++ b/packages/frontend/editor-ui/src/components/CanvasChat/composables/useChatState.ts
@@ -17,6 +17,7 @@ import { v4 as uuid } from 'uuid';
import type { Ref } from 'vue';
import { computed, provide, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
+import { LOGS_PANEL_STATE } from '../types/logs';
interface ChatState {
currentSessionId: Ref;
@@ -129,7 +130,7 @@ export function useChatState(isDisabled: Ref, onWindowResize: () => voi
watch(
() => chatPanelState.value,
(state) => {
- if (state !== 'closed') {
+ if (state !== LOGS_PANEL_STATE.CLOSED) {
setChatTriggerNode();
setConnectedNode();
diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.test.ts b/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.test.ts
index 48c2675d8b..c6d8bc9ee5 100644
--- a/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.test.ts
+++ b/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.test.ts
@@ -1,5 +1,5 @@
import { renderComponent } from '@/__tests__/render';
-import { fireEvent, waitFor } from '@testing-library/vue';
+import { fireEvent, within } from '@testing-library/vue';
import { mockedStore } from '@/__tests__/utils';
import LogsPanel from '@/components/CanvasChat/future/LogsPanel.vue';
import { useSettingsStore } from '@/stores/settings.store';
@@ -7,14 +7,15 @@ import { createTestingPinia, type TestingPinia } from '@pinia/testing';
import { setActivePinia } from 'pinia';
import { createRouter, createWebHistory } from 'vue-router';
import { useWorkflowsStore } from '@/stores/workflows.store';
-import { createTestNode } from '@/__tests__/mocks';
-import { CHAT_TRIGGER_NODE_TYPE } from '@/constants';
import { h } from 'vue';
+import { executionResponse, aiChatWorkflow, simpleWorkflow, nodeTypes } from '../__test__/data';
+import { useNodeTypesStore } from '@/stores/nodeTypes.store';
describe('LogsPanel', () => {
let pinia: TestingPinia;
let settingsStore: ReturnType>;
let workflowsStore: ReturnType>;
+ let nodeTypeStore: ReturnType>;
function render() {
return renderComponent(LogsPanel, {
@@ -39,50 +40,97 @@ describe('LogsPanel', () => {
settingsStore.isNewLogsEnabled = true;
workflowsStore = mockedStore(useWorkflowsStore);
+ workflowsStore.setWorkflowExecutionData(null);
+
+ nodeTypeStore = mockedStore(useNodeTypesStore);
+ nodeTypeStore.setNodeTypes(nodeTypes);
});
- it('renders collapsed panel by default', async () => {
- const rendered = render();
-
- expect(await rendered.findByText('Logs')).toBeInTheDocument();
- expect(
- rendered.queryByText('Nothing to display yet', { exact: false }),
- ).not.toBeInTheDocument();
- });
-
- it('renders chat panel if the workflow has chat trigger', async () => {
- workflowsStore.workflowTriggerNodes = [createTestNode({ type: CHAT_TRIGGER_NODE_TYPE })];
+ it('should render collapsed panel by default', async () => {
+ workflowsStore.setWorkflow(simpleWorkflow);
const rendered = render();
- expect(await rendered.findByText('Chat')).toBeInTheDocument();
+ 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);
+
+ const rendered = render();
+
+ expect(await rendered.findByTestId('logs-overview-header')).toBeInTheDocument();
+ expect(rendered.queryByTestId('chat-header')).not.toBeInTheDocument();
+ });
+
+ it('should render chat panel and logs panel if the workflow has chat trigger', async () => {
+ workflowsStore.setWorkflow(aiChatWorkflow);
+
+ const rendered = render();
+
+ expect(await rendered.findByTestId('logs-overview-header')).toBeInTheDocument();
+ expect(await rendered.findByTestId('chat-header')).toBeInTheDocument();
});
it('opens collapsed panel when clicked', async () => {
+ workflowsStore.setWorkflow(aiChatWorkflow);
+
const rendered = render();
- await rendered.findByText('Logs');
+ await fireEvent.click(await rendered.findByTestId('logs-overview-header'));
- await fireEvent.click(rendered.getByText('Logs'));
-
- expect(
- await rendered.findByText('Nothing to display yet', { exact: false }),
- ).toBeInTheDocument();
+ expect(await rendered.findByTestId('logs-overview-empty')).toBeInTheDocument();
});
- it('toggles panel when chevron icon button is clicked', async () => {
+ it('should toggle panel when chevron icon button in the overview panel is clicked', async () => {
+ workflowsStore.setWorkflow(aiChatWorkflow);
+
const rendered = render();
- await rendered.findByText('Logs');
+ const overviewPanel = await rendered.findByTestId('logs-overview-header');
- await fireEvent.click(rendered.getAllByRole('button').pop()!);
- expect(rendered.getByText('Nothing to display yet', { exact: false })).toBeInTheDocument();
+ await fireEvent.click(within(overviewPanel).getByLabelText('Open panel'));
+ expect(rendered.getByTestId('logs-overview-empty')).toBeInTheDocument();
- await fireEvent.click(rendered.getAllByRole('button').pop()!);
- await waitFor(() =>
- expect(
- rendered.queryByText('Nothing to display yet', { exact: false }),
- ).not.toBeInTheDocument(),
- );
+ await fireEvent.click(within(overviewPanel).getByLabelText('Collapse panel'));
+ expect(rendered.queryByTestId('logs-overview-empty')).not.toBeInTheDocument();
+ });
+
+ it('should open log details panel when a log entry is clicked in the logs overview panel', async () => {
+ workflowsStore.setWorkflow(aiChatWorkflow);
+ workflowsStore.setWorkflowExecutionData(executionResponse);
+
+ const rendered = render();
+
+ await fireEvent.click(await rendered.findByTestId('logs-overview-header'));
+ await fireEvent.click(await rendered.findByText('AI Agent'));
+ expect(rendered.getByTestId('log-details')).toBeInTheDocument();
+
+ // Click again to close the panel
+ await fireEvent.click(await rendered.findByText('AI Agent'));
+ expect(rendered.queryByTestId('log-details')).not.toBeInTheDocument();
+ });
+
+ 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);
+
+ const rendered = render();
+
+ await fireEvent.click(await rendered.findByTestId('logs-overview-header'));
+ await fireEvent.click(await rendered.findByText('AI Agent'));
+
+ const detailsPanel = rendered.getByTestId('log-details');
+
+ // Click the toggle button to close the panel
+ await fireEvent.click(within(detailsPanel).getByLabelText('Collapse panel'));
+ expect(rendered.queryByTestId('chat-messages-empty')).not.toBeInTheDocument();
+ expect(rendered.queryByText('AI Agent', { exact: false })).not.toBeInTheDocument();
+
+ // Click again to open the panel
+ await fireEvent.click(within(detailsPanel).getByLabelText('Open panel'));
+ expect(await rendered.findByTestId('chat-messages-empty')).toBeInTheDocument();
+ expect(await rendered.findByText('AI Agent', { exact: false })).toBeInTheDocument();
});
});
diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.vue b/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.vue
index d6ec7c6c1d..18273b1c10 100644
--- a/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.vue
+++ b/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.vue
@@ -1,7 +1,7 @@
@@ -88,16 +102,16 @@ watch([panelState, height], ([state, h]) => {
{
>
{
/>
+
+
+
+
+
-
-
-
-
-
-
+
-
+
@@ -209,4 +204,17 @@ watch([panelState, height], ([state, h]) => {
flex-shrink: 0;
max-width: 100%;
}
+
+.logsOverview {
+ flex-basis: 20%;
+ flex-grow: 1;
+ flex-shrink: 1;
+ min-width: 360px;
+}
+
+.logDetails {
+ flex-basis: 60%;
+ flex-grow: 1;
+ flex-shrink: 1;
+}
diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogDetailsPanel.vue b/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogDetailsPanel.vue
new file mode 100644
index 0000000000..f390f4529d
--- /dev/null
+++ b/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogDetailsPanel.vue
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewPanel.test.ts b/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewPanel.test.ts
index ddf5e9ecfe..2fe03c864a 100644
--- a/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewPanel.test.ts
+++ b/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewPanel.test.ts
@@ -1,6 +1,5 @@
import { renderComponent } from '@/__tests__/render';
import LogsOverviewPanel from './LogsOverviewPanel.vue';
-import { createTestNode, createTestWorkflow } from '@/__tests__/mocks';
import { setActivePinia } from 'pinia';
import { createTestingPinia, type TestingPinia } from '@pinia/testing';
import { mockedStore } from '@/__tests__/utils';
@@ -8,26 +7,12 @@ 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';
describe('LogsOverviewPanel', () => {
let pinia: TestingPinia;
let workflowsStore: ReturnType
>;
- const triggerNode = createTestNode({ name: 'Chat' });
- const aiAgentNode = createTestNode({ name: 'AI Agent' });
- const aiModelNode = createTestNode({ name: 'AI Model' });
- const workflow = createTestWorkflow({
- nodes: [triggerNode, aiAgentNode, aiModelNode],
- connections: {
- Chat: {
- main: [[{ node: 'AI Agent', index: 0, type: 'main' }]],
- },
- 'AI Model': {
- ai_languageModel: [[{ node: 'AI Agent', index: 0, type: 'ai_languageModel' }]],
- },
- },
- });
-
function render(props: ExtractPropTypes) {
return renderComponent(LogsOverviewPanel, {
props,
@@ -49,73 +34,24 @@ describe('LogsOverviewPanel', () => {
setActivePinia(pinia);
workflowsStore = mockedStore(useWorkflowsStore);
- workflowsStore.setWorkflow(workflow);
+ workflowsStore.setWorkflow(aiChatWorkflow);
workflowsStore.setWorkflowExecutionData(null);
});
it('should not render body if the panel is not open', () => {
const rendered = render({ isOpen: false, node: null });
- expect(
- rendered.queryByText('Nothing to display yet', { exact: false }),
- ).not.toBeInTheDocument();
+ expect(rendered.queryByTestId('logs-overview-empty')).not.toBeInTheDocument();
});
it('should render empty text if there is no execution', () => {
const rendered = render({ isOpen: true, node: null });
- expect(rendered.queryByText('Nothing to display yet', { exact: false })).toBeInTheDocument();
+ expect(rendered.queryByTestId('logs-overview-empty')).toBeInTheDocument();
});
it('should render summary text and executed nodes if there is an execution', async () => {
- workflowsStore.setWorkflowExecutionData({
- id: 'test-exec-id',
- finished: true,
- mode: 'manual',
- status: 'success',
- data: {
- resultData: {
- runData: {
- 'AI Agent': [
- {
- executionStatus: 'success',
- startTime: +new Date('2025-03-26T00:00:00.002Z'),
- executionTime: 1778,
- source: [],
- data: {},
- },
- ],
- 'AI Model': [
- {
- executionStatus: 'success',
- startTime: +new Date('2025-03-26T00:00:00.003Z'),
- executionTime: 1777,
- source: [],
- data: {
- ai_languageModel: [
- [
- {
- json: {
- tokenUsage: {
- completionTokens: 222,
- promptTokens: 333,
- totalTokens: 555,
- },
- },
- },
- ],
- ],
- },
- },
- ],
- },
- },
- },
- workflowData: workflow,
- createdAt: new Date('2025-03-26T00:00:00.000Z'),
- startedAt: new Date('2025-03-26T00:00:00.001Z'),
- stoppedAt: new Date('2025-03-26T00:00:02.000Z'),
- });
+ workflowsStore.setWorkflowExecutionData(executionResponse);
const rendered = render({ isOpen: true, node: aiAgentNode });
const summary = within(rendered.container.querySelector('.summary')!);
@@ -131,14 +67,15 @@ describe('LogsOverviewPanel', () => {
expect(row1.queryByText('AI Agent')).toBeInTheDocument();
expect(row1.queryByText('Success in 1.778s')).toBeInTheDocument();
- expect(row1.queryByText('Started 2025-03-26T00:00:00.002Z')).toBeInTheDocument();
+ expect(row1.queryByText('Started 00:00:00.002, 26 Mar')).toBeInTheDocument();
expect(row1.queryByText('555 Tokens')).toBeInTheDocument();
const row2 = within(tree.queryAllByRole('treeitem')[1]);
expect(row2.queryByText('AI Model')).toBeInTheDocument();
- expect(row2.queryByText('Success in 1.777s')).toBeInTheDocument();
- expect(row2.queryByText('Started 2025-03-26T00:00:00.003Z')).toBeInTheDocument();
+ expect(row2.queryByText('Error')).toBeInTheDocument();
+ expect(row2.queryByText('in 1.777s')).toBeInTheDocument();
+ expect(row2.queryByText('Started 00:00:00.003, 26 Mar')).toBeInTheDocument();
expect(row2.queryByText('555 Tokens')).toBeInTheDocument();
// collapse tree
diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewPanel.vue b/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewPanel.vue
index b6a4d15baa..79af0f6717 100644
--- a/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewPanel.vue
+++ b/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewPanel.vue
@@ -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, ref } from 'vue';
+import { computed } from 'vue';
import { ElTree, type TreeNode as ElTreeNode } from 'element-plus';
import {
createAiData,
@@ -18,10 +18,16 @@ import { type INodeUi } from '@/Interface';
import { upperFirst } from 'lodash-es';
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';
-const { node, isOpen } = defineProps<{ isOpen: boolean; node: INodeUi | null }>();
+const { node, isOpen, selected } = defineProps<{
+ isOpen: boolean;
+ node: INodeUi | null;
+ selected?: LogEntryIdentity;
+}>();
-const emit = defineEmits<{ clickHeader: [] }>();
+const emit = defineEmits<{ clickHeader: []; select: [LogEntryIdentity | undefined] }>();
defineSlots<{ actions: {} }>();
@@ -41,9 +47,9 @@ const executionTree = computed(() =>
: [],
);
const isEmpty = computed(() => workflowsStore.workflowExecutionData === null);
-const switchViewOptions = computed>(() => [
- { label: locale.baseText('logs.overview.header.switch.details'), value: 'details' },
- { label: locale.baseText('logs.overview.header.switch.overview'), value: 'overview' },
+const switchViewOptions = computed(() => [
+ { label: locale.baseText('logs.overview.header.switch.details'), value: 'details' as const },
+ { label: locale.baseText('logs.overview.header.switch.overview'), value: 'overview' as const },
]);
const executionStatusText = computed(() => {
const execution = workflowsStore.workflowExecutionData;
@@ -70,20 +76,18 @@ const consumedTokens = computed(() =>
getTotalConsumedTokens(...executionTree.value.map(getSubtreeTotalConsumedTokens)),
);
-const selectedRun = ref<{ node: string; runIndex: number } | undefined>(undefined);
-
function onClearExecutionData() {
workflowsStore.setWorkflowExecutionData(null);
nodeHelpers.updateNodesExecutionIssues();
}
function handleClickNode(clicked: TreeNode) {
- if (selectedRun.value?.node === clicked.node && selectedRun.value.runIndex === clicked.runIndex) {
- selectedRun.value = undefined;
+ if (selected?.node === clicked.node && selected.runIndex === clicked.runIndex) {
+ emit('select', undefined);
return;
}
- selectedRun.value = { node: clicked.node, runIndex: clicked.runIndex };
+ emit('select', { node: clicked.node, runIndex: clicked.runIndex });
telemetry.track('User selected node in log view', {
node_type: workflowsStore.nodesByName[clicked.node].type,
node_id: workflowsStore.nodesByName[clicked.node].id,
@@ -92,15 +96,23 @@ function handleClickNode(clicked: TreeNode) {
});
}
+function handleSwitchView(value: 'overview' | 'details') {
+ emit(
+ 'select',
+ value === 'overview' || executionTree.value.length === 0 ? undefined : executionTree.value[0],
+ );
+}
+
function handleToggleExpanded(treeNode: ElTreeNode) {
treeNode.expanded = !treeNode.expanded;
}
-
+
@@ -120,8 +132,19 @@ function handleToggleExpanded(treeNode: ElTreeNode) {
-
-
+
+
{{ locale.baseText('logs.overview.body.empty.message') }}
@@ -140,6 +163,7 @@ function handleToggleExpanded(treeNode: ElTreeNode) {
@@ -162,8 +185,9 @@ function handleToggleExpanded(treeNode: ElTreeNode) {
@@ -178,6 +202,7 @@ function handleToggleExpanded(treeNode: ElTreeNode) {
flex-direction: column;
align-items: stretch;
overflow: hidden;
+ background-color: var(--color-foreground-xlight);
}
.content {
diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewRow.vue b/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewRow.vue
index edf3006edd..cef42ea5d5 100644
--- a/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewRow.vue
+++ b/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewRow.vue
@@ -4,18 +4,21 @@ import { getSubtreeTotalConsumedTokens, type TreeNode } from '@/components/RunDa
import { useWorkflowsStore } from '@/stores/workflows.store';
import { computed } from 'vue';
import { type INodeUi } from '@/Interface';
-import { type ExecutionStatus, type ITaskData } from 'n8n-workflow';
-import { N8nIconButton, N8nText } from '@n8n/design-system';
+import { N8nIcon, N8nIconButton, N8nText } from '@n8n/design-system';
+import { type ITaskData } from 'n8n-workflow';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { upperFirst } from 'lodash-es';
import { useI18n } from '@/composables/useI18n';
import ConsumedTokenCountText from '@/components/CanvasChat/future/components/ConsumedTokenCountText.vue';
+import { I18nT } from 'vue-i18n';
+import { toDayMonth, toTime } from '@/utils/formatters/dateFormatter';
const props = defineProps<{
data: TreeNode;
node: ElTreeNode;
isSelected: boolean;
shouldShowConsumedTokens: boolean;
+ isCompact: boolean;
}>();
const emit = defineEmits<{ toggleExpanded: [node: ElTreeNode] }>();
@@ -33,32 +36,21 @@ const runData = computed(() =>
);
const type = computed(() => (node.value ? nodeTypeStore.getNodeType(node.value.type) : undefined));
const depth = computed(() => (props.node.level ?? 1) - 1);
-const timeTookText = computed(() => {
- const finalStatuses: ExecutionStatus[] = ['crashed', 'error', 'success'];
- const status = runData.value?.executionStatus;
-
- if (!status) {
- return '';
- }
-
- const statusText = upperFirst(status);
-
- return finalStatuses.includes(status)
- ? locale.baseText('logs.overview.body.summaryText', {
- interpolate: {
- status: statusText,
- time: locale.displayTimer(runData.value.executionTime, true),
- },
- })
- : statusText;
-});
-const startedAtText = computed(() =>
- locale.baseText('logs.overview.body.started', {
- interpolate: {
- time: new Date(runData.value?.startTime ?? 0).toISOString(), // TODO: confirm date format
- },
- }),
+const isSettled = computed(
+ () =>
+ runData.value?.executionStatus &&
+ ['crashed', 'error', 'success'].includes(runData.value.executionStatus),
);
+const isError = computed(() => !!runData.value?.error);
+const startedAtText = computed(() => {
+ const time = new Date(runData.value?.startTime ?? 0);
+
+ return locale.baseText('logs.overview.body.started', {
+ interpolate: {
+ time: `${toTime(time, true)}, ${toDayMonth(time)}`,
+ },
+ });
+});
const subtreeConsumedTokens = computed(() =>
props.shouldShowConsumedTokens ? getSubtreeTotalConsumedTokens(props.data) : undefined,
@@ -82,7 +74,12 @@ function isLastChild(level: number) {
+
-
{{ node.name }}
-
{{
- timeTookText
- }}
+
{{ node.name }}
+
+
+
+
+ {{
+ upperFirst(runData.executionStatus)
+ }}
+
+ {{ upperFirst(runData.executionStatus) }}
+
+ {{ locale.displayTimer(runData.executionTime, true) }}
+
+ {{ upperFirst(runData?.executionStatus) }}
{{
startedAtText
}}
@@ -116,19 +132,25 @@ function isLastChild(level: number) {
:consumed-tokens="subtreeConsumedTokens"
/>
-
-
-
+
+
@@ -138,6 +160,8 @@ function isLastChild(level: number) {
align-items: stretch;
justify-content: stretch;
overflow: hidden;
+ position: relative;
+ z-index: 1;
& > * {
overflow: hidden;
@@ -145,28 +169,25 @@ function isLastChild(level: number) {
white-space: nowrap;
padding: var(--spacing-2xs);
}
+}
- & > :has(.toggleButton) {
- flex-shrink: 0;
- display: flex;
- align-items: center;
- padding: 0;
- }
+.background {
+ position: absolute;
+ left: calc(var(--indent-depth) * 32px);
+ top: 0;
+ width: calc(100% - var(--indent-depth) * 32px);
+ height: 100%;
+ border-radius: var(--border-radius-base);
+ z-index: -1;
- & > .icon {
- border-top-left-radius: var(--border-radius-base);
- border-bottom-left-radius: var(--border-radius-base);
- }
-
- & > :last-of-type {
- border-top-right-radius: var(--border-radius-base);
- border-bottom-right-radius: var(--border-radius-base);
- }
-
- &.selected > :not(.indent),
- &:hover > :not(.indent) {
+ .selected &,
+ .container:hover & {
background-color: var(--color-foreground-base);
}
+
+ .selected:not(:hover).error & {
+ background-color: var(--color-danger-tint-2);
+ }
}
.indent {
@@ -212,12 +233,29 @@ function isLastChild(level: number) {
flex-grow: 0;
flex-shrink: 0;
width: 20%;
+
+ .errorIcon {
+ margin-right: var(--spacing-4xs);
+ vertical-align: text-bottom;
+ }
+
+ .compact:hover & {
+ width: auto;
+ }
+
+ .compact:not(:hover) & {
+ display: none;
+ }
}
.startedAt {
flex-grow: 0;
flex-shrink: 0;
width: 30%;
+
+ .compact & {
+ display: none;
+ }
}
.consumedTokens {
@@ -225,12 +263,33 @@ function isLastChild(level: number) {
flex-shrink: 0;
width: 10%;
text-align: right;
+
+ .compact:hover & {
+ width: auto;
+ }
+
+ .compact &:empty,
+ .compact:not(:hover) & {
+ display: none;
+ }
+}
+
+.compactErrorIcon {
+ flex-grow: 0;
+ flex-shrink: 0;
+
+ .container:hover & {
+ display: none;
+ }
}
.toggleButton {
+ flex-grow: 0;
+ flex-shrink: 0;
border: none;
background: transparent;
margin-inline-end: var(--spacing-5xs);
+ color: var(--color-text-base);
&:hover {
background: transparent;
diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsPanelActions.vue b/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsPanelActions.vue
new file mode 100644
index 0000000000..98196a963a
--- /dev/null
+++ b/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsPanelActions.vue
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/types/logs.ts b/packages/frontend/editor-ui/src/components/CanvasChat/types/logs.ts
new file mode 100644
index 0000000000..47b7830c5b
--- /dev/null
+++ b/packages/frontend/editor-ui/src/components/CanvasChat/types/logs.ts
@@ -0,0 +1,12 @@
+export interface LogEntryIdentity {
+ node: string;
+ runIndex: number;
+}
+
+export const LOGS_PANEL_STATE = {
+ CLOSED: 'closed',
+ ATTACHED: 'attached',
+ FLOATING: 'floating',
+} as const;
+
+export type LogsPanelState = (typeof LOGS_PANEL_STATE)[keyof typeof LOGS_PANEL_STATE];
diff --git a/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeTrigger.vue b/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeTrigger.vue
index 5a0f39b419..a4513237a6 100644
--- a/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeTrigger.vue
+++ b/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeTrigger.vue
@@ -1,4 +1,5 @@
diff --git a/packages/frontend/editor-ui/src/composables/useCanvasOperations.test.ts b/packages/frontend/editor-ui/src/composables/useCanvasOperations.test.ts
index 4a0ba06e55..23b2923c1d 100644
--- a/packages/frontend/editor-ui/src/composables/useCanvasOperations.test.ts
+++ b/packages/frontend/editor-ui/src/composables/useCanvasOperations.test.ts
@@ -53,6 +53,7 @@ import { nextTick } from 'vue';
import { useProjectsStore } from '@/stores/projects.store';
import type { CanvasLayoutEvent } from './useCanvasLayout';
import { useTelemetry } from './useTelemetry';
+import { LOGS_PANEL_STATE } from '@/components/CanvasChat/types/logs';
vi.mock('vue-router', async (importOriginal) => {
const actual = await importOriginal<{}>();
@@ -2932,11 +2933,11 @@ describe('useCanvasOperations', () => {
const { toggleChatOpen } = useCanvasOperations({ router });
workflowsStore.getCurrentWorkflow.mockReturnValue(createTestWorkflowObject());
- workflowsStore.chatPanelState = 'closed';
+ workflowsStore.chatPanelState = LOGS_PANEL_STATE.CLOSED;
await toggleChatOpen('main');
- expect(workflowsStore.setPanelState).toHaveBeenCalledWith('attached');
+ expect(workflowsStore.setPanelState).toHaveBeenCalledWith(LOGS_PANEL_STATE.ATTACHED);
});
it('should invoke workflowsStore#setPanelState with 1st argument "collapsed" if the chat panel is open', async () => {
@@ -2944,11 +2945,11 @@ describe('useCanvasOperations', () => {
const { toggleChatOpen } = useCanvasOperations({ router });
workflowsStore.getCurrentWorkflow.mockReturnValue(createTestWorkflowObject());
- workflowsStore.chatPanelState = 'attached';
+ workflowsStore.chatPanelState = LOGS_PANEL_STATE.ATTACHED;
await toggleChatOpen('main');
- expect(workflowsStore.setPanelState).toHaveBeenCalledWith('closed');
+ expect(workflowsStore.setPanelState).toHaveBeenCalledWith(LOGS_PANEL_STATE.CLOSED);
});
});
diff --git a/packages/frontend/editor-ui/src/composables/useCanvasOperations.ts b/packages/frontend/editor-ui/src/composables/useCanvasOperations.ts
index c31665e0e2..94a9ff1510 100644
--- a/packages/frontend/editor-ui/src/composables/useCanvasOperations.ts
+++ b/packages/frontend/editor-ui/src/composables/useCanvasOperations.ts
@@ -101,6 +101,7 @@ import { useUniqueNodeName } from '@/composables/useUniqueNodeName';
import { isPresent } from '../utils/typesUtils';
import { useProjectsStore } from '@/stores/projects.store';
import type { CanvasLayoutEvent } from './useCanvasLayout';
+import { LOGS_PANEL_STATE } from '@/components/CanvasChat/types/logs';
type AddNodeData = Partial & {
type: string;
@@ -1976,7 +1977,9 @@ export function useCanvasOperations({ router }: { router: ReturnType }) {
const nodeHelpers = useNodeHelpers();
@@ -182,7 +183,7 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType & { settings: NonNullable } = {
name: '',
@@ -116,8 +117,6 @@ const createEmptyWorkflow = (): IWorkflowDb => ({
let cachedWorkflowKey: string | null = '';
let cachedWorkflow: Workflow | null = null;
-type ChatPanelState = 'closed' | 'attached' | 'floating';
-
export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
const uiStore = useUIStore();
const telemetry = useTelemetry();
@@ -147,7 +146,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
const isInDebugMode = ref(false);
const chatMessages = ref([]);
const chatPartialExecutionDestinationNode = ref(null);
- const chatPanelState = ref('closed');
+ const chatPanelState = ref(LOGS_PANEL_STATE.CLOSED);
const { executingNode, addExecutingNode, removeExecutingNode, clearNodeExecutionQueue } =
useExecutingNode();
@@ -1208,7 +1207,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
// If chat trigger node is removed, close chat
if (node.type === CHAT_TRIGGER_NODE_TYPE && !settingsStore.isNewLogsEnabled) {
- setPanelState('closed');
+ setPanelState(LOGS_PANEL_STATE.CLOSED);
}
if (workflow.value.pinData && workflow.value.pinData.hasOwnProperty(node.name)) {
@@ -1670,7 +1669,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
// End Canvas V2 Functions
//
- function setPanelState(state: ChatPanelState) {
+ function setPanelState(state: LogsPanelState) {
chatPanelState.value = state;
}
diff --git a/packages/frontend/editor-ui/src/utils/formatters/dateFormatter.ts b/packages/frontend/editor-ui/src/utils/formatters/dateFormatter.ts
index c0a4526be3..bd49cc9b91 100644
--- a/packages/frontend/editor-ui/src/utils/formatters/dateFormatter.ts
+++ b/packages/frontend/editor-ui/src/utils/formatters/dateFormatter.ts
@@ -25,4 +25,5 @@ export function convertToDisplayDate(fullDate: Date | string | number): {
export const toDayMonth = (fullDate: Date | string) => dateformat(fullDate, 'd mmm');
-export const toTime = (fullDate: Date | string) => dateformat(fullDate, 'HH:MM:ss');
+export const toTime = (fullDate: Date | string, includeMillis: boolean = false) =>
+ dateformat(fullDate, includeMillis ? 'HH:MM:ss.l' : 'HH:MM:ss');
diff --git a/packages/frontend/editor-ui/src/views/NodeView.vue b/packages/frontend/editor-ui/src/views/NodeView.vue
index 6d16a6cebe..f1da046c3c 100644
--- a/packages/frontend/editor-ui/src/views/NodeView.vue
+++ b/packages/frontend/editor-ui/src/views/NodeView.vue
@@ -114,6 +114,7 @@ import { isValidNodeConnectionType } from '@/utils/typeGuards';
import { getEasyAiWorkflowJson } from '@/utils/easyAiWorkflowUtils';
import type { CanvasLayoutEvent } from '@/composables/useCanvasLayout';
import { useClearExecutionButtonVisible } from '@/composables/useClearExecutionButtonVisible';
+import { LOGS_PANEL_STATE } from '@/components/CanvasChat/types/logs';
defineOptions({
name: 'NodeView',
@@ -273,7 +274,7 @@ const keyBindingsEnabled = computed(() => {
return !ndvStore.activeNode && uiStore.activeModals.length === 0;
});
-const isChatOpen = computed(() => workflowsStore.chatPanelState !== 'closed');
+const isChatOpen = computed(() => workflowsStore.chatPanelState !== LOGS_PANEL_STATE.CLOSED);
/**
* Initialization