blob: de990349b17de0570ac9ee972cc07d8e1def8f24 [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { FormInstance } from 'antd/lib/form';
import shortid from 'shortid';
import { getInitialDataMask } from 'src/dataMask/reducer';
import { FilterRemoval, NativeFiltersForm } from './types';
import { Filter, FilterConfiguration, Target } from '../types';
export const REMOVAL_DELAY_SECS = 5;
export const validateForm = async (
form: FormInstance<NativeFiltersForm>,
currentFilterId: string,
filterConfigMap: Record<string, Filter>,
filterIds: string[],
removedFilters: Record<string, FilterRemoval>,
setCurrentFilterId: Function,
) => {
const addValidationError = (
filterId: string,
field: string,
error: string,
) => {
const fieldError = {
name: ['filters', filterId, field],
errors: [error],
};
form.setFields([fieldError]);
};
try {
let formValues: NativeFiltersForm;
try {
formValues = (await form.validateFields()) as NativeFiltersForm;
} catch (error) {
// In Jest tests in chain of tests, Ant generate `outOfDate` error so need to catch it here
if (!error?.errorFields?.length && error?.outOfDate) {
formValues = error.values;
} else {
throw error;
}
}
const validateCycles = (filterId: string, trace: string[] = []) => {
if (trace.includes(filterId)) {
addValidationError(
filterId,
'parentFilter',
'Cannot create cyclic hierarchy',
);
}
const parentId = formValues.filters[filterId]
? formValues.filters[filterId].parentFilter?.value
: filterConfigMap[filterId]?.cascadeParentIds?.[0];
if (parentId) {
validateCycles(parentId, [...trace, filterId]);
}
};
filterIds
.filter(id => !removedFilters[id])
.forEach(filterId => validateCycles(filterId));
return formValues;
} catch (error) {
console.warn('Filter configuration failed:', error);
if (!error.errorFields || !error.errorFields.length) return null; // not a validation error
// the name is in array format since the fields are nested
type ErrorFields = { name: ['filters', string, string] }[];
const errorFields = error.errorFields as ErrorFields;
// filter id is the second item in the field name
if (!errorFields.some(field => field.name[1] === currentFilterId)) {
// switch to the first tab that had a validation error
const filterError = errorFields.find(
field => field.name[0] === 'filters',
);
if (filterError) {
setCurrentFilterId(filterError.name[1]);
}
}
return null;
}
};
export const createHandleSave = (
filterConfigMap: Record<string, Filter>,
filterIds: string[],
removedFilters: Record<string, FilterRemoval>,
saveForm: Function,
values: NativeFiltersForm,
) => async () => {
const newFilterConfig: FilterConfiguration = filterIds
.filter(id => !removedFilters[id])
.map(id => {
// create a filter config object from the form inputs
const formInputs = values.filters[id];
// if user didn't open a filter, return the original config
if (!formInputs) return filterConfigMap[id];
const target: Partial<Target> = {};
if (formInputs.dataset) {
target.datasetId = formInputs.dataset.value;
}
if (formInputs.dataset && formInputs.column) {
target.column = { name: formInputs.column };
}
return {
id,
adhoc_filters: formInputs.adhoc_filters,
time_range: formInputs.time_range,
controlValues: formInputs.controlValues ?? {},
granularity_sqla: formInputs.granularity_sqla,
requiredFirst: Object.values(formInputs.requiredFirst ?? {}).find(
rf => rf,
),
name: formInputs.name,
filterType: formInputs.filterType,
// for now there will only ever be one target
targets: [target],
defaultDataMask: formInputs.defaultDataMask ?? getInitialDataMask(),
cascadeParentIds: formInputs.parentFilter
? [formInputs.parentFilter.value]
: [],
scope: formInputs.scope,
sortMetric: formInputs.sortMetric,
type: formInputs.type,
};
});
await saveForm(newFilterConfig);
};
export const createHandleTabEdit = (
setRemovedFilters: (
value:
| ((
prevState: Record<string, FilterRemoval>,
) => Record<string, FilterRemoval>)
| Record<string, FilterRemoval>,
) => void,
setSaveAlertVisible: Function,
addFilter: Function,
) => (filterId: string, action: 'add' | 'remove') => {
const completeFilterRemoval = (filterId: string) => {
// the filter state will actually stick around in the form,
// and the filterConfig/newFilterIds, but we use removedFilters
// to mark it as removed.
setRemovedFilters(removedFilters => ({
...removedFilters,
[filterId]: { isPending: false },
}));
};
if (action === 'remove') {
// first set up the timer to completely remove it
const timerId = window.setTimeout(
() => completeFilterRemoval(filterId),
REMOVAL_DELAY_SECS * 1000,
);
// mark the filter state as "removal in progress"
setRemovedFilters(removedFilters => ({
...removedFilters,
[filterId]: { isPending: true, timerId },
}));
setSaveAlertVisible(false);
} else if (action === 'add') {
addFilter();
}
};
export const NATIVE_FILTER_PREFIX = 'NATIVE_FILTER-';
export const generateFilterId = () =>
`${NATIVE_FILTER_PREFIX}${shortid.generate()}`;
export const getFilterIds = (config: FilterConfiguration) =>
config.map(filter => filter.id);