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:
Csaba Tuncsik
2023-03-23 18:07:46 +01:00
committed by GitHub
parent 4c583e2be4
commit d78a41db54
30 changed files with 1430 additions and 269 deletions

View File

@@ -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();
});
});

View File

@@ -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) =>