mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
feat(core): Add support for partial-match execution filters (#15797)
This commit is contained in:
@@ -175,7 +175,7 @@ export namespace ExecutionSummaries {
|
||||
status: ExecutionStatus[];
|
||||
workflowId: string;
|
||||
waitTill: boolean;
|
||||
metadata: Array<{ key: string; value: string }>;
|
||||
metadata: Array<{ key: string; value: string; exactMatch?: boolean }>;
|
||||
startedAfter: string;
|
||||
startedBefore: string;
|
||||
annotationTags: string[]; // tag IDs
|
||||
@@ -341,7 +341,7 @@ export interface IGetExecutionsQueryFilter {
|
||||
workflowId?: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
waitTill?: FindOperator<any> | boolean;
|
||||
metadata?: Array<{ key: string; value: string }>;
|
||||
metadata?: Array<{ key: string; value: string; exactMatch?: boolean }>;
|
||||
startedAfter?: string;
|
||||
startedBefore?: string;
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ export interface IGetExecutionsQueryFilter {
|
||||
workflowId?: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
waitTill?: FindOperator<any> | boolean;
|
||||
metadata?: Array<{ key: string; value: string }>;
|
||||
metadata?: Array<{ key: string; value: string; exactMatch?: boolean }>;
|
||||
startedAfter?: string;
|
||||
startedBefore?: string;
|
||||
}
|
||||
@@ -88,7 +88,11 @@ function parseFiltersToQueryBuilder(
|
||||
if (filters?.metadata) {
|
||||
qb.leftJoin(ExecutionMetadata, 'md', 'md.executionId = execution.id');
|
||||
for (const md of filters.metadata) {
|
||||
qb.andWhere('md.key = :key AND md.value = :value', md);
|
||||
if (md.exactMatch) {
|
||||
qb.andWhere('md.key = :key AND md.value = :value', md);
|
||||
} else {
|
||||
qb.andWhere('md.key = :key AND LOWER(md.value) LIKE LOWER(:value)', md);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (filters?.startedAfter) {
|
||||
@@ -983,16 +987,18 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
|
||||
if (startedAfter) qb.andWhere({ startedAt: moreThanOrEqual(startedAfter) });
|
||||
|
||||
if (metadata?.length === 1) {
|
||||
const [{ key, value }] = metadata;
|
||||
const [{ key, value, exactMatch }] = metadata;
|
||||
|
||||
qb.innerJoin(
|
||||
ExecutionMetadata,
|
||||
'md',
|
||||
'md.executionId = execution.id AND md.key = :key AND md.value = :value',
|
||||
);
|
||||
const executionIdMatch = 'md.executionId = execution.id';
|
||||
const keyMatch = exactMatch ? 'md.key = :key' : 'LOWER(md.key) = LOWER(:key)';
|
||||
const valueMatch = exactMatch ? 'md.value = :value' : 'LOWER(md.value) LIKE LOWER(:value)';
|
||||
|
||||
const matches = [executionIdMatch, keyMatch, valueMatch];
|
||||
|
||||
qb.innerJoin(ExecutionMetadata, 'md', matches.join(' AND '));
|
||||
|
||||
qb.setParameter('key', key);
|
||||
qb.setParameter('value', value);
|
||||
qb.setParameter('value', exactMatch ? value : `%${value}%`);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
|
||||
@@ -80,6 +80,10 @@ export const schemaGetExecutionsQueryFilter = {
|
||||
type: 'string',
|
||||
},
|
||||
value: { type: 'string' },
|
||||
exactMatch: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -244,7 +248,7 @@ export class ExecutionService {
|
||||
|
||||
async delete(req: ExecutionRequest.Delete, sharedWorkflowIds: string[]) {
|
||||
const { deleteBefore, ids, filters: requestFiltersRaw } = req.body;
|
||||
let requestFilters;
|
||||
let requestFilters: IGetExecutionsQueryFilter | undefined;
|
||||
if (requestFiltersRaw) {
|
||||
try {
|
||||
Object.keys(requestFiltersRaw).map((key) => {
|
||||
|
||||
@@ -270,21 +270,22 @@ describe('ExecutionService', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test('should filter executions by `metadata`', async () => {
|
||||
test('should filter executions by `metadata` with an exact match by default', async () => {
|
||||
const workflow = await createWorkflow();
|
||||
|
||||
const metadata = [{ key: 'myKey', value: 'myValue' }];
|
||||
const key = 'myKey';
|
||||
const value = 'myValue';
|
||||
|
||||
await Promise.all([
|
||||
createExecution({ status: 'success', metadata }, workflow),
|
||||
createExecution({ status: 'error' }, workflow),
|
||||
createExecution({ status: 'success', metadata: [{ key, value }] }, workflow),
|
||||
createExecution({ status: 'error', metadata: [{ key, value: `${value}2` }] }, workflow),
|
||||
]);
|
||||
|
||||
const query: ExecutionSummaries.RangeQuery = {
|
||||
kind: 'range',
|
||||
range: { limit: 20 },
|
||||
accessibleWorkflowIds: [workflow.id],
|
||||
metadata,
|
||||
metadata: [{ key, value, exactMatch: true }],
|
||||
};
|
||||
|
||||
const output = await executionService.findRangeWithCount(query);
|
||||
@@ -296,6 +297,36 @@ describe('ExecutionService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('should filter executions by `metadata` with a partial match', async () => {
|
||||
const workflow = await createWorkflow();
|
||||
|
||||
const key = 'myKey';
|
||||
|
||||
await Promise.all([
|
||||
createExecution({ status: 'success', metadata: [{ key, value: 'myValue' }] }, workflow),
|
||||
createExecution({ status: 'error', metadata: [{ key, value: 'var' }] }, workflow),
|
||||
createExecution({ status: 'success', metadata: [{ key, value: 'evaluation' }] }, workflow),
|
||||
]);
|
||||
|
||||
const query: ExecutionSummaries.RangeQuery = {
|
||||
kind: 'range',
|
||||
range: { limit: 20 },
|
||||
accessibleWorkflowIds: [workflow.id],
|
||||
metadata: [{ key, value: 'val', exactMatch: false }],
|
||||
};
|
||||
|
||||
const output = await executionService.findRangeWithCount(query);
|
||||
|
||||
expect(output).toEqual({
|
||||
count: 2,
|
||||
estimated: false,
|
||||
results: [
|
||||
expect.objectContaining({ status: 'success' }),
|
||||
expect.objectContaining({ status: 'success' }),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('should filter executions by `projectId`', async () => {
|
||||
const firstProject = await createTeamProject();
|
||||
const secondProject = await createTeamProject();
|
||||
|
||||
@@ -877,9 +877,10 @@
|
||||
"executionsFilter.startDate": "Earliest",
|
||||
"executionsFilter.endDate": "Latest",
|
||||
"executionsFilter.savedData": "Highlighted data",
|
||||
"executionsFilter.savedDataExactMatch": "Exact match",
|
||||
"executionsFilter.savedDataKey": "Key",
|
||||
"executionsFilter.savedDataKeyPlaceholder": "ID",
|
||||
"executionsFilter.savedDataValue": "Value (exact match)",
|
||||
"executionsFilter.savedDataValue": "Value",
|
||||
"executionsFilter.savedDataValuePlaceholder": "123",
|
||||
"executionsFilter.reset": "Reset all",
|
||||
"executionsFilter.customData.inputTooltip": "Upgrade plan to filter executions by custom data set at runtime. {link}",
|
||||
|
||||
@@ -1344,6 +1344,7 @@ export interface EnvironmentVariable {
|
||||
export type ExecutionFilterMetadata = {
|
||||
key: string;
|
||||
value: string;
|
||||
exactMatch?: boolean;
|
||||
};
|
||||
|
||||
export type ExecutionFilterVote = AnnotationVote | 'all';
|
||||
|
||||
@@ -17,7 +17,7 @@ const defaultFilterState: ExecutionFilterType = {
|
||||
annotationTags: [],
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
metadata: [{ key: '', value: '' }],
|
||||
metadata: [{ key: '', value: '', exactMatch: false }],
|
||||
vote: 'all',
|
||||
};
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ const getDefaultFilter = (): ExecutionFilterType => ({
|
||||
annotationTags: [],
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
metadata: [{ key: '', value: '' }],
|
||||
metadata: [{ key: '', value: '', exactMatch: false }],
|
||||
vote: 'all',
|
||||
});
|
||||
const filter = reactive(getDefaultFilter());
|
||||
@@ -116,11 +116,16 @@ const countSelectedFilterProps = computed(() => {
|
||||
|
||||
// vModel.metadata is a text input and needs a debounced emit to avoid too many requests
|
||||
// We use the :value and @input combo instead of v-model with this event listener
|
||||
const onFilterMetaChange = (index: number, prop: keyof ExecutionFilterMetadata, value: string) => {
|
||||
const onFilterMetaChange = <K extends keyof ExecutionFilterMetadata>(
|
||||
index: number,
|
||||
prop: K,
|
||||
value: ExecutionFilterMetadata[K],
|
||||
) => {
|
||||
if (!filter.metadata[index]) {
|
||||
filter.metadata[index] = {
|
||||
key: '',
|
||||
value: '',
|
||||
exactMatch: false,
|
||||
};
|
||||
}
|
||||
filter.metadata[index][prop] = value;
|
||||
@@ -327,6 +332,26 @@ onBeforeMount(() => {
|
||||
@update:model-value="onFilterMetaChange(0, 'key', $event)"
|
||||
/>
|
||||
</n8n-tooltip>
|
||||
<div :class="$style.checkboxWrapper">
|
||||
<n8n-tooltip :disabled="isAdvancedExecutionFilterEnabled" placement="top">
|
||||
<template #content>
|
||||
<i18n-t tag="span" keypath="executionsFilter.customData.inputTooltip">
|
||||
<template #link>
|
||||
<a href="#" @click.prevent="goToUpgrade">{{
|
||||
locale.baseText('executionsFilter.customData.inputTooltip.link')
|
||||
}}</a>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</template>
|
||||
<n8n-checkbox
|
||||
:label="locale.baseText('executionsFilter.savedDataExactMatch')"
|
||||
:model-value="filter.metadata[0]?.exactMatch"
|
||||
:disabled="!isAdvancedExecutionFilterEnabled"
|
||||
data-test-id="execution-filter-saved-data-exact-match-checkbox"
|
||||
@update:model-value="onFilterMetaChange(0, 'exactMatch', $event)"
|
||||
/>
|
||||
</n8n-tooltip>
|
||||
</div>
|
||||
<label for="execution-filter-saved-data-value">{{
|
||||
locale.baseText('executionsFilter.savedDataValue')
|
||||
}}</label>
|
||||
@@ -373,6 +398,7 @@ onBeforeMount(() => {
|
||||
display: inline-block;
|
||||
font-size: var(--font-size-2xs);
|
||||
margin: var(--spacing-s) 0 var(--spacing-3xs);
|
||||
color: var(--color-text-dark);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,6 +410,15 @@ onBeforeMount(() => {
|
||||
font-size: var(--font-size-3xs);
|
||||
margin: var(--spacing-4xs) 0 var(--spacing-4xs);
|
||||
}
|
||||
|
||||
.checkboxWrapper {
|
||||
margin-top: var(--spacing-s);
|
||||
margin-bottom: var(--spacing-2xs);
|
||||
|
||||
label {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dates {
|
||||
|
||||
Reference in New Issue
Block a user