mirror of
https://github.com/apache/superset.git
synced 2026-06-06 08:09:14 +00:00
feat(native-filters): Implement filter cards (#18874)
* feat(native-filters): Filter cards first pass * Add dependencies row and lots of improvements * Display tooltip correctly * Styling fix * Remove accidentaly added changes * Address comments * Fix type * Fixes
This commit is contained in:
committed by
GitHub
parent
324601e0bb
commit
0922c3ff2d
@@ -44,10 +44,6 @@ const Wrapper = styled.div`
|
||||
padding: ${({ theme }) => theme.gridUnit * 4}px;
|
||||
// 108px padding to make room for buttons with position: absolute
|
||||
padding-bottom: ${({ theme }) => theme.gridUnit * 27}px;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
type FilterControlsProps = {
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* 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, useRef } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { css, t, useTheme } from '@superset-ui/core';
|
||||
import { setDirectPathToChild } from 'src/dashboard/actions/dashboardState';
|
||||
import Icons from 'src/components/Icons';
|
||||
import {
|
||||
DependencyItem,
|
||||
Row,
|
||||
RowLabel,
|
||||
RowTruncationCount,
|
||||
RowValue,
|
||||
TooltipList,
|
||||
} from './Styles';
|
||||
import { useFilterDependencies } from './useFilterDependencies';
|
||||
import { useTruncation } from './useTruncation';
|
||||
import { DependencyValueProps, FilterCardRowProps } from './types';
|
||||
import { TooltipWithTruncation } from './TooltipWithTruncation';
|
||||
|
||||
const DependencyValue = ({ dependency, label }: DependencyValueProps) => {
|
||||
const dispatch = useDispatch();
|
||||
const handleClick = useCallback(() => {
|
||||
dispatch(setDirectPathToChild([dependency.id]));
|
||||
}, [dependency.id, dispatch]);
|
||||
return (
|
||||
<DependencyItem role="button" onClick={handleClick} tabIndex={0}>
|
||||
{label}
|
||||
</DependencyItem>
|
||||
);
|
||||
};
|
||||
|
||||
export const DependenciesRow = React.memo(({ filter }: FilterCardRowProps) => {
|
||||
const dependencies = useFilterDependencies(filter);
|
||||
const dependenciesRef = useRef<HTMLDivElement>(null);
|
||||
const [elementsTruncated, hasHiddenElements] = useTruncation(dependenciesRef);
|
||||
const theme = useTheme();
|
||||
|
||||
const tooltipText = useMemo(
|
||||
() =>
|
||||
elementsTruncated > 0 && dependencies ? (
|
||||
<TooltipList>
|
||||
{dependencies.map(dependency => (
|
||||
<li>
|
||||
<DependencyValue
|
||||
dependency={dependency}
|
||||
label={dependency.name}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</TooltipList>
|
||||
) : null,
|
||||
[elementsTruncated, dependencies],
|
||||
);
|
||||
|
||||
if (!Array.isArray(dependencies) || dependencies.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Row>
|
||||
<RowLabel
|
||||
css={css`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
`}
|
||||
>
|
||||
{t('Dependent on')}{' '}
|
||||
<TooltipWithTruncation
|
||||
title={t(
|
||||
'Filter only displays values relevant to selections made in other filters.',
|
||||
)}
|
||||
>
|
||||
<Icons.Info
|
||||
iconSize="s"
|
||||
iconColor={theme.colors.grayscale.light1}
|
||||
css={css`
|
||||
margin-left: ${theme.gridUnit}px;
|
||||
`}
|
||||
/>
|
||||
</TooltipWithTruncation>
|
||||
</RowLabel>
|
||||
<TooltipWithTruncation title={tooltipText}>
|
||||
<RowValue ref={dependenciesRef}>
|
||||
{dependencies.map((dependency, index) => (
|
||||
<DependencyValue
|
||||
dependency={dependency}
|
||||
label={index === 0 ? dependency.name : `, ${dependency.name}`}
|
||||
/>
|
||||
))}
|
||||
</RowValue>
|
||||
{hasHiddenElements && (
|
||||
<RowTruncationCount>+{elementsTruncated}</RowTruncationCount>
|
||||
)}
|
||||
</TooltipWithTruncation>
|
||||
</Row>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* 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 from 'react';
|
||||
import { Filter } from '@superset-ui/core';
|
||||
import { ScopeRow } from './ScopeRow';
|
||||
import { DependenciesRow } from './DependenciesRow';
|
||||
import { NameRow } from './NameRow';
|
||||
import { TypeRow } from './TypeRow';
|
||||
|
||||
export const FilterCardContent = ({ filter }: { filter: Filter }) => (
|
||||
<div>
|
||||
<NameRow filter={filter} />
|
||||
<TypeRow filter={filter} />
|
||||
<ScopeRow filter={filter} />
|
||||
<DependenciesRow filter={filter} />
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* 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, { useRef } from 'react';
|
||||
import { css, SupersetTheme } from '@superset-ui/core';
|
||||
import Icons from 'src/components/Icons';
|
||||
import { Row, FilterName } from './Styles';
|
||||
import { FilterCardRowProps } from './types';
|
||||
import { useTruncation } from './useTruncation';
|
||||
import { TooltipWithTruncation } from './TooltipWithTruncation';
|
||||
|
||||
export const NameRow = ({ filter }: FilterCardRowProps) => {
|
||||
const filterNameRef = useRef<HTMLElement>(null);
|
||||
const [elementsTruncated] = useTruncation(filterNameRef);
|
||||
return (
|
||||
<Row
|
||||
css={(theme: SupersetTheme) =>
|
||||
css`
|
||||
margin-bottom: ${theme.gridUnit * 3}px;
|
||||
`
|
||||
}
|
||||
>
|
||||
<Icons.FilterSmall
|
||||
css={(theme: SupersetTheme) =>
|
||||
css`
|
||||
margin-right: ${theme.gridUnit}px;
|
||||
`
|
||||
}
|
||||
/>
|
||||
<TooltipWithTruncation title={elementsTruncated ? filter.name : null}>
|
||||
<FilterName ref={filterNameRef}>{filter.name}</FilterName>
|
||||
</TooltipWithTruncation>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* 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, { useMemo, useRef } from 'react';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { useFilterScope } from './useFilterScope';
|
||||
import {
|
||||
Row,
|
||||
RowLabel,
|
||||
RowTruncationCount,
|
||||
RowValue,
|
||||
TooltipList,
|
||||
} from './Styles';
|
||||
import { useTruncation } from './useTruncation';
|
||||
import { FilterCardRowProps } from './types';
|
||||
import { TooltipWithTruncation } from './TooltipWithTruncation';
|
||||
|
||||
export const ScopeRow = React.memo(({ filter }: FilterCardRowProps) => {
|
||||
const scope = useFilterScope(filter);
|
||||
const scopeRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [elementsTruncated, hasHiddenElements] = useTruncation(scopeRef);
|
||||
const tooltipText = useMemo(
|
||||
() =>
|
||||
elementsTruncated > 0 && scope ? (
|
||||
<TooltipList>
|
||||
{scope.map(val => (
|
||||
<li>{val}</li>
|
||||
))}
|
||||
</TooltipList>
|
||||
) : null,
|
||||
[elementsTruncated, scope],
|
||||
);
|
||||
|
||||
if (!Array.isArray(scope) || scope.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Row>
|
||||
<RowLabel>{t('Scope')}</RowLabel>
|
||||
<TooltipWithTruncation title={tooltipText}>
|
||||
<RowValue ref={scopeRef}>
|
||||
{scope.map((element, index) => (
|
||||
<span>{index === 0 ? element : `, ${element}`}</span>
|
||||
))}
|
||||
</RowValue>
|
||||
{hasHiddenElements > 0 && (
|
||||
<RowTruncationCount>+{elementsTruncated}</RowTruncationCount>
|
||||
)}
|
||||
</TooltipWithTruncation>
|
||||
</Row>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* 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 { css, styled } from '@superset-ui/core';
|
||||
|
||||
export const Row = styled.div`
|
||||
${({ theme }) => css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: ${theme.gridUnit}px 0;
|
||||
font-size: ${theme.typography.sizes.s}px;
|
||||
|
||||
&:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
& .ant-tooltip-open {
|
||||
display: inline-flex;
|
||||
}
|
||||
`};
|
||||
`;
|
||||
|
||||
export const RowLabel = styled.span`
|
||||
${({ theme }) => css`
|
||||
color: ${theme.colors.grayscale.base};
|
||||
padding-right: ${theme.gridUnit * 4}px;
|
||||
margin-right: auto;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
`};
|
||||
`;
|
||||
|
||||
export const RowValue = styled.div`
|
||||
${({ theme }) => css`
|
||||
color: ${theme.colors.grayscale.dark1};
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: inline;
|
||||
`};
|
||||
`;
|
||||
|
||||
export const FilterName = styled.span`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export const DependencyItem = styled.span`
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
export const RowTruncationCount = styled.span`
|
||||
${({ theme }) => css`
|
||||
color: ${theme.colors.primary.base};
|
||||
`}
|
||||
`;
|
||||
|
||||
export const TooltipList = styled.ul`
|
||||
${({ theme }) => css`
|
||||
padding-left: ${theme.gridUnit * 3}px;
|
||||
margin-bottom: 0;
|
||||
`};
|
||||
`;
|
||||
|
||||
export const TooltipTrigger = styled.div`
|
||||
min-width: 0;
|
||||
display: inline-flex;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 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 from 'react';
|
||||
import { TooltipProps } from 'antd/lib/tooltip';
|
||||
import { Tooltip } from 'src/components/Tooltip';
|
||||
import { TooltipTrigger } from './Styles';
|
||||
|
||||
export const TooltipWithTruncation = ({
|
||||
title,
|
||||
children,
|
||||
...props
|
||||
}: TooltipProps) => (
|
||||
<Tooltip
|
||||
title={title}
|
||||
placement="bottom"
|
||||
overlayClassName="filter-card-tooltip"
|
||||
{...props}
|
||||
>
|
||||
<TooltipTrigger>{children}</TooltipTrigger>
|
||||
</Tooltip>
|
||||
);
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 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, { useMemo } from 'react';
|
||||
import { getChartMetadataRegistry, t } from '@superset-ui/core';
|
||||
import { Row, RowLabel, RowValue } from './Styles';
|
||||
import { FilterCardRowProps } from './types';
|
||||
|
||||
export const TypeRow = ({ filter }: FilterCardRowProps) => {
|
||||
const metadata = useMemo(
|
||||
() => getChartMetadataRegistry().get(filter.filterType),
|
||||
[filter.filterType],
|
||||
);
|
||||
return (
|
||||
<Row>
|
||||
<RowLabel>{t('Filter type')}</RowLabel>
|
||||
<RowValue>{metadata?.name}</RowValue>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* 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 Popover from 'src/components/Popover';
|
||||
import { FilterCardContent } from './FilterCardContent';
|
||||
import { FilterCardProps } from './types';
|
||||
|
||||
export const FilterCard = ({
|
||||
children,
|
||||
filter,
|
||||
getPopupContainer,
|
||||
isVisible: externalIsVisible = true,
|
||||
}: FilterCardProps) => {
|
||||
const [internalIsVisible, setInternalIsVisible] = useState(false);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
placement="right"
|
||||
overlayClassName="filter-card-popover"
|
||||
mouseEnterDelay={0.2}
|
||||
mouseLeaveDelay={0.2}
|
||||
onVisibleChange={visible => {
|
||||
setInternalIsVisible(visible);
|
||||
}}
|
||||
visible={externalIsVisible && internalIsVisible}
|
||||
content={<FilterCardContent filter={filter} />}
|
||||
getPopupContainer={getPopupContainer ?? (() => document.body)}
|
||||
destroyTooltipOnHide
|
||||
>
|
||||
{children}
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 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 { ReactNode } from 'react';
|
||||
import { Filter } from '@superset-ui/core';
|
||||
|
||||
export interface FilterCardProps {
|
||||
children: ReactNode;
|
||||
filter: Filter;
|
||||
getPopupContainer?: (node: HTMLElement) => HTMLElement;
|
||||
isVisible?: boolean;
|
||||
}
|
||||
|
||||
export interface FilterCardRowProps {
|
||||
filter: Filter;
|
||||
}
|
||||
|
||||
export interface DependencyValueProps {
|
||||
dependency: Filter;
|
||||
label: string;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* 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 { ensureIsArray, Filter } from '@superset-ui/core';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { RootState } from 'src/dashboard/types';
|
||||
|
||||
export const useFilterDependencies = (filter: Filter) => {
|
||||
const filterDependencyIds = ensureIsArray(filter.cascadeParentIds);
|
||||
return useSelector<RootState, Filter[]>(state => {
|
||||
if (filterDependencyIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return filterDependencyIds.reduce((acc: Filter[], filterDependencyId) => {
|
||||
acc.push(state.nativeFilters.filters[filterDependencyId]);
|
||||
return acc;
|
||||
}, []);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* 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 { Filter, t } from '@superset-ui/core';
|
||||
import { useSelector } from 'react-redux';
|
||||
import {
|
||||
ChartsState,
|
||||
Layout,
|
||||
LayoutItem,
|
||||
RootState,
|
||||
} from 'src/dashboard/types';
|
||||
import { DASHBOARD_ROOT_ID } from 'src/dashboard/util/constants';
|
||||
import { CHART_TYPE } from 'src/dashboard/util/componentTypes';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
const extractTabLabel = (tab?: LayoutItem) =>
|
||||
tab?.meta?.text || tab?.meta?.defaultText;
|
||||
const extractChartLabel = (chart?: LayoutItem) =>
|
||||
chart?.meta?.sliceNameOverride || chart?.meta?.sliceName || chart?.id;
|
||||
|
||||
const useCharts = () => {
|
||||
const charts = useSelector<RootState, ChartsState>(state => state.charts);
|
||||
return useMemo(() => Object.values(charts), [charts]);
|
||||
};
|
||||
|
||||
export const useFilterScope = (filter: Filter) => {
|
||||
const layout = useSelector<RootState, Layout>(
|
||||
state => state.dashboardLayout.present,
|
||||
);
|
||||
const charts = useCharts();
|
||||
|
||||
return useMemo(() => {
|
||||
let topLevelTabs: string[] | undefined;
|
||||
const topElementId = layout[DASHBOARD_ROOT_ID].children[0];
|
||||
if (topElementId.startsWith('TABS-')) {
|
||||
topLevelTabs = layout[topElementId].children;
|
||||
}
|
||||
|
||||
// no charts in scope
|
||||
if (filter.scope.rootPath.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// all charts in scope
|
||||
// no charts excluded and no top level tabs
|
||||
// OR no charts excluded and every top level tab is in rootPath
|
||||
if (
|
||||
filter.scope.excluded.length === 0 &&
|
||||
(filter.scope.rootPath[0] === DASHBOARD_ROOT_ID ||
|
||||
(topLevelTabs &&
|
||||
topLevelTabs.every(topLevelTab =>
|
||||
filter.scope.rootPath.includes(topLevelTab),
|
||||
)))
|
||||
) {
|
||||
return [t('All charts')];
|
||||
}
|
||||
|
||||
// no charts excluded and not every top level tab in scope
|
||||
// returns "TAB1, TAB2"
|
||||
if (filter.scope.excluded.length === 0 && topLevelTabs) {
|
||||
return filter.scope.rootPath.map(tabId => extractTabLabel(layout[tabId]));
|
||||
}
|
||||
|
||||
const layoutCharts = Object.values(layout).filter(
|
||||
layoutElement => layoutElement.type === CHART_TYPE,
|
||||
);
|
||||
|
||||
// no top level tabs, charts excluded
|
||||
// returns "CHART1, CHART2"
|
||||
if (filter.scope.rootPath[0] === DASHBOARD_ROOT_ID) {
|
||||
return charts
|
||||
.filter(chart => !filter.scope.excluded.includes(chart.id))
|
||||
.map(chart => {
|
||||
const layoutElement = layoutCharts.find(
|
||||
layoutChart => layoutChart.meta.chartId === chart.id,
|
||||
);
|
||||
return extractChartLabel(layoutElement);
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
// top level tabs, charts excluded
|
||||
// returns "TAB1, TAB2, CHART1"
|
||||
if (topLevelTabs) {
|
||||
// We start assuming that all charts are in scope for all tabs in the root path
|
||||
const topLevelTabsInFullScope = [...filter.scope.rootPath];
|
||||
const layoutChartElementsInTabsInScope = layoutCharts.filter(element =>
|
||||
element.parents.some(parent =>
|
||||
topLevelTabsInFullScope.includes(parent),
|
||||
),
|
||||
);
|
||||
// Exclude the tabs that contain excluded charts
|
||||
filter.scope.excluded.forEach(chartId => {
|
||||
const excludedIndex = topLevelTabsInFullScope.findIndex(tabId =>
|
||||
layoutChartElementsInTabsInScope
|
||||
.find(chart => chart.meta.chartId === chartId)
|
||||
?.parents.includes(tabId),
|
||||
);
|
||||
if (excludedIndex > -1) {
|
||||
topLevelTabsInFullScope.splice(excludedIndex, 1);
|
||||
}
|
||||
});
|
||||
// Handle charts that are in scope but belong to excluded tabs.
|
||||
const chartsInExcludedTabs = charts
|
||||
.filter(chart => !filter.scope.excluded.includes(chart.id))
|
||||
.reduce((acc: LayoutItem[], chart) => {
|
||||
const layoutChartElementInExcludedTab =
|
||||
layoutChartElementsInTabsInScope.find(
|
||||
element =>
|
||||
element.meta.chartId === chart.id &&
|
||||
element.parents.every(
|
||||
parent => !topLevelTabsInFullScope.includes(parent),
|
||||
),
|
||||
);
|
||||
if (layoutChartElementInExcludedTab) {
|
||||
acc.push(layoutChartElementInExcludedTab);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
// Join tab names and chart names
|
||||
return topLevelTabsInFullScope
|
||||
.map(tabId => extractTabLabel(layout[tabId]))
|
||||
.concat(chartsInExcludedTabs.map(extractChartLabel));
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [charts, filter.scope.excluded, filter.scope.rootPath, layout]);
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 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 { RefObject, useLayoutEffect, useState } from 'react';
|
||||
|
||||
export const useTruncation = (elementRef: RefObject<HTMLElement>) => {
|
||||
const [elementsTruncated, setElementsTruncated] = useState(0);
|
||||
const [hasHiddenElements, setHasHiddenElements] = useState(false);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const currentElement = elementRef.current;
|
||||
if (!currentElement) {
|
||||
return;
|
||||
}
|
||||
const { scrollWidth, clientWidth, childNodes } = currentElement;
|
||||
if (scrollWidth > clientWidth) {
|
||||
// "..." is around 6px wide
|
||||
const maxWidth = clientWidth - 6;
|
||||
const elementsCount = childNodes.length;
|
||||
let width = 0;
|
||||
let i = 0;
|
||||
while (width < maxWidth) {
|
||||
width += (childNodes[i] as HTMLElement).offsetWidth;
|
||||
i += 1;
|
||||
}
|
||||
if (i === elementsCount) {
|
||||
setElementsTruncated(1);
|
||||
setHasHiddenElements(false);
|
||||
} else {
|
||||
setElementsTruncated(elementsCount - i);
|
||||
setHasHiddenElements(true);
|
||||
}
|
||||
} else {
|
||||
setElementsTruncated(0);
|
||||
}
|
||||
}, [
|
||||
elementRef.current?.offsetWidth,
|
||||
elementRef.current?.clientWidth,
|
||||
elementRef,
|
||||
]);
|
||||
|
||||
return [elementsTruncated, hasHiddenElements];
|
||||
};
|
||||
@@ -17,8 +17,9 @@
|
||||
* under the License.
|
||||
*/
|
||||
import React, { FC, useRef, useEffect, useState } from 'react';
|
||||
import { FeatureFlag, isFeatureEnabled, t } from '@superset-ui/core';
|
||||
import { FeatureFlag, isFeatureEnabled, t, useTheme } from '@superset-ui/core';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Global } from '@emotion/react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useToasts } from 'src/components/MessageToasts/withToasts';
|
||||
import Loading from 'src/components/Loading';
|
||||
@@ -49,6 +50,7 @@ import { getUrlParam } from 'src/utils/urlUtils';
|
||||
import { canUserEditDashboard } from 'src/dashboard/util/findPermission';
|
||||
import { getFilterSets } from '../actions/nativeFilters';
|
||||
import { getFilterValue } from '../components/nativeFilters/FilterBar/keyValue';
|
||||
import { filterCardPopoverStyle } from '../styles';
|
||||
|
||||
export const MigrationContext = React.createContext(
|
||||
FILTER_BOX_MIGRATION_STATES.NOOP,
|
||||
@@ -68,6 +70,7 @@ const originalDocumentTitle = document.title;
|
||||
|
||||
const DashboardPage: FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
const theme = useTheme();
|
||||
const user = useSelector<any, UserWithPermissionsAndRoles>(
|
||||
state => state.user,
|
||||
);
|
||||
@@ -226,6 +229,7 @@ const DashboardPage: FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Global styles={filterCardPopoverStyle(theme)} />
|
||||
<FilterBoxMigrationModal
|
||||
show={filterboxMigrationState === FILTER_BOX_MIGRATION_STATES.UNDECIDED}
|
||||
hideFooter={!isMigrationEnabled}
|
||||
|
||||
44
superset-frontend/src/dashboard/styles.ts
Normal file
44
superset-frontend/src/dashboard/styles.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 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 { css, SupersetTheme } from '@superset-ui/core';
|
||||
|
||||
export const filterCardPopoverStyle = (theme: SupersetTheme) => css`
|
||||
.filter-card-popover {
|
||||
width: 240px;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
|
||||
.ant-popover-inner-content {
|
||||
padding: ${theme.gridUnit * 4}px;
|
||||
}
|
||||
|
||||
.ant-popover-arrow {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-card-tooltip {
|
||||
&.ant-tooltip-placement-bottom {
|
||||
padding-top: 0;
|
||||
& .ant-tooltip-arrow {
|
||||
top: -13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
Reference in New Issue
Block a user