feat: Execution custom data saving and filtering (#5496)
* wip: workflow execution filtering * fix: import type failing to build * fix: remove console.logs * feat: execution metadata migrations * fix(editor): Move global executions filter to its own component * fix(editor): Using the same filter component in workflow level * fix(editor): a small housekeeping * checking workflowId in filter applied * fix(editor): update filter after resolving merge conflicts * fix(editor): unify empy filter status * feat(editor): add datetime picker to filter * feat(editor): add meta fields * fix: fix button override in datepicker panel * feat(editor): add filter metadata * feat(core): add 'startedBefore' execution filter prop * feat(core): add 'tags' execution query filter * Revert "feat(core): add 'tags' execution query filter" This reverts commit a7b968081c91290b0c94df18c6a73d29950222d9. * feat(editor): add translations and tooltip and counting selected filter props * fix(editor): fix label layouts * fix(editor): update custom data docs link * fix(editor): update custom data tooltip position * fix(editor): update tooltip text * refactor: Ignore metadata if not enabled by license * fix(editor): Add paywall states to advanced execution filter * refactor: Save custom data also for worker mode * fix: Remove duplicate migration name from list * fix(editor): Reducing filter complexity and add debounce to text inputs * fix(editor): Remove unused import, add comment * fix(editor): simplify event listener * fix: Prevent error when there are running executions * test(editor): Add advanced execution filter basic unit test * test(editor): Add advanced execution filter state change unit test * fix: Small lint issue * feat: Add indices to speed up queries * feat: add customData limits * refactor: put metadata save in transaction * chore: remove unneed comment * test: add tests for execution metadata * fix(editor): Fixes after merge conflict * fix(editor): Remove unused import * wordings and ui fixes * fix(editor): type fixes * feat: add code node autocompletions for customData * fix: Prevent transaction issues and ambiguous ID in sql clauses * fix(editor): Suppress requesting current executions if metadata is used in filter (#5739) * fix(editor): Suppress requesting current executions if metadata is used in filter * fix(editor): Fix arrows for select in popover * refactor: Improve performance by correcting database indices * fix: Lint issue * test: Fix broken test * fix: Broken test * test: add call data check for saveExecutionMetadata test --------- Co-authored-by: Valya Bullions <valya@n8n.io> Co-authored-by: Alex Grozav <alex@grozav.com> Co-authored-by: Omar Ajoue <krynble@gmail.com> Co-authored-by: Romain Minaud <romain.minaud@gmail.com>
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import Vue from 'vue';
|
||||
import { PiniaVuePlugin } from 'pinia';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { render, RenderOptions } from '@testing-library/vue';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import ExecutionFilter from '@/components/ExecutionFilter.vue';
|
||||
import { STORES } from '@/constants';
|
||||
import { i18nInstance } from '@/plugins/i18n';
|
||||
import type { IWorkflowShortResponse, ExecutionFilterType } from '@/Interface';
|
||||
|
||||
Vue.use(PiniaVuePlugin);
|
||||
|
||||
const CLOUD_HOST = 'https://app.n8n.cloud';
|
||||
const PRODUCTION_SUBSCRIPTION_HOST = 'https://subscription.n8n.io';
|
||||
const DEVELOPMENT_SUBSCRIPTION_HOST = 'https://staging-subscription.n8n.io';
|
||||
|
||||
const defaultFilterState: ExecutionFilterType = {
|
||||
status: 'all',
|
||||
workflowId: 'all',
|
||||
tags: [],
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
metadata: [{ key: '', value: '' }],
|
||||
};
|
||||
|
||||
const workflowDataFactory = (): IWorkflowShortResponse => ({
|
||||
createdAt: faker.date.past().toDateString(),
|
||||
updatedAt: faker.date.past().toDateString(),
|
||||
id: faker.datatype.uuid(),
|
||||
name: faker.datatype.string(),
|
||||
active: faker.datatype.boolean(),
|
||||
tags: [],
|
||||
});
|
||||
|
||||
const workflowsData = Array.from({ length: 10 }, workflowDataFactory);
|
||||
|
||||
const initialState = {
|
||||
[STORES.SETTINGS]: {
|
||||
settings: {
|
||||
templates: {
|
||||
enabled: true,
|
||||
host: 'https://api.n8n.io/api/',
|
||||
},
|
||||
license: {
|
||||
environment: 'development',
|
||||
},
|
||||
deployment: {
|
||||
type: 'default',
|
||||
},
|
||||
enterprise: {
|
||||
advancedExecutionFilters: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const renderOptions: RenderOptions<ExecutionFilter> = {
|
||||
i18n: i18nInstance,
|
||||
};
|
||||
|
||||
describe('ExecutionFilter', () => {
|
||||
test.each([
|
||||
['development', 'default', DEVELOPMENT_SUBSCRIPTION_HOST, false, workflowsData],
|
||||
['development', 'default', '', true, workflowsData],
|
||||
['development', 'cloud', CLOUD_HOST, false, undefined],
|
||||
['development', 'cloud', '', true, undefined],
|
||||
['production', 'cloud', CLOUD_HOST, false, workflowsData],
|
||||
['production', 'cloud', '', true, undefined],
|
||||
['production', 'default', PRODUCTION_SUBSCRIPTION_HOST, false, undefined],
|
||||
['production', 'default', '', true, workflowsData],
|
||||
])(
|
||||
'renders in %s environment on %s deployment with advancedExecutionFilters %s and workflows %s',
|
||||
async (environment, deployment, plansLinkUrlBase, advancedExecutionFilters, workflows) => {
|
||||
initialState[STORES.SETTINGS].settings.license.environment = environment;
|
||||
initialState[STORES.SETTINGS].settings.deployment.type = deployment;
|
||||
initialState[STORES.SETTINGS].settings.enterprise.advancedExecutionFilters =
|
||||
advancedExecutionFilters;
|
||||
|
||||
renderOptions.pinia = createTestingPinia({ initialState });
|
||||
renderOptions.props = { workflows };
|
||||
|
||||
const { getByTestId, queryByTestId } = render(ExecutionFilter, renderOptions);
|
||||
|
||||
await userEvent.click(getByTestId('executions-filter-button'));
|
||||
await userEvent.hover(getByTestId('execution-filter-saved-data-key-input'));
|
||||
|
||||
if (!advancedExecutionFilters) {
|
||||
expect(getByTestId('executions-filter-view-plans-link').getAttribute('href')).contains(
|
||||
plansLinkUrlBase,
|
||||
);
|
||||
} else {
|
||||
expect(queryByTestId('executions-filter-view-plans-link')).not.toBeInTheDocument();
|
||||
}
|
||||
|
||||
expect(queryByTestId('executions-filter-reset-button')).not.toBeInTheDocument();
|
||||
expect(!!queryByTestId('executions-filter-workflows-select')).toBe(!!workflows?.length);
|
||||
},
|
||||
);
|
||||
|
||||
test('state change', async () => {
|
||||
const { getByTestId, queryByTestId, emitted } = render(ExecutionFilter, renderOptions);
|
||||
|
||||
const filterChangedEvent = emitted().filterChanged;
|
||||
expect(filterChangedEvent).toHaveLength(1);
|
||||
expect(filterChangedEvent[0]).toEqual([defaultFilterState]);
|
||||
|
||||
expect(getByTestId('execution-filter-form')).not.toBeVisible();
|
||||
expect(queryByTestId('executions-filter-reset-button')).not.toBeInTheDocument();
|
||||
expect(queryByTestId('execution-filter-badge')).not.toBeInTheDocument();
|
||||
|
||||
await userEvent.click(getByTestId('executions-filter-button'));
|
||||
expect(getByTestId('execution-filter-form')).toBeVisible();
|
||||
|
||||
await userEvent.click(getByTestId('executions-filter-status-select'));
|
||||
await userEvent.click(getByTestId('executions-filter-status-select').querySelectorAll('li')[1]);
|
||||
|
||||
expect(emitted().filterChanged).toHaveLength(2);
|
||||
expect(filterChangedEvent[1]).toEqual([{ ...defaultFilterState, status: 'error' }]);
|
||||
expect(getByTestId('executions-filter-reset-button')).toBeInTheDocument();
|
||||
expect(getByTestId('execution-filter-badge')).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(getByTestId('executions-filter-reset-button'));
|
||||
expect(emitted().filterChanged).toHaveLength(3);
|
||||
expect(filterChangedEvent[2]).toEqual([defaultFilterState]);
|
||||
expect(queryByTestId('executions-filter-reset-button')).not.toBeInTheDocument();
|
||||
expect(queryByTestId('execution-filter-badge')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -69,6 +69,15 @@ const renderOptions = {
|
||||
enabled: true,
|
||||
host: 'https://api.n8n.io/api/',
|
||||
},
|
||||
license: {
|
||||
environment: 'development',
|
||||
},
|
||||
deployment: {
|
||||
type: 'default',
|
||||
},
|
||||
enterprise: {
|
||||
advancedExecutionFilters: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -137,6 +146,7 @@ describe('ExecutionsList.vue', () => {
|
||||
|
||||
await userEvent.click(getByTestId('load-more-button'));
|
||||
|
||||
expect(getPastExecutionsSpy).toHaveBeenCalledTimes(2);
|
||||
expect(getAllByTestId('select-execution-checkbox').length).toBe(20);
|
||||
expect(
|
||||
getAllByTestId('select-execution-checkbox').filter((el) =>
|
||||
|
||||
Reference in New Issue
Block a user