mirror of
https://github.com/apache/superset.git
synced 2026-04-07 10:31:50 +00:00
feat: SIP-34 card/grid views for dashboards and charts (#10526)
This commit is contained in:
@@ -22,7 +22,7 @@ const path = require('path');
|
||||
const customConfig = require('../webpack.config.js');
|
||||
|
||||
module.exports = {
|
||||
stories: ['../src/components/**/*.stories.jsx'],
|
||||
stories: ['../src/components/**/*.stories.(t|j)sx'],
|
||||
addons: [
|
||||
'@storybook/addon-actions',
|
||||
'@storybook/addon-links',
|
||||
|
||||
BIN
superset-frontend/images/chart-card-fallback.png
Normal file
BIN
superset-frontend/images/chart-card-fallback.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
BIN
superset-frontend/images/dashboard-card-fallback.png
Normal file
BIN
superset-frontend/images/dashboard-card-fallback.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
21
superset-frontend/images/icons/card-view.svg
Normal file
21
superset-frontend/images/icons/card-view.svg
Normal file
@@ -0,0 +1,21 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.5 4C3.22386 4 3 4.22386 3 4.5V7.5C3 7.77614 3.22386 8 3.5 8H10.5C10.7761 8 11 7.77614 11 7.5V4.5C11 4.22386 10.7761 4 10.5 4H3.5ZM13.5 4C13.2239 4 13 4.22386 13 4.5V7.5C13 7.77614 13.2239 8 13.5 8H20.5C20.7761 8 21 7.77614 21 7.5V4.5C21 4.22386 20.7761 4 20.5 4H13.5ZM3 10.5C3 10.2239 3.22386 10 3.5 10H10.5C10.7761 10 11 10.2239 11 10.5V13.5C11 13.7761 10.7761 14 10.5 14H3.5C3.22386 14 3 13.7761 3 13.5V10.5ZM3.5 16C3.22386 16 3 16.2239 3 16.5V19.5C3 19.7761 3.22386 20 3.5 20H10.5C10.7761 20 11 19.7761 11 19.5V16.5C11 16.2239 10.7761 16 10.5 16H3.5ZM13 10.5C13 10.2239 13.2239 10 13.5 10H20.5C20.7761 10 21 10.2239 21 10.5V13.5C21 13.7761 20.7761 14 20.5 14H13.5C13.2239 14 13 13.7761 13 13.5V10.5ZM13.5 16C13.2239 16 13 16.2239 13 16.5V19.5C13 19.7761 13.2239 20 13.5 20H20.5C20.7761 20 21 19.7761 21 19.5V16.5C21 16.2239 20.7761 16 20.5 16H13.5Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
21
superset-frontend/images/icons/list-view.svg
Normal file
21
superset-frontend/images/icons/list-view.svg
Normal file
@@ -0,0 +1,21 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.71023 16.29C3.61513 16.199 3.50298 16.1276 3.38023 16.08C3.13677 15.98 2.86369 15.98 2.62023 16.08C2.49748 16.1276 2.38534 16.199 2.29023 16.29C2.19919 16.3851 2.12783 16.4972 2.08023 16.62C1.92364 16.9924 2.00649 17.4224 2.29023 17.71C2.38743 17.7983 2.49905 17.8694 2.62023 17.92C2.86227 18.027 3.13819 18.027 3.38023 17.92C3.50142 17.8694 3.61303 17.7983 3.71023 17.71C3.99398 17.4224 4.07683 16.9924 3.92023 16.62C3.87264 16.4972 3.80127 16.3851 3.71023 16.29ZM7.00023 8H21.0002C21.5525 8 22.0002 7.55228 22.0002 7C22.0002 6.44772 21.5525 6 21.0002 6H7.00023C6.44795 6 6.00023 6.44772 6.00023 7C6.00023 7.55228 6.44795 8 7.00023 8ZM3.71023 11.29C3.42267 11.0063 2.99263 10.9234 2.62023 11.08C2.49905 11.1306 2.38743 11.2017 2.29023 11.29C2.19919 11.3851 2.12783 11.4972 2.08023 11.62C1.97326 11.862 1.97326 12.138 2.08023 12.38C2.13087 12.5012 2.2019 12.6128 2.29023 12.71C2.38743 12.7983 2.49905 12.8694 2.62023 12.92C2.86227 13.027 3.13819 13.027 3.38023 12.92C3.50142 12.8694 3.61303 12.7983 3.71023 12.71C3.79856 12.6128 3.86959 12.5012 3.92023 12.38C4.02721 12.138 4.02721 11.862 3.92023 11.62C3.87264 11.4972 3.80127 11.3851 3.71023 11.29ZM21.0002 11H7.00023C6.44795 11 6.00023 11.4477 6.00023 12C6.00023 12.5523 6.44795 13 7.00023 13H21.0002C21.5525 13 22.0002 12.5523 22.0002 12C22.0002 11.4477 21.5525 11 21.0002 11ZM3.71023 6.29C3.61513 6.19896 3.50298 6.12759 3.38023 6.08C3.00784 5.9234 2.57779 6.00626 2.29023 6.29C2.2019 6.3872 2.13087 6.49882 2.08023 6.62C1.97326 6.86204 1.97326 7.13796 2.08023 7.38C2.13087 7.50118 2.2019 7.6128 2.29023 7.71C2.38743 7.79833 2.49905 7.86936 2.62023 7.92C2.99263 8.0766 3.42267 7.99374 3.71023 7.71C3.79856 7.6128 3.86959 7.50118 3.92023 7.38C4.02721 7.13796 4.02721 6.86204 3.92023 6.62C3.86959 6.49882 3.79856 6.3872 3.71023 6.29ZM21.0002 16H7.00023C6.44795 16 6.00023 16.4477 6.00023 17C6.00023 17.5523 6.44795 18 7.00023 18H21.0002C21.5525 18 22.0002 17.5523 22.0002 17C22.0002 16.4477 21.5525 16 21.0002 16Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
@@ -16,7 +16,7 @@
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 64 (93537) - https://sketch.com -->
|
||||
<title>Icon / Search@1.5x</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
@@ -24,7 +24,9 @@ import fetchMock from 'fetch-mock';
|
||||
import { supersetTheme, ThemeProvider } from '@superset-ui/style';
|
||||
|
||||
import ChartList from 'src/views/CRUD/chart/ChartList';
|
||||
import ListView from 'src/components/ListView/ListView';
|
||||
import ListView from 'src/components/ListView';
|
||||
import PropertiesModal from 'src/explore/components/PropertiesModal';
|
||||
import ListViewCard from 'src/components/ListViewCard';
|
||||
|
||||
// store needed for withToasts(ChartTable)
|
||||
const mockStore = configureStore([thunk]);
|
||||
@@ -96,4 +98,19 @@ describe('ChartList', () => {
|
||||
`"http://localhost/api/v1/chart/?q=(order_column:changed_on_delta_humanized,order_direction:desc,page:0,page_size:25)"`,
|
||||
);
|
||||
});
|
||||
|
||||
it('renders a card view', () => {
|
||||
expect(wrapper.find(ListViewCard)).toExist();
|
||||
});
|
||||
|
||||
it('renders a table view', () => {
|
||||
wrapper.find('[data-test="list-view"]').first().simulate('click');
|
||||
expect(wrapper.find('table')).toExist();
|
||||
});
|
||||
|
||||
it('edits', () => {
|
||||
expect(wrapper.find(PropertiesModal)).not.toExist();
|
||||
wrapper.find('[data-test="pencil"]').first().simulate('click');
|
||||
expect(wrapper.find(PropertiesModal)).toExist();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,8 +24,9 @@ import fetchMock from 'fetch-mock';
|
||||
import { supersetTheme, ThemeProvider } from '@superset-ui/style';
|
||||
|
||||
import DashboardList from 'src/views/CRUD/dashboard/DashboardList';
|
||||
import ListView from 'src/components/ListView/ListView';
|
||||
import ListView from 'src/components/ListView';
|
||||
import PropertiesModal from 'src/dashboard/components/PropertiesModal';
|
||||
import ListViewCard from 'src/components/ListViewCard';
|
||||
|
||||
// store needed for withToasts(DashboardTable)
|
||||
const mockStore = configureStore([thunk]);
|
||||
@@ -88,6 +89,16 @@ describe('DashboardList', () => {
|
||||
`"http://localhost/api/v1/dashboard/?q=(order_column:changed_on_delta_humanized,order_direction:desc,page:0,page_size:25)"`,
|
||||
);
|
||||
});
|
||||
|
||||
it('renders a card view', () => {
|
||||
expect(wrapper.find(ListViewCard)).toExist();
|
||||
});
|
||||
|
||||
it('renders a table view', () => {
|
||||
wrapper.find('[data-test="list-view"]').first().simulate('click');
|
||||
expect(wrapper.find('table')).toExist();
|
||||
});
|
||||
|
||||
it('edits', () => {
|
||||
expect(wrapper.find(PropertiesModal)).not.toExist();
|
||||
wrapper.find('[data-test="pencil"]').first().simulate('click');
|
||||
|
||||
@@ -24,7 +24,7 @@ import fetchMock from 'fetch-mock';
|
||||
import { supersetTheme, ThemeProvider } from '@superset-ui/style';
|
||||
|
||||
import DatasetList from 'src/views/CRUD/dataset/DatasetList';
|
||||
import ListView from 'src/components/ListView/ListView';
|
||||
import ListView from 'src/components/ListView';
|
||||
import Button from 'src/components/Button';
|
||||
import IndeterminateCheckbox from 'src/components/IndeterminateCheckbox';
|
||||
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
|
||||
|
||||
@@ -23,7 +23,7 @@ import configureStore from 'redux-mock-store';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import { supersetTheme, ThemeProvider } from '@superset-ui/style';
|
||||
|
||||
import ListView from 'src/components/ListView/ListView';
|
||||
import ListView from 'src/components/ListView';
|
||||
import DashboardTable from 'src/welcome/DashboardTable';
|
||||
|
||||
// store needed for withToasts(DashboardTable)
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import styled from '@superset-ui/style';
|
||||
import { getCategoricalSchemeRegistry } from '@superset-ui/color';
|
||||
import Avatar, { ConfigProvider } from 'react-avatar';
|
||||
import TooltipWrapper from 'src/components/TooltipWrapper';
|
||||
@@ -25,27 +24,20 @@ import TooltipWrapper from 'src/components/TooltipWrapper';
|
||||
interface Props {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
tableName: string;
|
||||
userName: string;
|
||||
uniqueKey: string;
|
||||
iconSize: number;
|
||||
textSize: number;
|
||||
}
|
||||
|
||||
const colorList = getCategoricalSchemeRegistry().get();
|
||||
|
||||
const StyledAvatar = styled(Avatar)`
|
||||
margin: 0px 5px;
|
||||
`;
|
||||
|
||||
export default function AvatarIcon({
|
||||
tableName,
|
||||
uniqueKey,
|
||||
firstName,
|
||||
lastName,
|
||||
userName,
|
||||
iconSize,
|
||||
textSize,
|
||||
}: Props) {
|
||||
const uniqueKey = `${tableName}-${userName}`;
|
||||
const fullName = `${firstName} ${lastName}`;
|
||||
|
||||
return (
|
||||
@@ -55,7 +47,7 @@ export default function AvatarIcon({
|
||||
tooltip={fullName}
|
||||
>
|
||||
<ConfigProvider colors={colorList && colorList.colors}>
|
||||
<StyledAvatar
|
||||
<Avatar
|
||||
key={uniqueKey}
|
||||
name={fullName}
|
||||
size={String(iconSize)}
|
||||
|
||||
@@ -61,7 +61,7 @@ export default class FaveStar extends React.PureComponent<FaveStarProps> {
|
||||
}
|
||||
viewBox="0 0 16 15"
|
||||
width={this.props.width || 20}
|
||||
height="auto"
|
||||
height={this.props.height || 'auto'}
|
||||
/>
|
||||
</a>
|
||||
</TooltipWrapper>
|
||||
|
||||
@@ -18,12 +18,13 @@
|
||||
*/
|
||||
import React, { SVGProps } from 'react';
|
||||
import { ReactComponent as CancelXIcon } from 'images/icons/cancel-x.svg';
|
||||
import { ReactComponent as CheckIcon } from 'images/icons/check.svg';
|
||||
import { ReactComponent as CircleCheckIcon } from 'images/icons/circle-check.svg';
|
||||
import { ReactComponent as CircleCheckSolidIcon } from 'images/icons/circle-check-solid.svg';
|
||||
import { ReactComponent as CardViewIcon } from 'images/icons/card-view.svg';
|
||||
import { ReactComponent as CheckboxHalfIcon } from 'images/icons/checkbox-half.svg';
|
||||
import { ReactComponent as CheckboxOffIcon } from 'images/icons/checkbox-off.svg';
|
||||
import { ReactComponent as CheckboxOnIcon } from 'images/icons/checkbox-on.svg';
|
||||
import { ReactComponent as CheckIcon } from 'images/icons/check.svg';
|
||||
import { ReactComponent as CircleCheckIcon } from 'images/icons/circle-check.svg';
|
||||
import { ReactComponent as CircleCheckSolidIcon } from 'images/icons/circle-check-solid.svg';
|
||||
import { ReactComponent as CloseIcon } from 'images/icons/close.svg';
|
||||
import { ReactComponent as CompassIcon } from 'images/icons/compass.svg';
|
||||
import { ReactComponent as DatasetPhysicalIcon } from 'images/icons/dataset_physical.svg';
|
||||
@@ -31,39 +32,42 @@ import { ReactComponent as DatasetVirtualIcon } from 'images/icons/dataset_virtu
|
||||
import { ReactComponent as ErrorIcon } from 'images/icons/error.svg';
|
||||
import { ReactComponent as FavoriteSelectedIcon } from 'images/icons/favorite-selected.svg';
|
||||
import { ReactComponent as FavoriteUnselectedIcon } from 'images/icons/favorite-unselected.svg';
|
||||
import { ReactComponent as PencilIcon } from 'images/icons/pencil.svg';
|
||||
import { ReactComponent as ListViewIcon } from 'images/icons/list-view.svg';
|
||||
import { ReactComponent as MoreIcon } from 'images/icons/more.svg';
|
||||
import { ReactComponent as PencilIcon } from 'images/icons/pencil.svg';
|
||||
import { ReactComponent as SearchIcon } from 'images/icons/search.svg';
|
||||
import { ReactComponent as ShareIcon } from 'images/icons/share.svg';
|
||||
import { ReactComponent as SortAscIcon } from 'images/icons/sort-asc.svg';
|
||||
import { ReactComponent as SortDescIcon } from 'images/icons/sort-desc.svg';
|
||||
import { ReactComponent as SortIcon } from 'images/icons/sort.svg';
|
||||
import { ReactComponent as TrashIcon } from 'images/icons/trash.svg';
|
||||
import { ReactComponent as WarningIcon } from 'images/icons/warning.svg';
|
||||
import { ReactComponent as ShareIcon } from 'images/icons/share.svg';
|
||||
|
||||
type IconName =
|
||||
| 'cancel-x'
|
||||
| 'card-view'
|
||||
| 'check'
|
||||
| 'checkbox-half'
|
||||
| 'checkbox-off'
|
||||
| 'checkbox-on'
|
||||
| 'close'
|
||||
| 'circle-check'
|
||||
| 'circle-check-solid'
|
||||
| 'circle-check'
|
||||
| 'close'
|
||||
| 'compass'
|
||||
| 'dataset-physical'
|
||||
| 'dataset-virtual'
|
||||
| 'error'
|
||||
| 'favorite-selected'
|
||||
| 'favorite-unselected'
|
||||
| 'list-view'
|
||||
| 'more'
|
||||
| 'pencil'
|
||||
| 'search'
|
||||
| 'sort'
|
||||
| 'share'
|
||||
| 'sort-asc'
|
||||
| 'sort-desc'
|
||||
| 'sort'
|
||||
| 'trash'
|
||||
| 'share'
|
||||
| 'warning';
|
||||
|
||||
export const iconsRegistry: Record<
|
||||
@@ -71,15 +75,17 @@ export const iconsRegistry: Record<
|
||||
React.ComponentType<SVGProps<SVGSVGElement>>
|
||||
> = {
|
||||
'cancel-x': CancelXIcon,
|
||||
'card-view': CardViewIcon,
|
||||
'checkbox-half': CheckboxHalfIcon,
|
||||
'checkbox-off': CheckboxOffIcon,
|
||||
'checkbox-on': CheckboxOnIcon,
|
||||
'circle-check': CircleCheckIcon,
|
||||
'circle-check-solid': CircleCheckSolidIcon,
|
||||
'circle-check': CircleCheckIcon,
|
||||
'dataset-physical': DatasetPhysicalIcon,
|
||||
'dataset-virtual': DatasetVirtualIcon,
|
||||
'favorite-selected': FavoriteSelectedIcon,
|
||||
'favorite-unselected': FavoriteUnselectedIcon,
|
||||
'list-view': ListViewIcon,
|
||||
'sort-asc': SortAscIcon,
|
||||
'sort-desc': SortDescIcon,
|
||||
check: CheckIcon,
|
||||
@@ -89,18 +95,25 @@ export const iconsRegistry: Record<
|
||||
more: MoreIcon,
|
||||
pencil: PencilIcon,
|
||||
search: SearchIcon,
|
||||
share: ShareIcon,
|
||||
sort: SortIcon,
|
||||
trash: TrashIcon,
|
||||
warning: WarningIcon,
|
||||
share: ShareIcon,
|
||||
};
|
||||
interface IconProps extends SVGProps<SVGSVGElement> {
|
||||
name: IconName;
|
||||
}
|
||||
|
||||
const Icon = ({ name, color = '#666666', ...rest }: IconProps) => {
|
||||
const Icon = ({
|
||||
name,
|
||||
color = '#666666',
|
||||
viewBox = '0 0 24 24',
|
||||
...rest
|
||||
}: IconProps) => {
|
||||
const Component = iconsRegistry[name];
|
||||
return <Component color={color} data-test={name} {...rest} />;
|
||||
return (
|
||||
<Component color={color} viewBox={viewBox} data-test={name} {...rest} />
|
||||
);
|
||||
};
|
||||
|
||||
export default Icon;
|
||||
|
||||
@@ -63,6 +63,13 @@ const SupersetLabel = styled(BootstrapLabel)`
|
||||
background-color: ${({ theme }) => theme.colors.error.base};
|
||||
}
|
||||
}
|
||||
|
||||
&.secondaryLabel {
|
||||
background-color: ${({ theme }) => theme.colors.secondary.base};
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.colors.secondary.base};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default function Label(props: LabelProps) {
|
||||
|
||||
56
superset-frontend/src/components/ListView/CardCollection.tsx
Normal file
56
superset-frontend/src/components/ListView/CardCollection.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* 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 { TableInstance } from 'react-table';
|
||||
import styled from '@superset-ui/style';
|
||||
|
||||
interface Props {
|
||||
renderCard?: (row: any) => React.ReactNode;
|
||||
prepareRow: TableInstance['prepareRow'];
|
||||
rows: TableInstance['rows'];
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
const CardContainer = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(459px, max-content));
|
||||
grid-gap: ${({ theme }) => theme.gridUnit * 8}px;
|
||||
justify-content: center;
|
||||
padding: ${({ theme }) => theme.gridUnit * 2}px
|
||||
${({ theme }) => theme.gridUnit * 4}px;
|
||||
`;
|
||||
|
||||
export default function CardCollection({
|
||||
renderCard,
|
||||
prepareRow,
|
||||
rows,
|
||||
loading,
|
||||
}: Props) {
|
||||
return (
|
||||
<CardContainer>
|
||||
{rows.map(row => {
|
||||
if (!renderCard) return null;
|
||||
prepareRow(row);
|
||||
return (
|
||||
<div key={row.id}>{renderCard({ ...row.original, loading })}</div>
|
||||
);
|
||||
})}
|
||||
</CardContainer>
|
||||
);
|
||||
}
|
||||
@@ -218,7 +218,10 @@ interface UIFiltersProps {
|
||||
}
|
||||
|
||||
const FilterWrapper = styled.div`
|
||||
padding: 24px 16px 8px;
|
||||
display: inline-block;
|
||||
padding: ${({ theme }) => theme.gridUnit * 6}px
|
||||
${({ theme }) => theme.gridUnit * 4}px
|
||||
${({ theme }) => theme.gridUnit * 2}px;
|
||||
`;
|
||||
|
||||
function UIFilters({
|
||||
|
||||
@@ -17,13 +17,15 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { t } from '@superset-ui/translation';
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import { Col, Row, Alert } from 'react-bootstrap';
|
||||
import React, { FunctionComponent, useState } from 'react';
|
||||
import { Alert } from 'react-bootstrap';
|
||||
import styled from '@superset-ui/style';
|
||||
import cx from 'classnames';
|
||||
import Button from 'src/components/Button';
|
||||
import Icon from 'src/components/Icon';
|
||||
import IndeterminateCheckbox from 'src/components/IndeterminateCheckbox';
|
||||
import TableCollection from './TableCollection';
|
||||
import CardCollection from './CardCollection';
|
||||
import Pagination from './Pagination';
|
||||
import FilterControls from './Filters';
|
||||
import { FetchDataConfig, Filters, SortColumn } from './types';
|
||||
@@ -42,173 +44,21 @@ const ListViewStyles = styled.div`
|
||||
.body {
|
||||
overflow: scroll;
|
||||
max-height: 64vh;
|
||||
|
||||
table {
|
||||
border-collapse: separate;
|
||||
|
||||
th {
|
||||
background: white;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
&:first-of-type {
|
||||
padding-left: ${({ theme }) => theme.gridUnit * 4}px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter-dropdown {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.filter-column {
|
||||
height: 30px;
|
||||
padding: 5px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.filter-close {
|
||||
height: 30px;
|
||||
padding: 5px;
|
||||
|
||||
i {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-cell-loader {
|
||||
position: relative;
|
||||
|
||||
.loading-bar {
|
||||
background-color: ${({ theme }) => theme.colors.secondary.light4};
|
||||
border-radius: 7px;
|
||||
|
||||
span {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
transform: translateY(-50%);
|
||||
top: 50%;
|
||||
left: 0;
|
||||
content: '';
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
background-image: linear-gradient(
|
||||
100deg,
|
||||
rgba(255, 255, 255, 0),
|
||||
rgba(255, 255, 255, 0.5) 60%,
|
||||
rgba(255, 255, 255, 0) 80%
|
||||
);
|
||||
background-size: 200px 48px;
|
||||
background-position: -100px 0;
|
||||
background-repeat: no-repeat;
|
||||
animation: loading-shimmer 1s infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
white-space: nowrap;
|
||||
font-size: 24px;
|
||||
min-width: 100px;
|
||||
|
||||
svg,
|
||||
i {
|
||||
margin-right: 8px;
|
||||
|
||||
&:hover {
|
||||
path {
|
||||
fill: ${({ theme }) => theme.colors.primary.base};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-row {
|
||||
.actions {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.colors.secondary.light5};
|
||||
|
||||
.actions {
|
||||
opacity: 1;
|
||||
transition: opacity ease-in ${({ theme }) => theme.transitionTiming}s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-row-selected {
|
||||
background-color: ${({ theme }) => theme.colors.secondary.light4};
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.colors.secondary.light4};
|
||||
}
|
||||
}
|
||||
|
||||
.table-cell {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
max-width: 300px;
|
||||
line-height: 1;
|
||||
vertical-align: middle;
|
||||
&:first-of-type {
|
||||
padding-left: ${({ theme }) => theme.gridUnit * 4}px;
|
||||
}
|
||||
}
|
||||
|
||||
.sort-icon {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.form-actions-container {
|
||||
position: absolute;
|
||||
left: 28px;
|
||||
}
|
||||
|
||||
.row-count-container {
|
||||
float: right;
|
||||
padding-right: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loading-shimmer {
|
||||
40% {
|
||||
background-position: 100% 0;
|
||||
}
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 100% 0;
|
||||
}
|
||||
.row-count-container {
|
||||
margin-top: ${({ theme }) => theme.gridUnit * 2}px;
|
||||
color: ${({ theme }) => theme.colors.grayscale.base};
|
||||
}
|
||||
`;
|
||||
|
||||
export interface ListViewProps {
|
||||
columns: any[];
|
||||
data: any[];
|
||||
count: number;
|
||||
pageSize: number;
|
||||
fetchData: (conf: FetchDataConfig) => any;
|
||||
loading: boolean;
|
||||
className?: string;
|
||||
initialSort?: SortColumn[];
|
||||
filters?: Filters;
|
||||
bulkActions?: Array<{
|
||||
key: string;
|
||||
name: React.ReactNode;
|
||||
onSelect: (rows: any[]) => any;
|
||||
type?: 'primary' | 'secondary' | 'danger';
|
||||
}>;
|
||||
bulkSelectEnabled?: boolean;
|
||||
disableBulkSelect?: () => void;
|
||||
renderBulkSelectCopy?: (selects: any[]) => React.ReactNode;
|
||||
}
|
||||
|
||||
const BulkSelectWrapper = styled(Alert)`
|
||||
border-radius: 0;
|
||||
margin-bottom: 0;
|
||||
@@ -257,6 +107,89 @@ const bulkSelectColumnConfig = {
|
||||
size: 'sm',
|
||||
};
|
||||
|
||||
const ViewModeContainer = styled.div`
|
||||
padding: ${({ theme }) => theme.gridUnit * 6}px 0px
|
||||
${({ theme }) => theme.gridUnit * 2}px
|
||||
${({ theme }) => theme.gridUnit * 4}px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
top: 8px;
|
||||
|
||||
.toggle-button {
|
||||
display: inline-block;
|
||||
border-radius: ${({ theme }) => theme.gridUnit / 2}px;
|
||||
padding: ${({ theme }) => theme.gridUnit}px;
|
||||
padding-bottom: 0;
|
||||
|
||||
&:first-of-type {
|
||||
margin-right: ${({ theme }) => theme.gridUnit * 2}px;
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color: ${({ theme }) => theme.colors.grayscale.base};
|
||||
svg {
|
||||
color: ${({ theme }) => theme.colors.grayscale.light5};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const ViewModeToggle = ({
|
||||
mode,
|
||||
setMode,
|
||||
}: {
|
||||
mode: 'table' | 'card';
|
||||
setMode: (mode: 'table' | 'card') => void;
|
||||
}) => {
|
||||
return (
|
||||
<ViewModeContainer>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={e => {
|
||||
e.currentTarget.blur();
|
||||
setMode('card');
|
||||
}}
|
||||
className={cx('toggle-button', { active: mode === 'card' })}
|
||||
>
|
||||
<Icon name="card-view" />
|
||||
</div>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={e => {
|
||||
e.currentTarget.blur();
|
||||
setMode('table');
|
||||
}}
|
||||
className={cx('toggle-button', { active: mode === 'table' })}
|
||||
>
|
||||
<Icon name="list-view" />
|
||||
</div>
|
||||
</ViewModeContainer>
|
||||
);
|
||||
};
|
||||
export interface ListViewProps<T = any> {
|
||||
columns: any[];
|
||||
data: T[];
|
||||
count: number;
|
||||
pageSize: number;
|
||||
fetchData: (conf: FetchDataConfig) => any;
|
||||
loading: boolean;
|
||||
className?: string;
|
||||
initialSort?: SortColumn[];
|
||||
filters?: Filters;
|
||||
bulkActions?: Array<{
|
||||
key: string;
|
||||
name: React.ReactNode;
|
||||
onSelect: (rows: any[]) => any;
|
||||
type?: 'primary' | 'secondary' | 'danger';
|
||||
}>;
|
||||
bulkSelectEnabled?: boolean;
|
||||
disableBulkSelect?: () => void;
|
||||
renderBulkSelectCopy?: (selects: any[]) => React.ReactNode;
|
||||
renderCard?: (row: T) => React.ReactNode;
|
||||
}
|
||||
|
||||
const ListView: FunctionComponent<ListViewProps> = ({
|
||||
columns,
|
||||
data,
|
||||
@@ -271,6 +204,7 @@ const ListView: FunctionComponent<ListViewProps> = ({
|
||||
bulkSelectEnabled = false,
|
||||
disableBulkSelect = () => {},
|
||||
renderBulkSelectCopy = selected => t('%s Selected', selected.length),
|
||||
renderCard,
|
||||
}) => {
|
||||
const {
|
||||
getTableProps,
|
||||
@@ -310,10 +244,18 @@ const ListView: FunctionComponent<ListViewProps> = ({
|
||||
});
|
||||
}
|
||||
|
||||
const cardViewEnabled = Boolean(renderCard);
|
||||
const [viewingMode, setViewingMode] = useState<'table' | 'card'>(
|
||||
cardViewEnabled ? 'card' : 'table',
|
||||
);
|
||||
|
||||
return (
|
||||
<ListViewStyles>
|
||||
<div className={`superset-list-view ${className}`}>
|
||||
<div className="header">
|
||||
{cardViewEnabled && (
|
||||
<ViewModeToggle mode={viewingMode} setMode={setViewingMode} />
|
||||
)}
|
||||
{filterable && (
|
||||
<FilterControls
|
||||
filters={filters}
|
||||
@@ -364,36 +306,42 @@ const ListView: FunctionComponent<ListViewProps> = ({
|
||||
)}
|
||||
</BulkSelectWrapper>
|
||||
)}
|
||||
<TableCollection
|
||||
getTableProps={getTableProps}
|
||||
getTableBodyProps={getTableBodyProps}
|
||||
prepareRow={prepareRow}
|
||||
headerGroups={headerGroups}
|
||||
rows={rows}
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
<div className="footer">
|
||||
<Row>
|
||||
<Col>
|
||||
<span className="row-count-container">
|
||||
showing{' '}
|
||||
<strong>
|
||||
{pageSize * pageIndex + (rows.length && 1)}-
|
||||
{pageSize * pageIndex + rows.length}
|
||||
</strong>{' '}
|
||||
of <strong>{count}</strong>
|
||||
</span>
|
||||
</Col>
|
||||
</Row>
|
||||
{viewingMode === 'card' && (
|
||||
<CardCollection
|
||||
prepareRow={prepareRow}
|
||||
renderCard={renderCard}
|
||||
rows={rows}
|
||||
loading={loading}
|
||||
/>
|
||||
)}
|
||||
{viewingMode === 'table' && (
|
||||
<TableCollection
|
||||
getTableProps={getTableProps}
|
||||
getTableBodyProps={getTableBodyProps}
|
||||
prepareRow={prepareRow}
|
||||
headerGroups={headerGroups}
|
||||
rows={rows}
|
||||
loading={loading}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="pagination-container">
|
||||
<Pagination
|
||||
totalPages={pageCount || 0}
|
||||
currentPage={pageCount ? pageIndex + 1 : 0}
|
||||
onChange={(p: number) => gotoPage(p - 1)}
|
||||
hideFirstAndLastPageLinks
|
||||
/>
|
||||
<div className="row-count-container">
|
||||
{t(
|
||||
'%s-%s of %s',
|
||||
pageSize * pageIndex + (rows.length && 1),
|
||||
pageSize * pageIndex + rows.length,
|
||||
count,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Pagination
|
||||
totalPages={pageCount || 0}
|
||||
currentPage={pageCount ? pageIndex + 1 : 0}
|
||||
onChange={(p: number) => gotoPage(p - 1)}
|
||||
hideFirstAndLastPageLinks
|
||||
/>
|
||||
</ListViewStyles>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ import { TableInstance } from 'react-table';
|
||||
import styled from '@superset-ui/style';
|
||||
import Icon from 'src/components/Icon';
|
||||
|
||||
interface Props {
|
||||
interface TableCollectionProps {
|
||||
getTableProps: (userProps?: any) => any;
|
||||
getTableBodyProps: (userProps?: any) => any;
|
||||
prepareRow: TableInstance['prepareRow'];
|
||||
@@ -32,7 +32,17 @@ interface Props {
|
||||
}
|
||||
|
||||
const Table = styled.table`
|
||||
border-collapse: separate;
|
||||
|
||||
th {
|
||||
background: ${({ theme }) => theme.colors.grayscale.light5};
|
||||
position: sticky;
|
||||
top: 0;
|
||||
|
||||
&:first-of-type {
|
||||
padding-left: ${({ theme }) => theme.gridUnit * 4}px;
|
||||
}
|
||||
|
||||
&.xs {
|
||||
min-width: 25px;
|
||||
}
|
||||
@@ -58,6 +68,7 @@ const Table = styled.table`
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
&.xs {
|
||||
width: 25px;
|
||||
@@ -78,6 +89,105 @@ const Table = styled.table`
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-cell-loader {
|
||||
position: relative;
|
||||
|
||||
.loading-bar {
|
||||
background-color: ${({ theme }) => theme.colors.secondary.light4};
|
||||
border-radius: 7px;
|
||||
|
||||
span {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
transform: translateY(-50%);
|
||||
top: 50%;
|
||||
left: 0;
|
||||
content: '';
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
background-image: linear-gradient(
|
||||
100deg,
|
||||
rgba(255, 255, 255, 0),
|
||||
rgba(255, 255, 255, 0.5) 60%,
|
||||
rgba(255, 255, 255, 0) 80%
|
||||
);
|
||||
background-size: 200px 48px;
|
||||
background-position: -100px 0;
|
||||
background-repeat: no-repeat;
|
||||
animation: loading-shimmer 1s infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
white-space: nowrap;
|
||||
min-width: 100px;
|
||||
|
||||
svg,
|
||||
i {
|
||||
margin-right: 8px;
|
||||
|
||||
&:hover {
|
||||
path {
|
||||
fill: ${({ theme }) => theme.colors.primary.base};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-row {
|
||||
.actions {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.colors.secondary.light5};
|
||||
|
||||
.actions {
|
||||
opacity: 1;
|
||||
transition: opacity ease-in ${({ theme }) => theme.transitionTiming}s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-row-selected {
|
||||
background-color: ${({ theme }) => theme.colors.secondary.light4};
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.colors.secondary.light4};
|
||||
}
|
||||
}
|
||||
|
||||
.table-cell {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
max-width: 300px;
|
||||
line-height: 1;
|
||||
vertical-align: middle;
|
||||
&:first-of-type {
|
||||
padding-left: ${({ theme }) => theme.gridUnit * 4}px;
|
||||
}
|
||||
}
|
||||
|
||||
.sort-icon {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
@keyframes loading-shimmer {
|
||||
40% {
|
||||
background-position: 100% 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 100% 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default function TableCollection({
|
||||
@@ -87,7 +197,7 @@ export default function TableCollection({
|
||||
headerGroups,
|
||||
rows,
|
||||
loading,
|
||||
}: Props) {
|
||||
}: TableCollectionProps) {
|
||||
return (
|
||||
<Table {...getTableProps()} className="table table-hover">
|
||||
<thead>
|
||||
|
||||
23
superset-frontend/src/components/ListView/index.ts
Normal file
23
superset-frontend/src/components/ListView/index.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './ListView';
|
||||
export * from './types';
|
||||
|
||||
export { default } from './ListView';
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* 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 { action } from '@storybook/addon-actions';
|
||||
import { withKnobs, boolean } from '@storybook/addon-knobs';
|
||||
import DashboardImg from 'images/dashboard-card-fallback.png';
|
||||
import ChartImg from 'images/chart-card-fallback.png';
|
||||
import { Dropdown, Menu } from 'src/common/components';
|
||||
import Icon from 'src/components/Icon';
|
||||
import FaveStar from 'src/components/FaveStar';
|
||||
import ListViewCard from './';
|
||||
|
||||
export default {
|
||||
title: 'ListViewCard',
|
||||
component: ListViewCard,
|
||||
decorators: [withKnobs],
|
||||
};
|
||||
|
||||
export const SupersetListViewCard = () => {
|
||||
return (
|
||||
<ListViewCard
|
||||
title="Superset Card Title"
|
||||
url="/superset/dashboard/births/"
|
||||
imgURL={DashboardImg}
|
||||
imgFallbackURL={ChartImg}
|
||||
description="Lorem ipsum dolor sit amet, consectetur adipiscing elit..."
|
||||
coverLeft="Left Section"
|
||||
coverRight="Right Section"
|
||||
actions={
|
||||
<ListViewCard.Actions>
|
||||
<FaveStar
|
||||
itemId={0}
|
||||
fetchFaveStar={action('fetchFaveStar')}
|
||||
saveFaveStar={action('saveFaveStar')}
|
||||
isStarred={boolean('isStarred', false)}
|
||||
/>
|
||||
<Dropdown
|
||||
overlay={
|
||||
<Menu>
|
||||
<Menu.Item
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={action('Delete')}
|
||||
>
|
||||
<ListViewCard.MenuIcon name="trash" /> Delete
|
||||
</Menu.Item>
|
||||
<Menu.Item role="button" tabIndex={0} onClick={action('Edit')}>
|
||||
<ListViewCard.MenuIcon name="pencil" /> Edit
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<Icon name="more" />
|
||||
</Dropdown>
|
||||
</ListViewCard.Actions>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
197
superset-frontend/src/components/ListViewCard/index.tsx
Normal file
197
superset-frontend/src/components/ListViewCard/index.tsx
Normal file
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* 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 styled from '@superset-ui/style';
|
||||
import Icon from 'src/components/Icon';
|
||||
import { Card } from 'src/common/components';
|
||||
|
||||
const MenuIcon = styled(Icon)`
|
||||
width: ${({ theme }) => theme.gridUnit * 4}px;
|
||||
height: ${({ theme }) => theme.gridUnit * 4}px;
|
||||
position: relative;
|
||||
top: ${({ theme }) => theme.gridUnit / 2}px;
|
||||
`;
|
||||
|
||||
const ActionsWrapper = styled.div`
|
||||
width: 64px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const StyledCard = styled(Card)`
|
||||
width: 459px;
|
||||
|
||||
.ant-card-body {
|
||||
padding: ${({ theme }) => theme.gridUnit * 4}px
|
||||
${({ theme }) => theme.gridUnit * 2}px;
|
||||
}
|
||||
.ant-card-meta-detail > div:not(:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const Cover = styled.div`
|
||||
height: 264px;
|
||||
overflow: hidden;
|
||||
|
||||
.cover-footer {
|
||||
transform: translateY(${({ theme }) => theme.gridUnit * 9}px);
|
||||
transition: ${({ theme }) => theme.transitionTiming}s ease-out;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.cover-footer {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const GradientContainer = styled.div`
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: inline-block;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(0, 0, 0, 0) 47.83%,
|
||||
rgba(0, 0, 0, 0.219135) 79.64%,
|
||||
rgba(0, 0, 0, 0.5) 100%
|
||||
);
|
||||
}
|
||||
`;
|
||||
const CardCoverImg = styled.img`
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
width: 459px;
|
||||
height: 264px;
|
||||
`;
|
||||
|
||||
const TitleContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
flex-direction: row;
|
||||
|
||||
.card-actions {
|
||||
margin-left: auto;
|
||||
align-self: flex-end;
|
||||
padding-left: ${({ theme }) => theme.gridUnit * 8}px;
|
||||
}
|
||||
`;
|
||||
|
||||
const TitleLink = styled.a`
|
||||
color: ${({ theme }) => theme.colors.grayscale.dark1} !important;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
& + .title-right {
|
||||
margin-left: ${({ theme }) => theme.gridUnit * 2}px;
|
||||
}
|
||||
`;
|
||||
|
||||
const CoverFooter = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
position: relative;
|
||||
top: -${({ theme }) => theme.gridUnit * 9}px;
|
||||
padding: 0 8px;
|
||||
`;
|
||||
|
||||
const CoverFooterLeft = styled.div`
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const CoverFooterRight = styled.div`
|
||||
align-self: flex-end;
|
||||
margin-left: auto;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`;
|
||||
|
||||
interface CardProps {
|
||||
title: React.ReactNode;
|
||||
url: string;
|
||||
imgURL: string;
|
||||
imgFallbackURL: string;
|
||||
description: string;
|
||||
titleRight?: React.ReactNode;
|
||||
coverLeft?: React.ReactNode;
|
||||
coverRight?: React.ReactNode;
|
||||
actions: React.ReactNode;
|
||||
}
|
||||
|
||||
function ListViewCard({
|
||||
title,
|
||||
url,
|
||||
titleRight,
|
||||
imgURL,
|
||||
imgFallbackURL,
|
||||
description,
|
||||
coverLeft,
|
||||
coverRight,
|
||||
actions,
|
||||
}: CardProps) {
|
||||
return (
|
||||
<StyledCard
|
||||
cover={
|
||||
<Cover>
|
||||
<a href={url}>
|
||||
<GradientContainer>
|
||||
<CardCoverImg
|
||||
src={imgURL}
|
||||
onError={e => {
|
||||
e.currentTarget.src = imgFallbackURL;
|
||||
}}
|
||||
/>
|
||||
</GradientContainer>
|
||||
</a>
|
||||
<CoverFooter className="cover-footer">
|
||||
{coverLeft && <CoverFooterLeft>{coverLeft}</CoverFooterLeft>}
|
||||
{coverRight && <CoverFooterRight>{coverRight}</CoverFooterRight>}
|
||||
</CoverFooter>
|
||||
</Cover>
|
||||
}
|
||||
>
|
||||
<Card.Meta
|
||||
title={
|
||||
<>
|
||||
<TitleContainer>
|
||||
<TitleLink href={url}>{title}</TitleLink>
|
||||
{titleRight && <div className="title-right"> {titleRight}</div>}
|
||||
<div className="card-actions">{actions}</div>
|
||||
</TitleContainer>
|
||||
</>
|
||||
}
|
||||
description={description}
|
||||
/>
|
||||
</StyledCard>
|
||||
);
|
||||
}
|
||||
|
||||
ListViewCard.Actions = ActionsWrapper;
|
||||
ListViewCard.MenuIcon = MenuIcon;
|
||||
export default ListViewCard;
|
||||
@@ -77,6 +77,7 @@ interface PaginationProps {
|
||||
const PaginationList = styled.ul`
|
||||
display: inline-block;
|
||||
margin: 16px 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
display: inline;
|
||||
|
||||
@@ -49,20 +49,18 @@ const commonStyles = `
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
display: block;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
cursor: pointer;
|
||||
`;
|
||||
const SearchIcon = styled(Icon)`
|
||||
${commonStyles}
|
||||
top: 2px;
|
||||
top: 1px;
|
||||
left: 2px;
|
||||
`;
|
||||
|
||||
const ClearIcon = styled(Icon)`
|
||||
${commonStyles}
|
||||
right: 0px;
|
||||
top: 3px;
|
||||
top: 1px;
|
||||
`;
|
||||
|
||||
export default function SearchInput({
|
||||
|
||||
@@ -38,6 +38,7 @@ import FormLabel from 'src/components/FormLabel';
|
||||
import getClientErrorObject from '../../utils/getClientErrorObject';
|
||||
|
||||
export type Slice = {
|
||||
id?: number;
|
||||
slice_id: number;
|
||||
slice_name: string;
|
||||
description: string | null;
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
* The Chart model as returned from the API
|
||||
*/
|
||||
|
||||
import Owner from './Owner';
|
||||
|
||||
export default interface Chart {
|
||||
id: number;
|
||||
url: string;
|
||||
@@ -30,4 +32,8 @@ export default interface Chart {
|
||||
changed_on: string;
|
||||
description: string | null;
|
||||
cache_timeout: number | null;
|
||||
thumbnail_url?: string;
|
||||
changed_on_delta_humanized?: string;
|
||||
owners?: Owner[];
|
||||
datasource_name_text?: string;
|
||||
}
|
||||
|
||||
29
superset-frontend/src/types/Owner.ts
Normal file
29
superset-frontend/src/types/Owner.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The Owner model as returned from the API
|
||||
*/
|
||||
|
||||
export default interface Owner {
|
||||
first_name: string;
|
||||
id: string;
|
||||
last_name: string;
|
||||
username: string;
|
||||
}
|
||||
@@ -30,17 +30,21 @@ import {
|
||||
} from 'src/views/CRUD/utils';
|
||||
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
|
||||
import SubMenu from 'src/components/Menu/SubMenu';
|
||||
import AvatarIcon from 'src/components/AvatarIcon';
|
||||
import Icon from 'src/components/Icon';
|
||||
import FaveStar from 'src/components/FaveStar';
|
||||
import ListView, { ListViewProps } from 'src/components/ListView/ListView';
|
||||
import {
|
||||
import ListView, {
|
||||
ListViewProps,
|
||||
FetchDataConfig,
|
||||
Filters,
|
||||
SelectOption,
|
||||
} from 'src/components/ListView/types';
|
||||
} from 'src/components/ListView';
|
||||
import withToasts from 'src/messageToasts/enhancers/withToasts';
|
||||
import PropertiesModal, { Slice } from 'src/explore/components/PropertiesModal';
|
||||
import Chart from 'src/types/Chart';
|
||||
import ListViewCard from 'src/components/ListViewCard';
|
||||
import Label from 'src/components/Label';
|
||||
import { Dropdown, Menu } from 'src/common/components';
|
||||
|
||||
const PAGE_SIZE = 25;
|
||||
const FAVESTAR_BASE_URL = '/superset/favstar/slice';
|
||||
@@ -53,7 +57,7 @@ interface Props {
|
||||
interface State {
|
||||
bulkSelectEnabled: boolean;
|
||||
chartCount: number;
|
||||
charts: any[];
|
||||
charts: Chart[];
|
||||
favoriteStatus: object;
|
||||
lastFetchDataConfig: FetchDataConfig | null;
|
||||
loading: boolean;
|
||||
@@ -191,7 +195,7 @@ class ChartList extends React.PureComponent<Props, State> {
|
||||
},
|
||||
}: any) => <a href={dsUrl}>{dsNameTxt}</a>,
|
||||
Header: t('Datasource'),
|
||||
accessor: 'datasource_id',
|
||||
accessor: 'datasource_name',
|
||||
},
|
||||
{
|
||||
Cell: ({
|
||||
@@ -225,7 +229,7 @@ class ChartList extends React.PureComponent<Props, State> {
|
||||
disableSortBy: true,
|
||||
},
|
||||
{
|
||||
accessor: 'datasource',
|
||||
accessor: 'datasource_id',
|
||||
hidden: true,
|
||||
disableSortBy: true,
|
||||
},
|
||||
@@ -457,6 +461,85 @@ class ChartList extends React.PureComponent<Props, State> {
|
||||
});
|
||||
};
|
||||
|
||||
renderCard = (props: Chart) => {
|
||||
const menu = (
|
||||
<Menu>
|
||||
{this.canDelete && (
|
||||
<Menu.Item>
|
||||
<ConfirmStatusChange
|
||||
title={t('Please Confirm')}
|
||||
description={
|
||||
<>
|
||||
{t('Are you sure you want to delete')}{' '}
|
||||
<b>{props.slice_name}</b>?
|
||||
</>
|
||||
}
|
||||
onConfirm={() => this.handleChartDelete(props)}
|
||||
>
|
||||
{confirmDelete => (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="action-button"
|
||||
onClick={confirmDelete}
|
||||
>
|
||||
<ListViewCard.MenuIcon name="trash" /> Delete
|
||||
</div>
|
||||
)}
|
||||
</ConfirmStatusChange>
|
||||
</Menu.Item>
|
||||
)}
|
||||
{this.canEdit && (
|
||||
<Menu.Item
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => this.openChartEditModal(props)}
|
||||
>
|
||||
<ListViewCard.MenuIcon name="pencil" /> Edit
|
||||
</Menu.Item>
|
||||
)}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<ListViewCard
|
||||
title={props.slice_name}
|
||||
url={props.url}
|
||||
imgURL={props.thumbnail_url ?? ''}
|
||||
imgFallbackURL={'/static/assets/images/chart-card-fallback.png'}
|
||||
description={t('Last modified %s', props.changed_on_delta_humanized)}
|
||||
coverLeft={(props.owners || []).slice(0, 5).map(owner => (
|
||||
<AvatarIcon
|
||||
key={owner.id}
|
||||
uniqueKey={`${owner.username}-${props.id}`}
|
||||
firstName={owner.first_name}
|
||||
lastName={owner.last_name}
|
||||
iconSize={24}
|
||||
textSize={9}
|
||||
/>
|
||||
))}
|
||||
coverRight={
|
||||
<Label className="secondaryLabel">{props.datasource_name_text}</Label>
|
||||
}
|
||||
actions={
|
||||
<ListViewCard.Actions>
|
||||
<FaveStar
|
||||
itemId={props.id}
|
||||
fetchFaveStar={this.fetchMethods.fetchFaveStar}
|
||||
saveFaveStar={this.fetchMethods.saveFaveStar}
|
||||
isStarred={!!this.state.favoriteStatus[props.id]}
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
<Dropdown overlay={menu}>
|
||||
<Icon name="more" />
|
||||
</Dropdown>
|
||||
</ListViewCard.Actions>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
bulkSelectEnabled,
|
||||
@@ -519,6 +602,7 @@ class ChartList extends React.PureComponent<Props, State> {
|
||||
initialSort={this.initialSort}
|
||||
loading={loading}
|
||||
pageSize={PAGE_SIZE}
|
||||
renderCard={this.renderCard}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -28,13 +28,21 @@ import {
|
||||
} from 'src/views/CRUD/utils';
|
||||
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
|
||||
import SubMenu from 'src/components/Menu/SubMenu';
|
||||
import ListView, { ListViewProps } from 'src/components/ListView/ListView';
|
||||
import AvatarIcon from 'src/components/AvatarIcon';
|
||||
import ListView, {
|
||||
ListViewProps,
|
||||
FetchDataConfig,
|
||||
Filters,
|
||||
} from 'src/components/ListView';
|
||||
import ExpandableList from 'src/components/ExpandableList';
|
||||
import { FetchDataConfig, Filters } from 'src/components/ListView/types';
|
||||
import Owner from 'src/types/Owner';
|
||||
import withToasts from 'src/messageToasts/enhancers/withToasts';
|
||||
import Icon from 'src/components/Icon';
|
||||
import Label from 'src/components/Label';
|
||||
import FaveStar from 'src/components/FaveStar';
|
||||
import PropertiesModal from 'src/dashboard/components/PropertiesModal';
|
||||
import ListViewCard from 'src/components/ListViewCard';
|
||||
import { Dropdown, Menu } from 'src/common/components';
|
||||
|
||||
const PAGE_SIZE = 25;
|
||||
const FAVESTAR_BASE_URL = '/superset/favstar/Dashboard';
|
||||
@@ -47,7 +55,7 @@ interface Props {
|
||||
interface State {
|
||||
bulkSelectEnabled: boolean;
|
||||
dashboardCount: number;
|
||||
dashboards: any[];
|
||||
dashboards: Dashboard[];
|
||||
favoriteStatus: object;
|
||||
dashboardToEdit: Dashboard | null;
|
||||
lastFetchDataConfig: FetchDataConfig | null;
|
||||
@@ -64,6 +72,8 @@ interface Dashboard {
|
||||
id: number;
|
||||
published: boolean;
|
||||
url: string;
|
||||
thumbnail_url: string;
|
||||
owners: Owner[];
|
||||
}
|
||||
|
||||
class DashboardList extends React.PureComponent<Props, State> {
|
||||
@@ -205,61 +215,7 @@ class DashboardList extends React.PureComponent<Props, State> {
|
||||
disableSortBy: true,
|
||||
},
|
||||
{
|
||||
Cell: ({ row: { original } }: any) => {
|
||||
const handleDelete = () => this.handleDashboardDelete(original);
|
||||
const handleEdit = () => this.openDashboardEditModal(original);
|
||||
const handleExport = () => this.handleBulkDashboardExport([original]);
|
||||
if (!this.canEdit && !this.canDelete && !this.canExport) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<span className="actions">
|
||||
{this.canDelete && (
|
||||
<ConfirmStatusChange
|
||||
title={t('Please Confirm')}
|
||||
description={
|
||||
<>
|
||||
{t('Are you sure you want to delete')}{' '}
|
||||
<b>{original.dashboard_title}</b>?
|
||||
</>
|
||||
}
|
||||
onConfirm={handleDelete}
|
||||
>
|
||||
{confirmDelete => (
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="action-button"
|
||||
onClick={confirmDelete}
|
||||
>
|
||||
<Icon name="trash" />
|
||||
</span>
|
||||
)}
|
||||
</ConfirmStatusChange>
|
||||
)}
|
||||
{this.canExport && (
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="action-button"
|
||||
onClick={handleExport}
|
||||
>
|
||||
<Icon name="share" />
|
||||
</span>
|
||||
)}
|
||||
{this.canEdit && (
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="action-button"
|
||||
onClick={handleEdit}
|
||||
>
|
||||
<Icon name="pencil" />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
Cell: ({ row: { original } }: any) => this.renderActions(original),
|
||||
Header: t('Actions'),
|
||||
id: 'actions',
|
||||
disableSortBy: true,
|
||||
@@ -444,6 +400,148 @@ class DashboardList extends React.PureComponent<Props, State> {
|
||||
});
|
||||
};
|
||||
|
||||
renderActions(original: Dashboard) {
|
||||
const handleDelete = () => this.handleDashboardDelete(original);
|
||||
const handleEdit = () => this.openDashboardEditModal(original);
|
||||
const handleExport = () => this.handleBulkDashboardExport([original]);
|
||||
if (!this.canEdit && !this.canDelete && !this.canExport) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<span className="actions">
|
||||
{this.canDelete && (
|
||||
<ConfirmStatusChange
|
||||
title={t('Please Confirm')}
|
||||
description={
|
||||
<>
|
||||
{t('Are you sure you want to delete')}{' '}
|
||||
<b>{original.dashboard_title}</b>?
|
||||
</>
|
||||
}
|
||||
onConfirm={handleDelete}
|
||||
>
|
||||
{confirmDelete => (
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="action-button"
|
||||
onClick={confirmDelete}
|
||||
>
|
||||
<Icon name="trash" />
|
||||
</span>
|
||||
)}
|
||||
</ConfirmStatusChange>
|
||||
)}
|
||||
{this.canExport && (
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="action-button"
|
||||
onClick={handleExport}
|
||||
>
|
||||
<Icon name="share" />
|
||||
</span>
|
||||
)}
|
||||
{this.canEdit && (
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="action-button"
|
||||
onClick={handleEdit}
|
||||
>
|
||||
<Icon name="pencil" />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
renderCard = (props: Dashboard) => {
|
||||
const menu = (
|
||||
<Menu>
|
||||
{this.canDelete && (
|
||||
<Menu.Item>
|
||||
<ConfirmStatusChange
|
||||
title={t('Please Confirm')}
|
||||
description={
|
||||
<>
|
||||
{t('Are you sure you want to delete')}{' '}
|
||||
<b>{props.dashboard_title}</b>?
|
||||
</>
|
||||
}
|
||||
onConfirm={() => this.handleDashboardDelete(props)}
|
||||
>
|
||||
{confirmDelete => (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="action-button"
|
||||
onClick={confirmDelete}
|
||||
>
|
||||
<ListViewCard.MenuIcon name="trash" /> Delete
|
||||
</div>
|
||||
)}
|
||||
</ConfirmStatusChange>
|
||||
</Menu.Item>
|
||||
)}
|
||||
{this.canExport && (
|
||||
<Menu.Item
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => this.handleBulkDashboardExport([props])}
|
||||
>
|
||||
<ListViewCard.MenuIcon name="share" /> Export
|
||||
</Menu.Item>
|
||||
)}
|
||||
{this.canEdit && (
|
||||
<Menu.Item
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => this.openDashboardEditModal(props)}
|
||||
>
|
||||
<ListViewCard.MenuIcon name="pencil" /> Edit
|
||||
</Menu.Item>
|
||||
)}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<ListViewCard
|
||||
title={props.dashboard_title}
|
||||
titleRight={<Label>{props.published ? 'published' : 'draft'}</Label>}
|
||||
url={props.url}
|
||||
imgURL={props.thumbnail_url}
|
||||
imgFallbackURL="/static/assets/images/dashboard-card-fallback.png"
|
||||
description={t('Last modified %s', props.changed_on_delta_humanized)}
|
||||
coverLeft={props.owners.slice(0, 5).map(owner => (
|
||||
<AvatarIcon
|
||||
key={owner.id}
|
||||
uniqueKey={`${owner.username}-${props.id}`}
|
||||
firstName={owner.first_name}
|
||||
lastName={owner.last_name}
|
||||
iconSize={24}
|
||||
textSize={9}
|
||||
/>
|
||||
))}
|
||||
actions={
|
||||
<ListViewCard.Actions>
|
||||
<FaveStar
|
||||
itemId={props.id}
|
||||
fetchFaveStar={this.fetchMethods.fetchFaveStar}
|
||||
saveFaveStar={this.fetchMethods.saveFaveStar}
|
||||
isStarred={!!this.state.favoriteStatus[props.id]}
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
<Dropdown overlay={menu}>
|
||||
<Icon name="more" />
|
||||
</Dropdown>
|
||||
</ListViewCard.Actions>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
bulkSelectEnabled,
|
||||
@@ -495,6 +593,7 @@ class DashboardList extends React.PureComponent<Props, State> {
|
||||
{dashboardToEdit && (
|
||||
<PropertiesModal
|
||||
dashboardId={dashboardToEdit.id}
|
||||
show
|
||||
onHide={() => this.setState({ dashboardToEdit: null })}
|
||||
onSubmit={this.handleDashboardEdit}
|
||||
/>
|
||||
@@ -512,6 +611,7 @@ class DashboardList extends React.PureComponent<Props, State> {
|
||||
initialSort={this.initialSort}
|
||||
loading={loading}
|
||||
pageSize={PAGE_SIZE}
|
||||
renderCard={this.renderCard}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -29,10 +29,14 @@ import { createFetchRelated, createErrorHandler } from 'src/views/CRUD/utils';
|
||||
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
|
||||
import DatasourceModal from 'src/datasource/DatasourceModal';
|
||||
import DeleteModal from 'src/components/DeleteModal';
|
||||
import ListView, { ListViewProps } from 'src/components/ListView/ListView';
|
||||
import ListView, {
|
||||
ListViewProps,
|
||||
FetchDataConfig,
|
||||
Filters,
|
||||
} from 'src/components/ListView';
|
||||
import SubMenu, { SubMenuProps } from 'src/components/Menu/SubMenu';
|
||||
import AvatarIcon from 'src/components/AvatarIcon';
|
||||
import { FetchDataConfig, Filters } from 'src/components/ListView/types';
|
||||
import Owner from 'src/types/Owner';
|
||||
import withToasts from 'src/messageToasts/enhancers/withToasts';
|
||||
import TooltipWrapper from 'src/components/TooltipWrapper';
|
||||
import Icon from 'src/components/Icon';
|
||||
@@ -40,13 +44,6 @@ import AddDatasetModal from './AddDatasetModal';
|
||||
|
||||
const PAGE_SIZE = 25;
|
||||
|
||||
type Owner = {
|
||||
first_name: string;
|
||||
id: string;
|
||||
last_name: string;
|
||||
username: string;
|
||||
};
|
||||
|
||||
type Dataset = {
|
||||
changed_by_name: string;
|
||||
changed_by_url: string;
|
||||
@@ -359,10 +356,9 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
||||
.map((owner: Owner) => (
|
||||
<AvatarIcon
|
||||
key={owner.id}
|
||||
tableName={tableName}
|
||||
uniqueKey={`${tableName}-${owner.username}`}
|
||||
firstName={owner.first_name}
|
||||
lastName={owner.last_name}
|
||||
userName={owner.username}
|
||||
iconSize={24}
|
||||
textSize={9}
|
||||
/>
|
||||
@@ -379,7 +375,7 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
||||
disableSortBy: true,
|
||||
},
|
||||
{
|
||||
Cell: ({ row: { state, original } }: any) => {
|
||||
Cell: ({ row: { original } }: any) => {
|
||||
const handleEdit = () => openDatasetEditModal(original);
|
||||
const handleDelete = () => openDatasetDeleteModal(original);
|
||||
if (!canEdit && !canDelete) {
|
||||
|
||||
@@ -20,10 +20,9 @@ import React from 'react';
|
||||
import { t } from '@superset-ui/translation';
|
||||
import { SupersetClient } from '@superset-ui/connection';
|
||||
import { debounce } from 'lodash';
|
||||
import ListView from 'src/components/ListView/ListView';
|
||||
import ListView, { FetchDataConfig } from 'src/components/ListView';
|
||||
import withToasts from 'src/messageToasts/enhancers/withToasts';
|
||||
import { Dashboard } from 'src/types/bootstrapTypes';
|
||||
import { FetchDataConfig } from 'src/components/ListView/types';
|
||||
|
||||
const PAGE_SIZE = 25;
|
||||
|
||||
|
||||
@@ -116,15 +116,21 @@ class ChartRestApi(BaseSupersetModelRestApi):
|
||||
"datasource_url",
|
||||
"table.default_endpoint",
|
||||
"table.table_name",
|
||||
"thumbnail_url",
|
||||
"viz_type",
|
||||
"params",
|
||||
"cache_timeout",
|
||||
"owners.id",
|
||||
"owners.username",
|
||||
"owners.first_name",
|
||||
"owners.last_name",
|
||||
]
|
||||
list_select_columns = list_columns + ["changed_on", "changed_by_fk"]
|
||||
order_columns = [
|
||||
"slice_name",
|
||||
"viz_type",
|
||||
"datasource_name",
|
||||
"datasource_id",
|
||||
"changed_by.first_name",
|
||||
"changed_on_delta_humanized",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user