Superset二次开发之源码DependencyList.tsx 分析
功能点
路径
superset-frontend\src\dashboard\components\nativeFilters\FiltersConfigModal\FiltersConfigForm\DependencyList.tsx
/**
* 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, { useState } from 'react';
import { styled, t } from '@superset-ui/core';
import Icons from 'src/components/Icons';
import { Select } from 'src/components';
import { CollapsibleControl } from './CollapsibleControl';
import { INPUT_WIDTH } from './constants';
interface DependencyListProps {
availableFilters: {
label: string;
value: string;
type: string | undefined;
}[];
dependencies: string[];
onDependenciesChange: (dependencies: string[]) => void;
getDependencySuggestion: () => string;
children?: JSX.Element;
}
const MainPanel = styled.div`
display: flex;
flex-direction: column;
`;
const AddFilter = styled.div`
${({ theme }) => `
display: inline-flex;
flex-direction: row;
align-items: center;
cursor: pointer;
color: ${theme.colors.primary.base};
&:hover {
color: ${theme.colors.primary.dark1};
}
`}
`;
const DeleteFilter = styled(Icons.Trash)`
${({ theme }) => `
cursor: pointer;
margin-left: ${theme.gridUnit * 2}px;
color: ${theme.colors.grayscale.base};
&:hover {
color: ${theme.colors.grayscale.dark1};
}
`}
`;
const RowPanel = styled.div`
${({ theme }) => `
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: ${theme.gridUnit}px;
& > div {
width: ${INPUT_WIDTH}px;
}
`}
`;
const Label = styled.div`
text-transform: uppercase;
font-size: ${({ theme }) => theme.typography.sizes.s}px;
color: ${({ theme }) => theme.colors.grayscale.base};
margin-bottom: ${({ theme }) => theme.gridUnit}px;
`;
const Row = ({
availableFilters,
selection,
onChange,
onDelete,
}: {
availableFilters: { label: string; value: string }[];
selection: string;
onChange: (id: string, value: string) => void;
onDelete: (id: string) => void;
}) => {
let value = availableFilters.find(e => e.value === selection);
let options = availableFilters;
if (!value) {
value = { label: t('(deleted or invalid type)'), value: selection };
options = [value, ...options];
}
return (
<RowPanel>
<Select
ariaLabel={t('Limit type')}
labelInValue
options={options}
onChange={option =>
onChange(selection, (option as { value: string }).value)
}
value={value}
/>
<DeleteFilter iconSize="xl" onClick={() => onDelete(selection)} />
</RowPanel>
);
};
const List = ({
availableFilters = [],
dependencies = [],
onDependenciesChange,
}: DependencyListProps) => {
const [rows, setRows] = useState<string[]>(dependencies);
const updateRows = (newRows: string[]) => {
setRows(newRows);
onDependenciesChange(newRows);
};
const onAdd = () => {
const filter = availableFilters.find(
availableFilter => !rows.includes(availableFilter.value),
);
if (filter) {
const newRows = [...rows];
newRows.push(filter.value);
updateRows(newRows);
}
};
const onChange = (id: string, value: string) => {
const indexOf = rows.findIndex(row => row === id);
const newRows = [...rows];
newRows[indexOf] = value;
updateRows(newRows);
};
const onDelete = (id: string) => {
const newRows = [...rows];
newRows.splice(rows.indexOf(id), 1);
updateRows(newRows);
};
if (availableFilters.length === 0) {
return <span>{t('No available filters.')}</span>;
}
return (
<>
{rows.map(row => (
<Row
key={row}
selection={row}
availableFilters={availableFilters.filter(
e => e.value === row || !rows.includes(e.value),
)}
onChange={onChange}
onDelete={onDelete}
/>
))}
{availableFilters.length > rows.length && (
<AddFilter onClick={onAdd}>
<Icons.PlusSmall />
{t('Add filter')}
</AddFilter>
)}
</>
);
};
const DependencyList = ({
availableFilters = [],
dependencies = [],
onDependenciesChange,
getDependencySuggestion,
children,
}: DependencyListProps) => {
const hasAvailableFilters = availableFilters.length > 0;
const hasDependencies = dependencies.length > 0;
const onCheckChanged = (value: boolean) => {
const newDependencies: string[] = [];
if (value && !hasDependencies && hasAvailableFilters) {
newDependencies.push(getDependencySuggestion());
}
onDependenciesChange(newDependencies);
};
return (
<MainPanel>
<CollapsibleControl
title={t('Values are dependent on other filters')}
initialValue={hasDependencies}
onChange={onCheckChanged}
tooltip={t(
'Values selected in other filters will affect the filter options to only show relevant values',
)}
>
{hasDependencies && <Label>{t('Values dependent on')}</Label>}
<List
availableFilters={availableFilters}
dependencies={dependencies}
onDependenciesChange={onDependenciesChange}
getDependencySuggestion={getDependencySuggestion}
/>
{children}
</CollapsibleControl>
</MainPanel>
);
};
export default DependencyList;
组件功能:
- 显示当前过滤器所依赖的其他过滤器列表
- 允许用户添加或删除依赖关系
- 提供依赖关系的可视化展示
主要组件结构:
export const DependencyList: React.FC<DependencyListProps> = ({
dependencies = [],
onRemove,
onAdd,
getFilterTitle,
}) => {
// ... 组件实现
}
这个组件接收依赖列表、删除和添加依赖的回调函数,以及获取过滤器标题的函数作为props。
依赖项渲染:
{dependencies.map(dependency => (
<StyledItem key={dependency}>
<StyledItemContent>
<FilterValue>{getFilterTitle(dependency)}</FilterValue>
</StyledItemContent>
<StyledTrashIcon
name="trash"
onClick={() => onRemove(dependency)}
/>
</StyledItem>
))}
这段代码遍历依赖列表,为每个依赖项渲染一个包含标题和删除图标的项目。
添加依赖功能
<StyledAdd onClick={onAdd}>
<PlusOutlined />
<span>{t('Add Dependent')}</span>
</StyledAdd>
这部分代码渲染了一个"添加依赖"按钮,点击时触发onAdd回调。
样式和布局
文件中使用了多个样式化组件(如StyledItem, StyledItemContent等)来定制组件的外观。
国际化
使用t函数进行文本国际化,支持多语言
类型定义
type DependencyListProps = {
dependencies?: string[];
onRemove: (id: string) => void;
onAdd: () => void;
getFilterTitle: (id: string) => string;
};
总结
这个DependencyList组件是实现"Values are dependent on other filters"逻辑的重要部分。它提供了一个用户界面,允许配置和管理过滤器之间的依赖关系。通过这个组件,用户可以直观地看到和修改过滤器的依赖结构,从而实现动态且相互关联的过滤系统