mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
fix(core): Allow insights breakdown by workflow to be sorted by workflow name (#17184)
This commit is contained in:
@@ -20,6 +20,26 @@ describe('ListInsightsWorkflowQueryDto', () => {
|
|||||||
sortBy: 'total:asc',
|
sortBy: 'total:asc',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'valid sortBy workflowName:asc',
|
||||||
|
request: {
|
||||||
|
sortBy: 'workflowName:asc',
|
||||||
|
},
|
||||||
|
parsedResult: {
|
||||||
|
...DEFAULT_PAGINATION,
|
||||||
|
sortBy: 'workflowName:asc',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'valid sortBy workflowName:desc',
|
||||||
|
request: {
|
||||||
|
sortBy: 'workflowName:desc',
|
||||||
|
},
|
||||||
|
parsedResult: {
|
||||||
|
...DEFAULT_PAGINATION,
|
||||||
|
sortBy: 'workflowName:desc',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'valid skip and take',
|
name: 'valid skip and take',
|
||||||
request: {
|
request: {
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ const VALID_SORT_OPTIONS = [
|
|||||||
'runTime:desc',
|
'runTime:desc',
|
||||||
'averageRunTime:asc',
|
'averageRunTime:asc',
|
||||||
'averageRunTime:desc',
|
'averageRunTime:desc',
|
||||||
|
'workflowName:asc',
|
||||||
|
'workflowName:desc',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
// ---------------------
|
// ---------------------
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import type { InsightsDateRange } from '@n8n/api-types';
|
import type { InsightsDateRange } from '@n8n/api-types';
|
||||||
import { mockInstance } from '@n8n/backend-test-utils';
|
import { mockInstance, createWorkflow, createTeamProject, testDb } from '@n8n/backend-test-utils';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
import { Telemetry } from '@/telemetry';
|
import { Telemetry } from '@/telemetry';
|
||||||
|
import { createCompactedInsightsEvent } from '@/modules/insights/database/entities/__tests__/db-utils';
|
||||||
|
|
||||||
import { createUser } from '../shared/db/users';
|
import { createUser } from '../shared/db/users';
|
||||||
import type { SuperAgentTest } from '../shared/types';
|
import type { SuperAgentTest } from '../shared/types';
|
||||||
@@ -116,11 +118,20 @@ describe('GET /insights/by-workflow', () => {
|
|||||||
features: ['feat:insights:viewSummary', 'feat:insights:viewDashboard'],
|
features: ['feat:insights:viewSummary', 'feat:insights:viewDashboard'],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
test('Call should work with valid query parameters', async () => {
|
|
||||||
await agents.owner
|
test.each([
|
||||||
.get('/insights/by-workflow')
|
{
|
||||||
.query({ skip: '10', take: '20', sortBy: 'total:desc' })
|
skip: '10',
|
||||||
.expect(200);
|
take: '20',
|
||||||
|
sortBy: 'total:desc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
skip: '1',
|
||||||
|
take: '25',
|
||||||
|
sortBy: 'workflowName:asc',
|
||||||
|
},
|
||||||
|
])('Call should work with valid query parameters: %s', async (queryParams) => {
|
||||||
|
await agents.owner.get('/insights/by-workflow').query(queryParams).expect(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.each<{ skip: string; take?: string; sortBy?: string }>([
|
test.each<{ skip: string; take?: string; sortBy?: string }>([
|
||||||
@@ -149,4 +160,168 @@ describe('GET /insights/by-workflow', () => {
|
|||||||
})
|
})
|
||||||
.expect(400);
|
.expect(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
'total:asc',
|
||||||
|
'total:desc',
|
||||||
|
'succeeded:asc',
|
||||||
|
'succeeded:desc',
|
||||||
|
'failed:asc',
|
||||||
|
'failed:desc',
|
||||||
|
'failureRate:asc',
|
||||||
|
'failureRate:desc',
|
||||||
|
'timeSaved:asc',
|
||||||
|
'timeSaved:desc',
|
||||||
|
'runTime:asc',
|
||||||
|
'runTime:desc',
|
||||||
|
'averageRunTime:asc',
|
||||||
|
'averageRunTime:desc',
|
||||||
|
'workflowName:asc',
|
||||||
|
'workflowName:desc',
|
||||||
|
])('Call should return 200 with valid sortBy option: %s', async (sortBy) => {
|
||||||
|
const response = await agents.owner.get('/insights/by-workflow').query({ sortBy }).expect(200);
|
||||||
|
|
||||||
|
expect(response.body).toHaveProperty('data');
|
||||||
|
expect(response.body.data).toHaveProperty('count');
|
||||||
|
expect(Array.isArray(response.body.data.data)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sorting order verification', () => {
|
||||||
|
afterEach(async () => {
|
||||||
|
await testDb.truncate([
|
||||||
|
'InsightsRaw',
|
||||||
|
'InsightsByPeriod',
|
||||||
|
'InsightsMetadata',
|
||||||
|
'SharedWorkflow',
|
||||||
|
'WorkflowEntity',
|
||||||
|
'Project',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return workflows sorted by total:desc', async () => {
|
||||||
|
const project = await createTeamProject('Test Project 1');
|
||||||
|
const testData = [
|
||||||
|
{ name: 'Workflow A', total: 10 },
|
||||||
|
{ name: 'Workflow B', total: 5 },
|
||||||
|
{ name: 'Workflow C', total: 15 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const workflows = await Promise.all(
|
||||||
|
testData.map(async (data) => await createWorkflow({ name: data.name }, project)),
|
||||||
|
);
|
||||||
|
|
||||||
|
const periodStart = DateTime.utc().startOf('day');
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
workflows.map(
|
||||||
|
async (workflow, index) =>
|
||||||
|
await createCompactedInsightsEvent(workflow, {
|
||||||
|
type: 'success',
|
||||||
|
value: testData[index].total,
|
||||||
|
periodUnit: 'day',
|
||||||
|
periodStart,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await agents.owner
|
||||||
|
.get('/insights/by-workflow')
|
||||||
|
.query({ sortBy: 'total:desc' })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body.data.count).toBe(3);
|
||||||
|
expect(response.body.data.data).toHaveLength(3);
|
||||||
|
|
||||||
|
// Verify descending order by total
|
||||||
|
const expectedOrder = [15, 10, 5];
|
||||||
|
response.body.data.data.forEach((item: any, index: number) => {
|
||||||
|
expect(item.total).toBe(expectedOrder[index]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return workflows sorted by workflowName:asc', async () => {
|
||||||
|
const project = await createTeamProject('Test Project A');
|
||||||
|
const workflowNames = ['Zebra Workflow', 'Alpha Workflow', 'Beta Workflow'];
|
||||||
|
|
||||||
|
const workflows = await Promise.all(
|
||||||
|
workflowNames.map(async (name) => await createWorkflow({ name }, project)),
|
||||||
|
);
|
||||||
|
|
||||||
|
const periodStart = DateTime.utc().startOf('day');
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
workflows.map(
|
||||||
|
async (workflow) =>
|
||||||
|
await createCompactedInsightsEvent(workflow, {
|
||||||
|
type: 'success',
|
||||||
|
value: 5,
|
||||||
|
periodUnit: 'day',
|
||||||
|
periodStart,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await agents.owner
|
||||||
|
.get('/insights/by-workflow')
|
||||||
|
.query({ sortBy: 'workflowName:asc' })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body.data.count).toBe(3);
|
||||||
|
expect(response.body.data.data).toHaveLength(3);
|
||||||
|
|
||||||
|
// Verify ascending order by workflow name
|
||||||
|
const expectedOrder = ['Alpha Workflow', 'Beta Workflow', 'Zebra Workflow'];
|
||||||
|
response.body.data.data.forEach((item: any, index: number) => {
|
||||||
|
expect(item.workflowName).toBe(expectedOrder[index]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return workflows sorted by failureRate:asc', async () => {
|
||||||
|
const project = await createTeamProject('Another Test Project');
|
||||||
|
const testData = [
|
||||||
|
{ name: 'Low Failure', success: 9, failure: 1 }, // 10% failure rate
|
||||||
|
{ name: 'High Failure', success: 2, failure: 8 }, // 80% failure rate
|
||||||
|
{ name: 'Medium Failure', success: 5, failure: 5 }, // 50% failure rate
|
||||||
|
];
|
||||||
|
|
||||||
|
const workflows = await Promise.all(
|
||||||
|
testData.map(async (data) => await createWorkflow({ name: data.name }, project)),
|
||||||
|
);
|
||||||
|
|
||||||
|
const periodStart = DateTime.utc().startOf('day');
|
||||||
|
|
||||||
|
// Create insights events for each workflow individually to avoid race conditions
|
||||||
|
for (const [index, workflow] of workflows.entries()) {
|
||||||
|
const data = testData[index];
|
||||||
|
|
||||||
|
await createCompactedInsightsEvent(workflow, {
|
||||||
|
type: 'success',
|
||||||
|
value: data.success,
|
||||||
|
periodUnit: 'day',
|
||||||
|
periodStart,
|
||||||
|
});
|
||||||
|
|
||||||
|
await createCompactedInsightsEvent(workflow, {
|
||||||
|
type: 'failure',
|
||||||
|
value: data.failure,
|
||||||
|
periodUnit: 'day',
|
||||||
|
periodStart,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await agents.owner
|
||||||
|
.get('/insights/by-workflow')
|
||||||
|
.query({ sortBy: 'failureRate:asc' })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body.data.count).toBe(3);
|
||||||
|
expect(response.body.data.data).toHaveLength(3);
|
||||||
|
|
||||||
|
// Verify ascending order by failure rate
|
||||||
|
const expectedOrder = [0.1, 0.5, 0.8]; // 10%, 50%, 80%
|
||||||
|
response.body.data.data.forEach((item: any, index: number) => {
|
||||||
|
expect(item.failureRate).toBe(expectedOrder[index]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,325 @@
|
|||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
|
import { screen, within } from '@testing-library/vue';
|
||||||
|
import { vi } from 'vitest';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
|
import { useEmitters } from '@/__tests__/utils';
|
||||||
|
import InsightsTableWorkflows from '@/features/insights/components/tables/InsightsTableWorkflows.vue';
|
||||||
|
import type { InsightsByWorkflow } from '@n8n/api-types';
|
||||||
|
|
||||||
|
const { emitters, addEmitter } = useEmitters<'n8nDataTableServer'>();
|
||||||
|
|
||||||
|
// Mock telemetry
|
||||||
|
const mockTelemetry = {
|
||||||
|
track: vi.fn(),
|
||||||
|
};
|
||||||
|
vi.mock('@/composables/useTelemetry', () => ({
|
||||||
|
useTelemetry: () => mockTelemetry,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock N8nDataTableServer like in SettingsUsersTable.test.ts
|
||||||
|
vi.mock('@n8n/design-system', async (importOriginal) => {
|
||||||
|
const original = await importOriginal<object>();
|
||||||
|
return {
|
||||||
|
...original,
|
||||||
|
N8nDataTableServer: defineComponent({
|
||||||
|
props: {
|
||||||
|
headers: { type: Array, required: true },
|
||||||
|
items: { type: Array, required: true },
|
||||||
|
itemsLength: { type: Number, required: true },
|
||||||
|
sortBy: { type: Array },
|
||||||
|
page: { type: Number },
|
||||||
|
itemsPerPage: { type: Number },
|
||||||
|
},
|
||||||
|
setup(_, { emit }) {
|
||||||
|
addEmitter('n8nDataTableServer', emit);
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<div data-test-id="insights-table">
|
||||||
|
<div v-for="item in items" :key="item.workflowId"
|
||||||
|
:data-test-id="'workflow-row-' + item.workflowId">
|
||||||
|
<div v-for="header in headers" :key="header.key">
|
||||||
|
<slot :name="'item.' + header.key" :item="item"
|
||||||
|
:value="header.value ? header.value(item) : item[header.key]">
|
||||||
|
<span v-if="header.value">{{ header.value(item) }}</span>
|
||||||
|
<span v-else>{{ item[header.key] }}</span>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockInsightsData: InsightsByWorkflow = {
|
||||||
|
count: 2,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
workflowId: 'workflow-1',
|
||||||
|
workflowName: 'Test Workflow 1',
|
||||||
|
total: 100,
|
||||||
|
failed: 5,
|
||||||
|
failureRate: 0.05,
|
||||||
|
timeSaved: 3600,
|
||||||
|
averageRunTime: 1200,
|
||||||
|
projectName: 'Test Project 1',
|
||||||
|
projectId: 'project-1',
|
||||||
|
succeeded: 95,
|
||||||
|
runTime: 114000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
workflowId: 'workflow-2',
|
||||||
|
workflowName: 'Test Workflow 2 With Very Long Name That Should Be Truncated',
|
||||||
|
total: 50,
|
||||||
|
failed: 0,
|
||||||
|
failureRate: 0,
|
||||||
|
timeSaved: 0,
|
||||||
|
averageRunTime: 800,
|
||||||
|
projectName: '',
|
||||||
|
projectId: 'project-2',
|
||||||
|
succeeded: 50,
|
||||||
|
runTime: 40000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
let renderComponent: ReturnType<typeof createComponentRenderer>;
|
||||||
|
|
||||||
|
describe('InsightsTableWorkflows', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
renderComponent = createComponentRenderer(InsightsTableWorkflows, {
|
||||||
|
pinia: createTestingPinia(),
|
||||||
|
props: {
|
||||||
|
data: mockInsightsData,
|
||||||
|
loading: false,
|
||||||
|
},
|
||||||
|
global: {
|
||||||
|
stubs: {
|
||||||
|
'router-link': {
|
||||||
|
template: '<a><slot /></a>',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('basic rendering', () => {
|
||||||
|
it('should display the correct heading', () => {
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
expect(screen.getByRole('heading', { name: 'Workflow insights' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render workflow data in table rows', () => {
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
expect(screen.getByTestId('workflow-row-workflow-1')).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId('workflow-row-workflow-2')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('data display', () => {
|
||||||
|
it('should display formatted numbers for total and failed columns', () => {
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
const row1 = screen.getByTestId('workflow-row-workflow-1');
|
||||||
|
expect(within(row1).getByText('100')).toBeInTheDocument();
|
||||||
|
expect(within(row1).getByText('5')).toBeInTheDocument();
|
||||||
|
|
||||||
|
const row2 = screen.getByTestId('workflow-row-workflow-2');
|
||||||
|
expect(within(row2).getByText('50')).toBeInTheDocument();
|
||||||
|
expect(within(row2).getByText('0')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle large numbers with locale formatting', () => {
|
||||||
|
const largeNumberData = {
|
||||||
|
...mockInsightsData,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
...mockInsightsData.data[0],
|
||||||
|
total: 1000000,
|
||||||
|
failed: 50000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
renderComponent = createComponentRenderer(InsightsTableWorkflows, {
|
||||||
|
pinia: createTestingPinia(),
|
||||||
|
props: {
|
||||||
|
data: largeNumberData,
|
||||||
|
loading: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
const row = screen.getByTestId('workflow-row-workflow-1');
|
||||||
|
expect(within(row).getByText('1,000,000')).toBeInTheDocument();
|
||||||
|
expect(within(row).getByText('50,000')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('template slots', () => {
|
||||||
|
describe('workflowName slot', () => {
|
||||||
|
it('should render workflow name with tooltip', () => {
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
const row1 = screen.getByTestId('workflow-row-workflow-1');
|
||||||
|
expect(within(row1).getByText('Test Workflow 1')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should track telemetry on workflow name click', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
const row1 = screen.getByTestId('workflow-row-workflow-1');
|
||||||
|
const workflowLink = within(row1).getByText('Test Workflow 1');
|
||||||
|
await user.click(workflowLink);
|
||||||
|
|
||||||
|
expect(mockTelemetry.track).toHaveBeenCalledWith(
|
||||||
|
'User clicked on workflow from insights table',
|
||||||
|
{
|
||||||
|
workflow_id: 'workflow-1',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('timeSaved slot', () => {
|
||||||
|
it('should show estimate link when timeSaved is 0', () => {
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
const row2 = screen.getByTestId('workflow-row-workflow-2');
|
||||||
|
expect(within(row2).getByText('Estimate')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show formatted value when timeSaved exists', () => {
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
// The actual formatted value will be processed by the utility functions
|
||||||
|
// We just verify the slot is rendered with the item that has timeSaved
|
||||||
|
const row1 = screen.getByTestId('workflow-row-workflow-1');
|
||||||
|
expect(row1).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('projectName slot', () => {
|
||||||
|
it('should render project name with tooltip when projectName exists', () => {
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
const row1 = screen.getByTestId('workflow-row-workflow-1');
|
||||||
|
expect(within(row1).getByText('Test Project 1')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render dash when projectName is null', () => {
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
const row2 = screen.getByTestId('workflow-row-workflow-2');
|
||||||
|
expect(within(row2).getByText('-')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('event delegation', () => {
|
||||||
|
it('should delegate update:options event from N8nDataTableServer', () => {
|
||||||
|
const { emitted } = renderComponent();
|
||||||
|
|
||||||
|
const updateOptions = {
|
||||||
|
page: 1,
|
||||||
|
itemsPerPage: 20,
|
||||||
|
sortBy: [{ id: 'workflowName', desc: false }],
|
||||||
|
};
|
||||||
|
|
||||||
|
emitters.n8nDataTableServer.emit('update:options', updateOptions);
|
||||||
|
|
||||||
|
expect(emitted()).toHaveProperty('update:options');
|
||||||
|
expect(emitted()['update:options'][0]).toEqual([updateOptions]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('telemetry tracking', () => {
|
||||||
|
it('should track sort changes with correct payload', async () => {
|
||||||
|
const { rerender } = renderComponent();
|
||||||
|
|
||||||
|
// Simulate sortBy change
|
||||||
|
await rerender({
|
||||||
|
sortBy: [{ id: 'total', desc: true }],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockTelemetry.track).toHaveBeenCalledWith('User sorted insights table', {
|
||||||
|
sorted_by: [
|
||||||
|
{
|
||||||
|
id: 'total',
|
||||||
|
desc: true,
|
||||||
|
label: 'Prod. executions',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty sortBy array', async () => {
|
||||||
|
const { rerender } = renderComponent();
|
||||||
|
|
||||||
|
// Simulate sortBy change to empty array
|
||||||
|
await rerender({
|
||||||
|
sortBy: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockTelemetry.track).toHaveBeenCalledWith('User sorted insights table', {
|
||||||
|
sorted_by: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('edge cases', () => {
|
||||||
|
it('should handle empty data array', () => {
|
||||||
|
const emptyData = { count: 0, data: [] };
|
||||||
|
|
||||||
|
renderComponent = createComponentRenderer(InsightsTableWorkflows, {
|
||||||
|
pinia: createTestingPinia(),
|
||||||
|
props: {
|
||||||
|
data: emptyData,
|
||||||
|
loading: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
expect(screen.getByTestId('insights-table')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('workflow-row-workflow-1')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle loading state', () => {
|
||||||
|
renderComponent = createComponentRenderer(InsightsTableWorkflows, {
|
||||||
|
pinia: createTestingPinia(),
|
||||||
|
props: {
|
||||||
|
data: mockInsightsData,
|
||||||
|
loading: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
expect(screen.getByTestId('insights-table')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle missing optional props', () => {
|
||||||
|
renderComponent = createComponentRenderer(InsightsTableWorkflows, {
|
||||||
|
pinia: createTestingPinia(),
|
||||||
|
props: {
|
||||||
|
data: mockInsightsData,
|
||||||
|
// loading prop omitted
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
expect(screen.getByTestId('insights-table')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -7,10 +7,8 @@ import {
|
|||||||
transformInsightsTimeSaved,
|
transformInsightsTimeSaved,
|
||||||
} from '@/features/insights/insights.utils';
|
} from '@/features/insights/insights.utils';
|
||||||
import type { InsightsByWorkflow } from '@n8n/api-types';
|
import type { InsightsByWorkflow } from '@n8n/api-types';
|
||||||
import { N8nTooltip } from '@n8n/design-system';
|
import { N8nTooltip, N8nDataTableServer } from '@n8n/design-system';
|
||||||
import N8nDataTableServer, {
|
import type { TableHeader } from '@n8n/design-system/components/N8nDataTableServer';
|
||||||
type TableHeader,
|
|
||||||
} from '@n8n/design-system/components/N8nDataTableServer/N8nDataTableServer.vue';
|
|
||||||
import { smartDecimal } from '@n8n/utils/number/smartDecimal';
|
import { smartDecimal } from '@n8n/utils/number/smartDecimal';
|
||||||
import { useTelemetry } from '@/composables/useTelemetry';
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
import { VIEWS } from '@/constants';
|
import { VIEWS } from '@/constants';
|
||||||
@@ -34,7 +32,6 @@ const headers = ref<Array<TableHeader<Item>>>([
|
|||||||
title: 'Name',
|
title: 'Name',
|
||||||
key: 'workflowName',
|
key: 'workflowName',
|
||||||
width: 400,
|
width: 400,
|
||||||
disableSort: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: i18n.baseText('insights.banner.title.total'),
|
title: i18n.baseText('insights.banner.title.total'),
|
||||||
|
|||||||
Reference in New Issue
Block a user