blob: 4ae5413741ebb501345d36e86a6eca0e72783419 [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 React, { useCallback, useMemo, useState } from 'react';
import { uniq } from 'lodash';
import { t, styled } from '@superset-ui/core';
import { Form } from 'src/common/components';
import { StyledModal } from 'src/components/Modal';
import ErrorBoundary from 'src/components/ErrorBoundary';
import { useFilterConfigMap, useFilterConfiguration } from '../state';
import { FilterRemoval, NativeFiltersForm } from './types';
import { FilterConfiguration } from '../types';
import {
createHandleSave,
createHandleTabEdit,
generateFilterId,
getFilterIds,
} from './utils';
import Footer from './Footer/Footer';
import FilterTabs from './FilterTabs';
import FiltersConfigForm from './FiltersConfigForm/FiltersConfigForm';
import { useOpenModal, useRemoveCurrentFilter } from './state';
export const StyledModalBody = styled.div`
display: flex;
flex-direction: row;
.filters-list {
width: ${({ theme }) => theme.gridUnit * 50}px;
overflow: auto;
}
`;
export const StyledForm = styled(Form)`
width: 100%;
`;
export interface FiltersConfigModalProps {
isOpen: boolean;
initialFilterId?: string;
createNewOnOpen?: boolean;
onSave: (filterConfig: FilterConfiguration) => Promise<void>;
onCancel: () => void;
}
/**
* This is the modal to configure all the dashboard-native filters.
* Manages modal-level state, such as what filters are in the list,
* and which filter is currently being edited.
*
* Calls the `save` callback with the new FilterConfiguration object
* when the user saves the filters.
*/
export function FiltersConfigModal({
isOpen,
initialFilterId,
createNewOnOpen,
onSave,
onCancel,
}: FiltersConfigModalProps) {
const [form] = Form.useForm<NativeFiltersForm>();
// the filter config from redux state, this does not change until modal is closed.
const filterConfig = useFilterConfiguration();
const filterConfigMap = useFilterConfigMap();
// new filter ids belong to filters have been added during
// this configuration session, and only exist in the form state until we submit.
const [newFilterIds, setNewFilterIds] = useState<string[]>([]);
// store ids of filters that have been removed with the time they were removed
// so that we can disappear them after a few secs.
// filters are still kept in state until form is submitted.
const [removedFilters, setRemovedFilters] = useState<
Record<string, FilterRemoval>
>({});
const [saveAlertVisible, setSaveAlertVisible] = useState<boolean>(false);
// The full ordered set of ((original + new) - completely removed) filter ids
// Use this as the canonical list of what filters are being configured!
// This includes filter ids that are pending removal, so check for that.
const filterIds = useMemo(
() =>
uniq([...getFilterIds(filterConfig), ...newFilterIds]).filter(
id => !removedFilters[id] || removedFilters[id]?.isPending,
),
[filterConfig, newFilterIds, removedFilters],
);
// open the first filter in the list to start
const initialCurrentFilterId = initialFilterId ?? filterIds[0];
const [currentFilterId, setCurrentFilterId] = useState(
initialCurrentFilterId,
);
// the form values are managed by the antd form, but we copy them to here
// so that we can display them (e.g. filter titles in the tab headers)
const [formValues, setFormValues] = useState<NativeFiltersForm>({
filters: {},
});
const unsavedFiltersIds = newFilterIds.filter(id => !removedFilters[id]);
// brings back a filter that was previously removed ("Undo")
const restoreFilter = (id: string) => {
const removal = removedFilters[id];
// gotta clear the removal timeout to prevent the filter from getting deleted
if (removal?.isPending) clearTimeout(removal.timerId);
setRemovedFilters(current => ({ ...current, [id]: null }));
};
// generates a new filter id and appends it to the newFilterIds
const addFilter = useCallback(() => {
const newFilterId = generateFilterId();
setNewFilterIds([...newFilterIds, newFilterId]);
setCurrentFilterId(newFilterId);
setSaveAlertVisible(false);
}, [newFilterIds, setCurrentFilterId]);
useOpenModal(isOpen, addFilter, createNewOnOpen);
useRemoveCurrentFilter(
removedFilters,
currentFilterId,
filterIds,
setCurrentFilterId,
);
const handleTabEdit = createHandleTabEdit(
setRemovedFilters,
setSaveAlertVisible,
addFilter,
);
// After this, it should be as if the modal was just opened fresh.
// Called when the modal is closed.
const resetForm = () => {
form.resetFields();
setNewFilterIds([]);
setCurrentFilterId(initialCurrentFilterId);
setRemovedFilters({});
setSaveAlertVisible(false);
};
const getFilterTitle = (id: string) =>
formValues.filters[id]?.name ??
filterConfigMap[id]?.name ??
t('New filter');
const getParentFilters = (id: string) =>
filterIds
.filter(filterId => filterId !== id && !removedFilters[filterId])
.map(id => ({
id,
title: getFilterTitle(id),
}));
const handleSave = createHandleSave(
form,
currentFilterId,
filterConfigMap,
filterIds,
removedFilters,
setCurrentFilterId,
resetForm,
onSave,
);
const handleConfirmCancel = () => {
resetForm();
onCancel();
};
const handleCancel = () => {
if (unsavedFiltersIds.length > 0) {
setSaveAlertVisible(true);
} else {
handleConfirmCancel();
}
};
return (
<StyledModal
visible={isOpen}
maskClosable={false}
title={t('Filters configuration and scoping')}
width="55%"
destroyOnClose
onCancel={handleCancel}
onOk={handleSave}
centered
data-test="filter-modal"
footer={
<Footer
onDismiss={() => setSaveAlertVisible(false)}
onCancel={handleCancel}
getFilterTitle={getFilterTitle}
handleSave={handleSave}
saveAlertVisible={saveAlertVisible}
unsavedFiltersIds={unsavedFiltersIds}
onConfirmCancel={handleConfirmCancel}
/>
}
>
<ErrorBoundary>
<StyledModalBody>
<StyledForm
preserve={false}
form={form}
onValuesChange={(changes, values: NativeFiltersForm) => {
if (
changes.filters &&
Object.values(changes.filters).some(
(filter: any) => filter.name != null,
)
) {
// we only need to set this if a name changed
setFormValues(values);
}
setSaveAlertVisible(false);
}}
layout="vertical"
>
<FilterTabs
onEdit={handleTabEdit}
onChange={setCurrentFilterId}
getFilterTitle={getFilterTitle}
currentFilterId={currentFilterId}
filterIds={filterIds}
removedFilters={removedFilters}
restoreFilter={restoreFilter}
>
{(id: string) => (
<FiltersConfigForm
form={form}
filterId={id}
filterToEdit={filterConfigMap[id]}
removed={!!removedFilters[id]}
restoreFilter={restoreFilter}
parentFilters={getParentFilters(id)}
/>
)}
</FilterTabs>
</StyledForm>
</StyledModalBody>
</ErrorBoundary>
</StyledModal>
);
}