mirror of https://github.com/apache/superset.git
feat: create table component based on ant design Table (#21520)
Co-authored-by: Lyndsi Kay Williams <55605634+lyndsiWilliams@users.noreply.github.com> Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com>
This commit is contained in:
parent
9f7bd1e63f
commit
736b53418a
|
@ -24,7 +24,8 @@ module.exports = {
|
|||
builder: 'webpack5',
|
||||
},
|
||||
stories: [
|
||||
'../src/@(components|common|filters|explore)/**/*.stories.@(tsx|jsx|mdx)',
|
||||
'../src/@(components|common|filters|explore)/**/*.stories.@(tsx|jsx)',
|
||||
'../src/@(components|common|filters|explore)/**/*.*.@(mdx)',
|
||||
],
|
||||
addons: [
|
||||
'@storybook/addon-essentials',
|
||||
|
|
|
@ -68,7 +68,15 @@ addParameters({
|
|||
['Controls', 'Display', 'Feedback', 'Input', '*'],
|
||||
['Overview', 'Examples', '*'],
|
||||
'Design System',
|
||||
['Foundations', 'Components', 'Patterns', '*'],
|
||||
[
|
||||
'Introduction',
|
||||
'Foundations',
|
||||
'Components',
|
||||
['Overview', 'Examples', '*'],
|
||||
'Patterns',
|
||||
'*',
|
||||
],
|
||||
['Overview', 'Examples', '*'],
|
||||
'*',
|
||||
],
|
||||
},
|
||||
|
|
|
@ -39,11 +39,13 @@ export type ButtonStyle =
|
|||
| 'link'
|
||||
| 'dashed';
|
||||
|
||||
export type ButtonSize = 'default' | 'small' | 'xsmall';
|
||||
|
||||
export type ButtonProps = Omit<AntdButtonProps, 'css'> &
|
||||
Pick<TooltipProps, 'placement'> & {
|
||||
tooltip?: string;
|
||||
className?: string;
|
||||
buttonSize?: 'default' | 'small' | 'xsmall';
|
||||
buttonSize?: ButtonSize;
|
||||
buttonStyle?: ButtonStyle;
|
||||
cta?: boolean;
|
||||
showMarginRight?: boolean;
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
import { Meta, Source } from '@storybook/addon-docs';
|
||||
import AtomicDesign from './atomic-design.png';
|
||||
|
||||
<Meta title="Design System/Introduction" />
|
||||
|
||||
# Superset Design System
|
||||
|
||||
A design system is a complete set of standards intended to manage design at scale using reusable components and patterns.
|
||||
|
||||
You can get an overview of Atomic Design concepts and a link to the full book on the topic here:
|
||||
|
||||
<a href="https://bradfrost.com/blog/post/atomic-web-design/" target="_blank">
|
||||
Intro to Atomic Design
|
||||
</a>
|
||||
|
||||
While the Superset Design System will use Atomic Design principles, we choose a different language to describe the elements.
|
||||
|
||||
| Atomic Design | Atoms | Molecules | Organisms | Templates | Pages / Screens |
|
||||
| :-------------- | :---------: | :--------: | :-------: | :-------: | :-------------: |
|
||||
| Superset Design | Foundations | Components | Patterns | Templates | Features |
|
||||
|
||||
<img
|
||||
src={AtomicDesign}
|
||||
alt="Atoms = Foundations, Molecules = Components, Organisms = Patterns, Templates = Templates, Pages / Screens = Features"
|
||||
/>
|
|
@ -20,6 +20,7 @@ import React, { RefObject } from 'react';
|
|||
import { AntdDropdown } from 'src/components';
|
||||
import { DropDownProps } from 'antd/lib/dropdown';
|
||||
import { styled } from '@superset-ui/core';
|
||||
import Icons from 'src/components/Icons';
|
||||
|
||||
const MenuDots = styled.div`
|
||||
width: ${({ theme }) => theme.gridUnit * 0.75}px;
|
||||
|
@ -66,14 +67,35 @@ const MenuDotsWrapper = styled.div`
|
|||
padding-left: ${({ theme }) => theme.gridUnit}px;
|
||||
`;
|
||||
|
||||
export enum IconOrientation {
|
||||
VERTICAL = 'vertical',
|
||||
HORIZONTAL = 'horizontal',
|
||||
}
|
||||
export interface DropdownProps extends DropDownProps {
|
||||
overlay: React.ReactElement;
|
||||
iconOrientation?: IconOrientation;
|
||||
}
|
||||
|
||||
export const Dropdown = ({ overlay, ...rest }: DropdownProps) => (
|
||||
const RenderIcon = (
|
||||
iconOrientation: IconOrientation = IconOrientation.VERTICAL,
|
||||
) => {
|
||||
const component =
|
||||
iconOrientation === IconOrientation.HORIZONTAL ? (
|
||||
<Icons.MoreHoriz iconSize="xl" />
|
||||
) : (
|
||||
<MenuDots />
|
||||
);
|
||||
return component;
|
||||
};
|
||||
|
||||
export const Dropdown = ({
|
||||
overlay,
|
||||
iconOrientation = IconOrientation.VERTICAL,
|
||||
...rest
|
||||
}: DropdownProps) => (
|
||||
<AntdDropdown overlay={overlay} {...rest}>
|
||||
<MenuDotsWrapper data-test="dropdown-trigger">
|
||||
<MenuDots />
|
||||
{RenderIcon(iconOrientation)}
|
||||
</MenuDotsWrapper>
|
||||
</AntdDropdown>
|
||||
);
|
||||
|
|
|
@ -40,7 +40,7 @@ export const LoadingGallery = () => (
|
|||
}}
|
||||
>
|
||||
<h4>{position}</h4>
|
||||
<Loading position={position} image="/src/assets/images/loading.gif" />
|
||||
<Loading position={position} />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
|
@ -71,7 +71,7 @@ InteractiveLoading.story = {
|
|||
};
|
||||
|
||||
InteractiveLoading.args = {
|
||||
image: '/src/assets/images/loading.gif',
|
||||
image: '',
|
||||
className: '',
|
||||
};
|
||||
|
||||
|
|
|
@ -26,11 +26,9 @@ test('Rerendering correctly with default props', () => {
|
|||
render(<Loading />);
|
||||
const loading = screen.getByRole('status');
|
||||
const classNames = loading.getAttribute('class')?.split(' ');
|
||||
const imagePath = loading.getAttribute('src');
|
||||
const ariaLive = loading.getAttribute('aria-live');
|
||||
const ariaLabel = loading.getAttribute('aria-label');
|
||||
expect(loading).toBeInTheDocument();
|
||||
expect(imagePath).toBe('/static/assets/images/loading.gif');
|
||||
expect(classNames).toContain('floating');
|
||||
expect(classNames).toContain('loading');
|
||||
expect(ariaLive).toContain('polite');
|
||||
|
@ -56,7 +54,7 @@ test('support for extra classes', () => {
|
|||
expect(classNames).toContain('extra-class');
|
||||
});
|
||||
|
||||
test('Diferent image path', () => {
|
||||
test('Different image path', () => {
|
||||
render(<Loading image="/src/assets/images/loading.gif" />);
|
||||
const loading = screen.getByRole('status');
|
||||
const imagePath = loading.getAttribute('src');
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
import React from 'react';
|
||||
import { styled } from '@superset-ui/core';
|
||||
import cls from 'classnames';
|
||||
import Loader from 'src/assets/images/loading.gif';
|
||||
|
||||
export type PositionOption =
|
||||
| 'floating'
|
||||
|
@ -35,6 +36,7 @@ export interface Props {
|
|||
const LoaderImg = styled.img`
|
||||
z-index: 99;
|
||||
width: 50px;
|
||||
height: unset;
|
||||
position: relative;
|
||||
margin: 10px;
|
||||
&.inline {
|
||||
|
@ -57,14 +59,14 @@ const LoaderImg = styled.img`
|
|||
`;
|
||||
export default function Loading({
|
||||
position = 'floating',
|
||||
image = '/static/assets/images/loading.gif',
|
||||
image,
|
||||
className,
|
||||
}: Props) {
|
||||
return (
|
||||
<LoaderImg
|
||||
className={cls('loading', position, className)}
|
||||
alt="Loading..."
|
||||
src={image}
|
||||
src={image || Loader}
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-label="Loading"
|
||||
|
|
|
@ -1,17 +1,25 @@
|
|||
import { Meta, Source } from '@storybook/addon-docs';
|
||||
import { Meta, Source, Story } from '@storybook/addon-docs';
|
||||
|
||||
<Meta title="MetadataBar/Overview" />
|
||||
<Meta title="Design System/Components/MetadataBar/Overview" />
|
||||
|
||||
# Usage
|
||||
# Metadata bar
|
||||
|
||||
The metadata bar component is used to display additional information about an entity. Some of the common applications in Superset are:
|
||||
The metadata bar component is used to display additional information about an entity.
|
||||
|
||||
## Usage
|
||||
|
||||
Some of the common applications in Superset are:
|
||||
|
||||
- Display the chart's metadata in Explore to help the user understand what dashboards this chart is added to and get
|
||||
to know the details of the chart
|
||||
- Display the database's metadata in a drill to detail modal to help the user understand what data they are looking
|
||||
at while accessing the feature in the dashboard
|
||||
|
||||
# Variations
|
||||
## Basic example
|
||||
|
||||
<Story id="design-system-components-metadatabar-examples--basic" />
|
||||
|
||||
## Variations
|
||||
|
||||
The metadata bar is by default a static component (besides the links in text).
|
||||
The variations in this component are related to content and entity type as all of the details are predefined
|
||||
|
@ -25,7 +33,7 @@ have the same icon and when hovered it will present who created the entity, its
|
|||
To extend the list of content types, a developer needs to request the inclusion of the new type in the design system.
|
||||
This process is important to make sure the new type is reviewed by the design team, improving Superset consistency.
|
||||
|
||||
To check each content type in detail and its interactions, check the [MetadataBar](/story/metadatabar--component) page.
|
||||
To check each content type in detail and its interactions, check the [MetadataBar](/story/design-system-components-metadatabar-examples--basic) page.
|
||||
Below you can find the configurations for each content type:
|
||||
|
||||
<Source
|
|
@ -22,13 +22,13 @@ import { useResizeDetector } from 'react-resize-detector';
|
|||
import MetadataBar, { MetadataBarProps, MetadataType } from '.';
|
||||
|
||||
export default {
|
||||
title: 'MetadataBar',
|
||||
title: 'Design System/Components/MetadataBar/Examples',
|
||||
component: MetadataBar,
|
||||
};
|
||||
|
||||
const A_WEEK_AGO = 'a week ago';
|
||||
|
||||
export const Component = ({
|
||||
export const Basic = ({
|
||||
items,
|
||||
onClick,
|
||||
}: MetadataBarProps & {
|
||||
|
@ -61,7 +61,7 @@ export const Component = ({
|
|||
);
|
||||
};
|
||||
|
||||
Component.story = {
|
||||
Basic.story = {
|
||||
parameters: {
|
||||
knobs: {
|
||||
disable: true,
|
||||
|
@ -69,7 +69,7 @@ Component.story = {
|
|||
},
|
||||
};
|
||||
|
||||
Component.args = {
|
||||
Basic.args = {
|
||||
items: [
|
||||
{
|
||||
type: MetadataType.SQL,
|
||||
|
@ -99,7 +99,7 @@ Component.args = {
|
|||
],
|
||||
};
|
||||
|
||||
Component.argTypes = {
|
||||
Basic.argTypes = {
|
||||
onClick: {
|
||||
action: 'onClick',
|
||||
table: {
|
||||
|
|
|
@ -0,0 +1,260 @@
|
|||
import { Meta, Source, Story, ArgsTable } from '@storybook/addon-docs';
|
||||
|
||||
<Meta title="Design System/Components/Table/Overview" />
|
||||
|
||||
# Table
|
||||
|
||||
A table is UI that allows the user to explore data in a tabular format.
|
||||
|
||||
## Usage
|
||||
|
||||
Common table applications in Superset:
|
||||
|
||||
- Display lists of user-generated entities (e.g. dashboard, charts, queries) for further exploration and use
|
||||
- Display data that can help the user make a decision (e.g. query results)
|
||||
|
||||
This component provides a general use Table.
|
||||
|
||||
---
|
||||
|
||||
### [Basic example](./?path=/docs/design-system-components-table-examples--basic)
|
||||
|
||||
<Story id="design-system-components-table-examples--basic" />
|
||||
|
||||
### Data and Columns
|
||||
|
||||
To set the visible columns and data for the table you use the `columns` and `data` props.
|
||||
|
||||
<details>
|
||||
|
||||
The basic table example for the `columns` prop is:
|
||||
|
||||
```
|
||||
const basicColumns: = [
|
||||
{
|
||||
title: 'Name',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 150,
|
||||
sorter: (a: BasicData, b: BasicData) =>
|
||||
alphabeticalSort('name', a, b),
|
||||
},
|
||||
{
|
||||
title: 'Category',
|
||||
dataIndex: 'category',
|
||||
key: 'category',
|
||||
sorter: (a: BasicData, b: BasicData) =>
|
||||
alphabeticalSort('category', a, b),
|
||||
},
|
||||
{
|
||||
title: 'Price',
|
||||
dataIndex: 'price',
|
||||
key: 'price',
|
||||
sorter: (a: BasicData, b: BasicData) =>
|
||||
numericalSort('price', a, b),
|
||||
},
|
||||
{
|
||||
title: 'Description',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
The data prop is:
|
||||
|
||||
```
|
||||
const basicData: = [
|
||||
{
|
||||
key: 1,
|
||||
name: 'Floppy Disk 10 pack',
|
||||
category: 'Disk Storage',
|
||||
price: '9.99'
|
||||
description: 'A real blast from the past',
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
name: 'DVD 100 pack',
|
||||
category: 'Optical Storage',
|
||||
price: '27.99'
|
||||
description: 'Still pretty ancient',
|
||||
},
|
||||
{
|
||||
key: 3,
|
||||
name: '128 GB SSD',
|
||||
category: 'Hardrive',
|
||||
price: '49.99'
|
||||
description: 'Reliable and fast data storage',
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Column Sort Functions
|
||||
|
||||
To ensure consistency for column sorting and to avoid redundant definitions for common column sorters, reusable sort functions are provided.
|
||||
When defining the object for the `columns` prop you can provide an optional attribute `sorter`.
|
||||
The function provided in the `sorter` prop is given the entire record representing a row as props `a` and `b`.
|
||||
When using a provided sorter function the pattern is to wrap the call to the sorter with an inline function, then specify the specific attribute value from `dataIndex`, representing a column
|
||||
of the data object for that row, as the first argument of the sorter function.
|
||||
|
||||
#### alphabeticalSort
|
||||
|
||||
The alphabeticalSort is for columns that display a string of text.
|
||||
|
||||
<details>
|
||||
|
||||
```
|
||||
import { alphabeticalSort } from 'src/components/Table/sorters';
|
||||
|
||||
const basicColumns = [
|
||||
{
|
||||
title: 'Column Name',
|
||||
dataIndex: 'columnName',
|
||||
key: 'columnName',
|
||||
sorter: (a, b) =>
|
||||
alphabeticalSort('columnName', a, b),
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
#### numericSort
|
||||
|
||||
The numericalSort is for columns that display a numeric value.
|
||||
|
||||
<details>
|
||||
|
||||
```
|
||||
import { numericalSort } from './sorters';
|
||||
|
||||
const basicColumns = [
|
||||
{
|
||||
title: 'Height',
|
||||
dataIndex: 'height',
|
||||
key: 'height',
|
||||
sorter: (a, b) =>
|
||||
numericalSort('height', a, b),
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
If a different sort option is needed, consider adding it as a reusable sort function following the pattern provided above.
|
||||
|
||||
---
|
||||
|
||||
### Cell Content Renderers
|
||||
|
||||
By default, each column will render the value as simple text. Often you will want to show formatted values, such as a numeric column showing as currency, or a more complex component such as a button or action menu as a cell value.
|
||||
Cell Renderers are React components provided to the optional `render` attribute on a column definition that enables injecting a specific React component to enable this.
|
||||
|
||||
<Story id="design-system-components-table-examples--cell-renderers" />
|
||||
|
||||
For convenience and consistency, the Table component provides pre-built Cell Renderers for:
|
||||
The following data types can be displayed in table cells.
|
||||
|
||||
- Text (default)
|
||||
- [Button Cell](./?path=/docs/design-system-components-table-cell-renderers-buttoncell--basic)
|
||||
- [Numeric Cell](./docs/design-system-components-table-cell-renderers-numericcell--basic)
|
||||
- Support Locale and currency formatting
|
||||
- w/ icons - Coming Soon
|
||||
- [Action Menu Cell](./?path=/docs/design-system-components-table-cell-renderers-actioncell-overview--page)
|
||||
- Provide a list of menu options with callback functions that retain a reference to the row the menu is defined for
|
||||
- Custom
|
||||
- You can provide your own React component as a cell renderer in cases not supported
|
||||
|
||||
---
|
||||
|
||||
### Loading
|
||||
|
||||
The table can be set to a loading state simply by setting the loading prop to true | false
|
||||
|
||||
<Story id="design-system-components-table-examples--loading" />
|
||||
|
||||
---
|
||||
|
||||
### Pagination
|
||||
|
||||
The table displays a set number of rows at a time, the user navigates the table via pagination. Use in scenarios where the user is searching for a specific piece of content.
|
||||
The default page size and page size options for the menu are configurable via the `pageSizeOptions` and `defaultPageSize` props.
|
||||
NOTE: Pagination controls will only display when the data for the table has more records than the default page size.
|
||||
|
||||
<Story id="design-system-components-table-examples--many-columns" />
|
||||
|
||||
```
|
||||
<Table pageSizeOptions={[5, 10, 15, 20, 25] defaultPageSize={10} />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration Checklist
|
||||
|
||||
The following specifications are required every time a table is used. These choices should be intentional based on the specific user needs for the table instance.
|
||||
|
||||
<details>
|
||||
|
||||
- [ ] Size
|
||||
- Large
|
||||
- Small
|
||||
- Columns
|
||||
- [ ] Number of
|
||||
- [ ] Contents
|
||||
- [ ] Order
|
||||
- [ ] Widths
|
||||
- Column headers
|
||||
- [ ] Labels
|
||||
- [ ] Has tooltip
|
||||
- [ ] Tooltip text
|
||||
- [ ] Default sort
|
||||
- Functionality
|
||||
- [ ] Can sort columns
|
||||
- [ ] Can filter columns
|
||||
- [ ] Loading
|
||||
- Pagination
|
||||
- [ ] Number of rows per page
|
||||
- Infinite scroll
|
||||
- [ ] Has toolbar
|
||||
- [ ] Has table title
|
||||
- [ ] Label
|
||||
- [ ] Has buttons
|
||||
- [ ] Labels
|
||||
- [ ] Actions
|
||||
- [ ] Has search
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## Experimental features
|
||||
|
||||
The Table component has features that are still experimental and can be used at your own risk.
|
||||
These features are intended to be made fully stable in future releases.
|
||||
|
||||
### Resizable Columns
|
||||
|
||||
The prop `resizable` enables table columns to be resized by the user dragging from the right edge of each
|
||||
column to increase or decrease the columns' width
|
||||
|
||||
<Story id="design-system-components-table-examples--resizable-columns" />
|
||||
|
||||
### Drag & Drop Columns
|
||||
|
||||
The prop `reorderable` can enable column drag and drop reordering as well as dragging a column to another component. If you want to accept the drop event of a Table Column
|
||||
you can register `onDragOver` and `onDragDrop` event handlers on the destination component. In the `onDragDrop` handler you can check for `SUPERSET_TABLE_COLUMN`
|
||||
as the getData key as shown below.
|
||||
|
||||
```
|
||||
import { SUPERSET_TABLE_COLUMN } from 'src/components/table';
|
||||
|
||||
const handleDrop = (ev:Event) => {
|
||||
const json = ev.dataTransfer?.getData?.(SUPERSET_TABLE_COLUMN);
|
||||
const data = JSON.parse(json);
|
||||
// ... do something with the data here
|
||||
}
|
||||
```
|
||||
|
||||
<Story id="design-system-components-table-examples--reorderable-columns" />
|
|
@ -0,0 +1,432 @@
|
|||
/**
|
||||
* 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 { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { supersetTheme, ThemeProvider } from '@superset-ui/core';
|
||||
import { Table, TableSize, SUPERSET_TABLE_COLUMN, ColumnsType } from './index';
|
||||
import { numericalSort, alphabeticalSort } from './sorters';
|
||||
import ButtonCell from './cell-renderers/ButtonCell';
|
||||
import ActionCell from './cell-renderers/ActionCell';
|
||||
import { exampleMenuOptions } from './cell-renderers/ActionCell/fixtures';
|
||||
import NumericCell, {
|
||||
CurrencyCode,
|
||||
LocaleCode,
|
||||
Style,
|
||||
} from './cell-renderers/NumericCell';
|
||||
|
||||
export default {
|
||||
title: 'Design System/Components/Table/Examples',
|
||||
component: Table,
|
||||
argTypes: { onClick: { action: 'clicked' } },
|
||||
} as ComponentMeta<typeof Table>;
|
||||
|
||||
export interface BasicData {
|
||||
name: string;
|
||||
category: string;
|
||||
price: number;
|
||||
description?: string;
|
||||
key: number;
|
||||
}
|
||||
|
||||
export interface RendererData {
|
||||
key: number;
|
||||
buttonCell: string;
|
||||
textCell: string;
|
||||
euroCell: number;
|
||||
dollarCell: number;
|
||||
}
|
||||
|
||||
export interface ExampleData {
|
||||
title: string;
|
||||
name: string;
|
||||
age: number;
|
||||
address: string;
|
||||
tags?: string[];
|
||||
key: number;
|
||||
}
|
||||
|
||||
function generateValues(amount: number): object {
|
||||
const cells = {};
|
||||
for (let i = 0; i < amount; i += 1) {
|
||||
cells[`col-${i}`] = `Text ${i}`;
|
||||
}
|
||||
return cells;
|
||||
}
|
||||
|
||||
function generateColumns(amount: number): ColumnsType<ExampleData>[] {
|
||||
const newCols: any[] = [];
|
||||
for (let i = 0; i < amount; i += 1) {
|
||||
newCols.push({
|
||||
title: `Column Header ${i}`,
|
||||
dataIndex: `col-${i}`,
|
||||
key: `col-${i}`,
|
||||
});
|
||||
}
|
||||
return newCols as ColumnsType<ExampleData>[];
|
||||
}
|
||||
const recordCount = 200;
|
||||
const columnCount = 12;
|
||||
const randomCols: ColumnsType<ExampleData>[] = generateColumns(columnCount);
|
||||
|
||||
const basicData: BasicData[] = [
|
||||
{
|
||||
key: 1,
|
||||
name: 'Floppy Disk 10 pack',
|
||||
category: 'Disk Storage',
|
||||
price: 9.99,
|
||||
description: 'A real blast from the past',
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
name: 'DVD 100 pack',
|
||||
category: 'Optical Storage',
|
||||
price: 27.99,
|
||||
description: 'Still pretty ancient',
|
||||
},
|
||||
{
|
||||
key: 3,
|
||||
name: '128 GB SSD',
|
||||
category: 'Hardrive',
|
||||
price: 49.99,
|
||||
description: 'Reliable and fast data storage',
|
||||
},
|
||||
];
|
||||
|
||||
const basicColumns: ColumnsType<BasicData> = [
|
||||
{
|
||||
title: 'Name',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 150,
|
||||
sorter: (a: BasicData, b: BasicData) => alphabeticalSort('name', a, b),
|
||||
},
|
||||
{
|
||||
title: 'Category',
|
||||
dataIndex: 'category',
|
||||
key: 'category',
|
||||
sorter: (a: BasicData, b: BasicData) => alphabeticalSort('category', a, b),
|
||||
},
|
||||
{
|
||||
title: 'Price',
|
||||
dataIndex: 'price',
|
||||
key: 'price',
|
||||
sorter: (a: BasicData, b: BasicData) => numericalSort('price', a, b),
|
||||
},
|
||||
{
|
||||
title: 'Description',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
},
|
||||
];
|
||||
|
||||
const bigColumns: ColumnsType<ExampleData> = [
|
||||
{
|
||||
title: 'Name',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
render: (text: string, row: object, index: number) => (
|
||||
<ButtonCell
|
||||
label={text}
|
||||
onClick={action('button-cell-click')}
|
||||
row={row}
|
||||
index={index}
|
||||
/>
|
||||
),
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: 'Age',
|
||||
dataIndex: 'age',
|
||||
key: 'age',
|
||||
},
|
||||
{
|
||||
title: 'Address',
|
||||
dataIndex: 'address',
|
||||
key: 'address',
|
||||
},
|
||||
...(randomCols as ColumnsType<ExampleData>),
|
||||
];
|
||||
|
||||
const rendererColumns: ColumnsType<RendererData> = [
|
||||
{
|
||||
title: 'Button Cell',
|
||||
dataIndex: 'buttonCell',
|
||||
key: 'buttonCell',
|
||||
width: 150,
|
||||
render: (text: string, data: object, index: number) => (
|
||||
<ButtonCell
|
||||
label={text}
|
||||
row={data}
|
||||
index={index}
|
||||
onClick={action('button-cell-click')}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Text Cell',
|
||||
dataIndex: 'textCell',
|
||||
key: 'textCell',
|
||||
},
|
||||
{
|
||||
title: 'Euro Cell',
|
||||
dataIndex: 'euroCell',
|
||||
key: 'euroCell',
|
||||
render: (value: number) => (
|
||||
<NumericCell
|
||||
options={{ style: Style.CURRENCY, currency: CurrencyCode.EUR }}
|
||||
value={value}
|
||||
locale={LocaleCode.en_US}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Dollar Cell',
|
||||
dataIndex: 'dollarCell',
|
||||
key: 'dollarCell',
|
||||
render: (value: number) => (
|
||||
<NumericCell
|
||||
options={{ style: Style.CURRENCY, currency: CurrencyCode.USD }}
|
||||
value={value}
|
||||
locale={LocaleCode.en_US}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
dataIndex: 'actions',
|
||||
key: 'actions',
|
||||
render: (text: string, row: object) => (
|
||||
<ActionCell row={row} menuOptions={exampleMenuOptions} />
|
||||
),
|
||||
width: 32,
|
||||
fixed: 'right',
|
||||
},
|
||||
];
|
||||
|
||||
const baseData: any[] = [
|
||||
{
|
||||
key: 1,
|
||||
name: 'John Brown',
|
||||
age: 32,
|
||||
address: 'New York No. 1 Lake Park',
|
||||
tags: ['nice', 'developer'],
|
||||
...generateValues(columnCount),
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
name: 'Jim Green',
|
||||
age: 42,
|
||||
address: 'London No. 1 Lake Park',
|
||||
tags: ['loser'],
|
||||
...generateValues(columnCount),
|
||||
},
|
||||
{
|
||||
key: 3,
|
||||
name: 'Joe Black',
|
||||
age: 32,
|
||||
address: 'Sidney No. 1 Lake Park',
|
||||
tags: ['cool', 'teacher'],
|
||||
...generateValues(columnCount),
|
||||
},
|
||||
];
|
||||
|
||||
const bigdata: any[] = [];
|
||||
for (let i = 0; i < recordCount; i += 1) {
|
||||
bigdata.push({
|
||||
key: i + baseData.length,
|
||||
name: `Dynamic record ${i}`,
|
||||
age: 32 + i,
|
||||
address: `DynamoCity, Dynamic Lane no. ${i}`,
|
||||
...generateValues(columnCount),
|
||||
});
|
||||
}
|
||||
|
||||
export const Basic: ComponentStory<typeof Table> = args => (
|
||||
<ThemeProvider theme={supersetTheme}>
|
||||
<div>
|
||||
<Table {...args} />
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
function handlers(record: object, rowIndex: number) {
|
||||
return {
|
||||
onClick: action(
|
||||
`row onClick, row: ${rowIndex}, record: ${JSON.stringify(record)}`,
|
||||
), // click row
|
||||
onDoubleClick: action(
|
||||
`row onDoubleClick, row: ${rowIndex}, record: ${JSON.stringify(record)}`,
|
||||
), // double click row
|
||||
onContextMenu: action(
|
||||
`row onContextMenu, row: ${rowIndex}, record: ${JSON.stringify(record)}`,
|
||||
), // right button click row
|
||||
onMouseEnter: action(`Mouse Enter, row: ${rowIndex}`), // mouse enter row
|
||||
onMouseLeave: action(`Mouse Leave, row: ${rowIndex}`), // mouse leave row
|
||||
};
|
||||
}
|
||||
|
||||
Basic.args = {
|
||||
data: basicData,
|
||||
columns: basicColumns,
|
||||
size: TableSize.SMALL,
|
||||
onRow: handlers,
|
||||
pageSizeOptions: ['5', '10', '15', '20', '25'],
|
||||
defaultPageSize: 10,
|
||||
};
|
||||
|
||||
export const ManyColumns: ComponentStory<typeof Table> = args => (
|
||||
<ThemeProvider theme={supersetTheme}>
|
||||
<div style={{ height: '350px' }}>
|
||||
<Table {...args} />
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
ManyColumns.args = {
|
||||
data: bigdata,
|
||||
columns: bigColumns,
|
||||
size: TableSize.SMALL,
|
||||
resizable: true,
|
||||
reorderable: true,
|
||||
height: 350,
|
||||
};
|
||||
|
||||
export const Loading: ComponentStory<typeof Table> = args => (
|
||||
<ThemeProvider theme={supersetTheme}>
|
||||
<Table {...args} />
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
Loading.args = {
|
||||
data: basicData,
|
||||
columns: basicColumns,
|
||||
size: TableSize.SMALL,
|
||||
loading: true,
|
||||
};
|
||||
|
||||
export const ResizableColumns: ComponentStory<typeof Table> = args => (
|
||||
<ThemeProvider theme={supersetTheme}>
|
||||
<div>
|
||||
<Table {...args} />
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
ResizableColumns.args = {
|
||||
data: basicData,
|
||||
columns: basicColumns,
|
||||
size: TableSize.SMALL,
|
||||
resizable: true,
|
||||
};
|
||||
|
||||
export const ReorderableColumns: ComponentStory<typeof Table> = args => {
|
||||
const [droppedItem, setDroppedItem] = useState<string | undefined>();
|
||||
const dragOver = (ev: React.DragEvent<HTMLDivElement>) => {
|
||||
ev.preventDefault();
|
||||
const element: HTMLElement | null = ev?.currentTarget as HTMLElement;
|
||||
if (element?.style) {
|
||||
element.style.border = '1px dashed green';
|
||||
}
|
||||
};
|
||||
|
||||
const dragOut = (ev: React.DragEvent<HTMLDivElement>) => {
|
||||
ev.preventDefault();
|
||||
const element: HTMLElement | null = ev?.currentTarget as HTMLElement;
|
||||
if (element?.style) {
|
||||
element.style.border = '1px solid grey';
|
||||
}
|
||||
};
|
||||
|
||||
const dragDrop = (ev: React.DragEvent<HTMLDivElement>) => {
|
||||
const data = ev.dataTransfer?.getData?.(SUPERSET_TABLE_COLUMN);
|
||||
const element: HTMLElement | null = ev?.currentTarget as HTMLElement;
|
||||
if (element?.style) {
|
||||
element.style.border = '1px solid grey';
|
||||
}
|
||||
setDroppedItem(data);
|
||||
};
|
||||
return (
|
||||
<ThemeProvider theme={supersetTheme}>
|
||||
<div>
|
||||
<div
|
||||
onDragOver={(ev: React.DragEvent<HTMLDivElement>) => dragOver(ev)}
|
||||
onDragLeave={(ev: React.DragEvent<HTMLDivElement>) => dragOut(ev)}
|
||||
onDrop={(ev: React.DragEvent<HTMLDivElement>) => dragDrop(ev)}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '40px',
|
||||
border: '1px solid grey',
|
||||
marginBottom: '8px',
|
||||
padding: '8px',
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
>
|
||||
{droppedItem ?? 'Drop column here...'}
|
||||
</div>
|
||||
<Table {...args} />
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
ReorderableColumns.args = {
|
||||
data: basicData,
|
||||
columns: basicColumns,
|
||||
size: TableSize.SMALL,
|
||||
reorderable: true,
|
||||
};
|
||||
|
||||
const rendererData: RendererData[] = [
|
||||
{
|
||||
key: 1,
|
||||
buttonCell: 'Click Me',
|
||||
textCell: 'Some text',
|
||||
euroCell: 45.5,
|
||||
dollarCell: 45.5,
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
buttonCell: 'I am a button',
|
||||
textCell: 'More text',
|
||||
euroCell: 1700,
|
||||
dollarCell: 1700,
|
||||
},
|
||||
{
|
||||
key: 3,
|
||||
buttonCell: 'Button 3',
|
||||
textCell: 'The third string of text',
|
||||
euroCell: 500.567,
|
||||
dollarCell: 500.567,
|
||||
},
|
||||
];
|
||||
|
||||
export const CellRenderers: ComponentStory<typeof Table> = args => (
|
||||
<ThemeProvider theme={supersetTheme}>
|
||||
<div>
|
||||
<Table {...args} />
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
CellRenderers.args = {
|
||||
data: rendererData,
|
||||
columns: rendererColumns,
|
||||
size: TableSize.SMALL,
|
||||
reorderable: true,
|
||||
};
|
|
@ -0,0 +1,80 @@
|
|||
/**
|
||||
* 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 { render, screen, waitFor } from 'spec/helpers/testing-library';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { Table, TableSize } from './index';
|
||||
|
||||
interface BasicData {
|
||||
columnName: string;
|
||||
columnType: string;
|
||||
dataType: string;
|
||||
}
|
||||
|
||||
const testData: BasicData[] = [
|
||||
{
|
||||
columnName: 'Number',
|
||||
columnType: 'Numerical',
|
||||
dataType: 'number',
|
||||
},
|
||||
{
|
||||
columnName: 'String',
|
||||
columnType: 'Physical',
|
||||
dataType: 'string',
|
||||
},
|
||||
{
|
||||
columnName: 'Date',
|
||||
columnType: 'Virtual',
|
||||
dataType: 'date',
|
||||
},
|
||||
];
|
||||
|
||||
const testColumns: ColumnsType<BasicData> = [
|
||||
{
|
||||
title: 'Column Name',
|
||||
dataIndex: 'columnName',
|
||||
key: 'columnName',
|
||||
},
|
||||
{
|
||||
title: 'Column Type',
|
||||
dataIndex: 'columnType',
|
||||
key: 'columnType',
|
||||
},
|
||||
{
|
||||
title: 'Data Type',
|
||||
dataIndex: 'dataType',
|
||||
key: 'dataType',
|
||||
},
|
||||
];
|
||||
|
||||
test('renders with default props', async () => {
|
||||
render(
|
||||
<Table size={TableSize.MIDDLE} columns={testColumns} data={testData} />,
|
||||
);
|
||||
await waitFor(() =>
|
||||
testColumns.forEach(column =>
|
||||
expect(screen.getByText(column.title as string)).toBeInTheDocument(),
|
||||
),
|
||||
);
|
||||
testData.forEach(row => {
|
||||
expect(screen.getByText(row.columnName)).toBeInTheDocument();
|
||||
expect(screen.getByText(row.columnType)).toBeInTheDocument();
|
||||
expect(screen.getByText(row.dataType)).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,69 @@
|
|||
import { Meta, Source, Story, ArgsTable } from '@storybook/addon-docs';
|
||||
|
||||
<Meta title="Design System/Components/Table/Cell Renderers/ActionCell/Overview" />
|
||||
|
||||
# ActionCell
|
||||
|
||||
An ActionCell is used to display an overflow icon that opens a menu allowing the user to take actions
|
||||
specific to the data in the table row that the cell is a member of.
|
||||
|
||||
### [Basic example](./?path=/docs/design-system-components-table-cell-renderers-actioncell--basic)
|
||||
|
||||
<Story id="design-system-components-table-cell-renderers-actioncell--basic" />
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
The action cell accepts an array of objects that define the label, tooltip, onClick callback functions,
|
||||
and an optional data payload to be provided back to the onClick handler function.
|
||||
|
||||
### [Basic example](./?path=/docs/design-system-components-table-cell-renderers-actioncell--basic)
|
||||
|
||||
<Story id="design-system-components-table-cell-renderers-actioncell--basic" />
|
||||
|
||||
```
|
||||
import { ActionMenuItem } from 'src/components/Table/cell-renderers/index';
|
||||
|
||||
export const exampleMenuOptions: ActionMenuItem[] = [
|
||||
{
|
||||
label: 'Action 1',
|
||||
tooltip: "This is a tip, don't spend it all in one place",
|
||||
onClick: (item: ActionMenuItem) => {
|
||||
// eslint-disable-next-line no-alert
|
||||
alert(JSON.stringify(item));
|
||||
},
|
||||
payload: {
|
||||
taco: 'spicy chicken',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Action 2',
|
||||
tooltip: 'This is another tip',
|
||||
onClick: (item: ActionMenuItem) => {
|
||||
// eslint-disable-next-line no-alert
|
||||
alert(JSON.stringify(item));
|
||||
},
|
||||
payload: {
|
||||
taco: 'saucy tofu',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
```
|
||||
|
||||
Within the context of adding an action cell to cell definitions provided to the table using the ActionCell component
|
||||
for the return value from the render function on the cell definition. See the [Basic example](./?path=/docs/design-system-components-table-examples--basic)
|
||||
|
||||
```
|
||||
import ActionCell from './index';
|
||||
|
||||
const cellExample = [
|
||||
{
|
||||
title: 'Actions',
|
||||
dataIndex: 'actions',
|
||||
key: 'actions',
|
||||
render: () => <ActionCell menuOptions={exampleMenuOptions} />,
|
||||
}
|
||||
]
|
||||
```
|
|
@ -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 React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import ActionCell from './index';
|
||||
import { exampleMenuOptions, exampleRow } from './fixtures';
|
||||
|
||||
export default {
|
||||
title: 'Design System/Components/Table/Cell Renderers/ActionCell',
|
||||
component: ActionCell,
|
||||
} as ComponentMeta<typeof ActionCell>;
|
||||
|
||||
export const Basic: ComponentStory<typeof ActionCell> = args => (
|
||||
<ActionCell {...args} />
|
||||
);
|
||||
|
||||
Basic.args = {
|
||||
menuOptions: exampleMenuOptions,
|
||||
row: exampleRow,
|
||||
};
|
|
@ -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 from 'react';
|
||||
import { render, screen } from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import ActionCell, { appendDataToMenu } from './index';
|
||||
import { exampleMenuOptions, exampleRow } from './fixtures';
|
||||
|
||||
test('renders with default props', async () => {
|
||||
const clickHandler = jest.fn();
|
||||
exampleMenuOptions[0].onClick = clickHandler;
|
||||
render(<ActionCell menuOptions={exampleMenuOptions} row={exampleRow} />);
|
||||
// Open the menu
|
||||
userEvent.click(await screen.findByTestId('dropdown-trigger'));
|
||||
// verify all of the menu items are being displayed
|
||||
exampleMenuOptions.forEach((item, index) => {
|
||||
expect(screen.getByText(item.label)).toBeInTheDocument();
|
||||
if (index === 0) {
|
||||
// verify the menu items' onClick gets invoked
|
||||
userEvent.click(screen.getByText(item.label));
|
||||
}
|
||||
});
|
||||
expect(clickHandler).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
/**
|
||||
* Validate that the appendDataToMenu utility function used within the
|
||||
* Action cell menu rendering works as expected
|
||||
*/
|
||||
test('appendDataToMenu utility', () => {
|
||||
exampleMenuOptions.forEach(item => expect(item?.row).toBeUndefined());
|
||||
const modifiedMenuOptions = appendDataToMenu(exampleMenuOptions, exampleRow);
|
||||
modifiedMenuOptions.forEach(item => expect(item?.row).toBeDefined());
|
||||
});
|
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* 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 { action } from '@storybook/addon-actions';
|
||||
import { ActionMenuItem } from './index';
|
||||
|
||||
export const exampleMenuOptions: ActionMenuItem[] = [
|
||||
{
|
||||
label: 'Action 1',
|
||||
tooltip: "This is a tip, don't spend it all in one place",
|
||||
onClick: action('menu item onClick'),
|
||||
payload: {
|
||||
taco: 'spicy chicken',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Action 2',
|
||||
tooltip: 'This is another tip',
|
||||
onClick: action('menu item onClick'),
|
||||
payload: {
|
||||
taco: 'saucy tofu',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const exampleRow = {
|
||||
key: 1,
|
||||
buttonCell: 'Click Me',
|
||||
textCell: 'Some text',
|
||||
euroCell: 45.5,
|
||||
dollarCell: 45.5,
|
||||
};
|
|
@ -0,0 +1,145 @@
|
|||
/**
|
||||
* 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, useEffect } from 'react';
|
||||
import { styled } from '@superset-ui/core';
|
||||
import { Dropdown, IconOrientation } from 'src/components/Dropdown';
|
||||
import { Menu } from 'src/components/Menu';
|
||||
import { MenuProps } from 'antd/lib/menu';
|
||||
|
||||
/**
|
||||
* Props interface for Action Cell Renderer
|
||||
*/
|
||||
export interface ActionCellProps {
|
||||
/**
|
||||
* The Menu option presented to user when menu displays
|
||||
*/
|
||||
menuOptions: ActionMenuItem[];
|
||||
/**
|
||||
* Object representing the data rendering the Table row with attribute for each column
|
||||
*/
|
||||
row: object;
|
||||
}
|
||||
|
||||
export interface ActionMenuItem {
|
||||
/**
|
||||
* Click handler specific to the menu item
|
||||
* @param menuItem The definition of the menu item that was clicked
|
||||
* @returns ActionMenuItem
|
||||
*/
|
||||
onClick: (menuItem: ActionMenuItem) => void;
|
||||
/**
|
||||
* Label user will see displayed in the list of menu options
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* Optional tooltip user will see if they hover over the menu option to get more context
|
||||
*/
|
||||
tooltip?: string;
|
||||
/**
|
||||
* Optional variable that can contain data relevant to the menu item that you
|
||||
* want easy access to in the callback function for the menu
|
||||
*/
|
||||
payload?: any;
|
||||
/**
|
||||
* Object representing the data rendering the Table row with attribute for each column
|
||||
*/
|
||||
row?: object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props interface for ActionMenu
|
||||
*/
|
||||
export interface ActionMenuProps {
|
||||
menuOptions: ActionMenuItem[];
|
||||
setVisible: (visible: boolean) => void;
|
||||
}
|
||||
|
||||
const SHADOW =
|
||||
'box-shadow: 0px 3px 6px -4px rgba(0, 0, 0, 0.12), 0px 9px 28px 8px rgba(0, 0, 0, 0.05)';
|
||||
const FILTER = 'drop-shadow(0px 6px 16px rgba(0, 0, 0, 0.08))';
|
||||
|
||||
const StyledMenu = styled(Menu)`
|
||||
box-shadow: ${SHADOW} !important;
|
||||
filter: ${FILTER} !important;
|
||||
border-radius: 2px !important;
|
||||
-webkit-box-shadow: ${SHADOW} !important;
|
||||
`;
|
||||
|
||||
export const appendDataToMenu = (
|
||||
options: ActionMenuItem[],
|
||||
row: object,
|
||||
): ActionMenuItem[] => {
|
||||
const newOptions = options?.map?.(option => ({
|
||||
...option,
|
||||
row,
|
||||
}));
|
||||
return newOptions;
|
||||
};
|
||||
|
||||
function ActionMenu(props: ActionMenuProps) {
|
||||
const { menuOptions, setVisible } = props;
|
||||
const handleClick: MenuProps['onClick'] = ({ key }) => {
|
||||
setVisible?.(false);
|
||||
const menuItem = menuOptions[key];
|
||||
if (menuItem) {
|
||||
menuItem?.onClick?.(menuItem);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledMenu onClick={handleClick}>
|
||||
{menuOptions?.map?.((option: ActionMenuItem, index: number) => (
|
||||
<Menu.Item key={index}>{option?.label}</Menu.Item>
|
||||
))}
|
||||
</StyledMenu>
|
||||
);
|
||||
}
|
||||
|
||||
export function ActionCell(props: ActionCellProps) {
|
||||
const { menuOptions, row } = props;
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [appendedMenuOptions, setAppendedMenuOptions] = useState(
|
||||
appendDataToMenu(menuOptions, row),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const newOptions = appendDataToMenu(menuOptions, row);
|
||||
setAppendedMenuOptions(newOptions);
|
||||
}, [menuOptions, row]);
|
||||
|
||||
const handleVisibleChange = (flag: boolean) => {
|
||||
setVisible(flag);
|
||||
};
|
||||
return (
|
||||
<Dropdown
|
||||
iconOrientation={IconOrientation.HORIZONTAL}
|
||||
onVisibleChange={handleVisibleChange}
|
||||
trigger={['click']}
|
||||
overlay={
|
||||
<ActionMenu menuOptions={appendedMenuOptions} setVisible={setVisible} />
|
||||
}
|
||||
disabled={
|
||||
!(appendedMenuOptions?.length && appendedMenuOptions.length > 0)
|
||||
}
|
||||
visible={visible}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default ActionCell;
|
|
@ -0,0 +1,62 @@
|
|||
/**
|
||||
* 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 { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { ButtonCell } from './index';
|
||||
|
||||
export default {
|
||||
title: 'Design System/Components/Table/Cell Renderers/ButtonCell',
|
||||
component: ButtonCell,
|
||||
} as ComponentMeta<typeof ButtonCell>;
|
||||
|
||||
const clickHandler = action('button cell onClick');
|
||||
|
||||
export const Basic: ComponentStory<typeof ButtonCell> = args => (
|
||||
<ButtonCell {...args} />
|
||||
);
|
||||
|
||||
Basic.args = {
|
||||
onClick: clickHandler,
|
||||
label: 'Primary',
|
||||
row: {
|
||||
key: 1,
|
||||
buttonCell: 'Click Me',
|
||||
textCell: 'Some text',
|
||||
euroCell: 45.5,
|
||||
dollarCell: 45.5,
|
||||
},
|
||||
};
|
||||
|
||||
export const Secondary: ComponentStory<typeof ButtonCell> = args => (
|
||||
<ButtonCell {...args} />
|
||||
);
|
||||
|
||||
Secondary.args = {
|
||||
onClick: clickHandler,
|
||||
label: 'Secondary',
|
||||
buttonStyle: 'secondary',
|
||||
row: {
|
||||
key: 1,
|
||||
buttonCell: 'Click Me',
|
||||
textCell: 'Some text',
|
||||
euroCell: 45.5,
|
||||
dollarCell: 45.5,
|
||||
},
|
||||
};
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* 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 { render, screen } from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import ButtonCell from './index';
|
||||
import { exampleRow } from '../fixtures';
|
||||
|
||||
test('renders with default props', async () => {
|
||||
const clickHandler = jest.fn();
|
||||
const BUTTON_LABEL = 'Button Label';
|
||||
|
||||
render(
|
||||
<ButtonCell
|
||||
label={BUTTON_LABEL}
|
||||
key={5}
|
||||
index={0}
|
||||
row={exampleRow}
|
||||
onClick={clickHandler}
|
||||
/>,
|
||||
);
|
||||
await userEvent.click(screen.getByText(BUTTON_LABEL));
|
||||
expect(clickHandler).toHaveBeenCalled();
|
||||
});
|
|
@ -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 React from 'react';
|
||||
import Button, { ButtonStyle, ButtonSize } from 'src/components/Button';
|
||||
|
||||
type onClickFunction = (row: object, index: number) => void;
|
||||
|
||||
export interface ButtonCellProps {
|
||||
label: string;
|
||||
onClick: onClickFunction;
|
||||
row: object;
|
||||
index: number;
|
||||
tooltip?: string;
|
||||
buttonStyle?: ButtonStyle;
|
||||
buttonSize?: ButtonSize;
|
||||
}
|
||||
|
||||
export function ButtonCell(props: ButtonCellProps) {
|
||||
const {
|
||||
label,
|
||||
onClick,
|
||||
row,
|
||||
index,
|
||||
tooltip,
|
||||
buttonStyle = 'primary',
|
||||
buttonSize = 'small',
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Button
|
||||
buttonStyle={buttonStyle}
|
||||
buttonSize={buttonSize}
|
||||
onClick={() => onClick?.(row, index)}
|
||||
key={`${buttonStyle}_${buttonSize}`}
|
||||
tooltip={tooltip}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export default ButtonCell;
|
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* 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 { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { CurrencyCode, LocaleCode, NumericCell, Style } from './index';
|
||||
|
||||
export default {
|
||||
title: 'Design System/Components/Table/Cell Renderers/NumericCell',
|
||||
component: NumericCell,
|
||||
} as ComponentMeta<typeof NumericCell>;
|
||||
|
||||
export const Basic: ComponentStory<typeof NumericCell> = args => (
|
||||
<NumericCell {...args} />
|
||||
);
|
||||
|
||||
Basic.args = {
|
||||
value: 5678943,
|
||||
};
|
||||
|
||||
export const FrenchLocale: ComponentStory<typeof NumericCell> = args => (
|
||||
<NumericCell {...args} />
|
||||
);
|
||||
|
||||
FrenchLocale.args = {
|
||||
value: 5678943,
|
||||
locale: LocaleCode.fr,
|
||||
options: {
|
||||
style: Style.CURRENCY,
|
||||
currency: CurrencyCode.EUR,
|
||||
},
|
||||
};
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* 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 { render, screen } from 'spec/helpers/testing-library';
|
||||
import NumericCell, { CurrencyCode, LocaleCode, Style } from './index';
|
||||
|
||||
test('renders with French locale and Euro currency format', () => {
|
||||
render(
|
||||
<NumericCell
|
||||
value={5678943}
|
||||
locale={LocaleCode.fr}
|
||||
options={{
|
||||
style: Style.CURRENCY,
|
||||
currency: CurrencyCode.EUR,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText('5 678 943,00 €')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders with English US locale and USD currency format', () => {
|
||||
render(
|
||||
<NumericCell
|
||||
value={5678943}
|
||||
locale={LocaleCode.en_US}
|
||||
options={{
|
||||
style: Style.CURRENCY,
|
||||
currency: CurrencyCode.USD,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText('$5,678,943.00')).toBeInTheDocument();
|
||||
});
|
|
@ -0,0 +1,418 @@
|
|||
/**
|
||||
* 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 { logging } from '@superset-ui/core';
|
||||
|
||||
export interface NumericCellProps {
|
||||
/**
|
||||
* The number to display (before optional formatting applied)
|
||||
*/
|
||||
value: number;
|
||||
/**
|
||||
* ISO 639-1 language code with optional region or script modifier (e.g. en_US).
|
||||
*/
|
||||
locale?: LocaleCode;
|
||||
/**
|
||||
* Options for number formatting
|
||||
*/
|
||||
options?: NumberOptions;
|
||||
}
|
||||
|
||||
interface NumberOptions {
|
||||
/**
|
||||
* Style of number to display
|
||||
*/
|
||||
style?: Style;
|
||||
|
||||
/**
|
||||
* ISO 4217 currency code
|
||||
*/
|
||||
currency?: CurrencyCode;
|
||||
|
||||
/**
|
||||
* Languages in the form of a ISO 639-1 language code with optional region or script modifier (e.g. de_AT).
|
||||
*/
|
||||
maximumFractionDigits?: number;
|
||||
|
||||
/**
|
||||
* A number from 1 to 21 (default is 21)
|
||||
*/
|
||||
maximumSignificantDigits?: number;
|
||||
|
||||
/**
|
||||
* A number from 0 to 20 (default is 3)
|
||||
*/
|
||||
minimumFractionDigits?: number;
|
||||
|
||||
/**
|
||||
* A number from 1 to 21 (default is 1)
|
||||
*/
|
||||
minimumIntegerDigits?: number;
|
||||
|
||||
/**
|
||||
* A number from 1 to 21 (default is 21)
|
||||
*/
|
||||
minimumSignificantDigits?: number;
|
||||
}
|
||||
|
||||
export enum Style {
|
||||
CURRENCY = 'currency',
|
||||
DECIMAL = 'decimal',
|
||||
PERCENT = 'percent',
|
||||
}
|
||||
|
||||
export enum CurrencyDisplay {
|
||||
SYMBOL = 'symbol',
|
||||
CODE = 'code',
|
||||
NAME = 'name',
|
||||
}
|
||||
|
||||
export enum LocaleCode {
|
||||
af = 'af',
|
||||
ak = 'ak',
|
||||
sq = 'sq',
|
||||
am = 'am',
|
||||
ar = 'ar',
|
||||
hy = 'hy',
|
||||
as = 'as',
|
||||
az = 'az',
|
||||
bm = 'bm',
|
||||
bn = 'bn',
|
||||
eu = 'eu',
|
||||
be = 'be',
|
||||
bs = 'bs',
|
||||
br = 'br',
|
||||
bg = 'bg',
|
||||
my = 'my',
|
||||
ca = 'ca',
|
||||
ce = 'ce',
|
||||
zh = 'zh',
|
||||
zh_Hans = 'zh-Hans',
|
||||
zh_Hant = 'zh-Hant',
|
||||
cu = 'cu',
|
||||
kw = 'kw',
|
||||
co = 'co',
|
||||
hr = 'hr',
|
||||
cs = 'cs',
|
||||
da = 'da',
|
||||
nl = 'nl',
|
||||
nl_BE = 'nl-BE',
|
||||
dz = 'dz',
|
||||
en = 'en',
|
||||
en_AU = 'en-AU',
|
||||
en_CA = 'en-CA',
|
||||
en_GB = 'en-GB',
|
||||
en_US = 'en-US',
|
||||
eo = 'eo',
|
||||
et = 'et',
|
||||
ee = 'ee',
|
||||
fo = 'fo',
|
||||
fi = 'fi',
|
||||
fr = 'fr',
|
||||
fr_CA = 'fr-CA',
|
||||
fr_CH = 'fr-CH',
|
||||
ff = 'ff',
|
||||
gl = 'gl',
|
||||
lg = 'lg',
|
||||
ka = 'ka',
|
||||
de = 'de',
|
||||
de_AT = 'de-AT',
|
||||
de_CH = 'de-CH',
|
||||
el = 'el',
|
||||
gu = 'gu',
|
||||
ht = 'ht',
|
||||
ha = 'ha',
|
||||
he = 'he',
|
||||
hi = 'hi',
|
||||
hu = 'hu',
|
||||
is = 'is',
|
||||
ig = 'ig',
|
||||
id = 'id',
|
||||
ia = 'ia',
|
||||
ga = 'ga',
|
||||
it = 'it',
|
||||
ja = 'ja',
|
||||
jv = 'jv',
|
||||
kl = 'kl',
|
||||
kn = 'kn',
|
||||
ks = 'ks',
|
||||
kk = 'kk',
|
||||
km = 'km',
|
||||
ki = 'ki',
|
||||
rw = 'rw',
|
||||
ko = 'ko',
|
||||
ku = 'ku',
|
||||
ky = 'ky',
|
||||
lo = 'lo',
|
||||
la = 'la',
|
||||
lv = 'lv',
|
||||
ln = 'ln',
|
||||
lt = 'lt',
|
||||
lu = 'lu',
|
||||
lb = 'lb',
|
||||
mk = 'mk',
|
||||
mg = 'mg',
|
||||
ms = 'ms',
|
||||
ml = 'ml',
|
||||
mt = 'mt',
|
||||
gv = 'gv',
|
||||
mi = 'mi',
|
||||
mr = 'mr',
|
||||
mn = 'mn',
|
||||
ne = 'ne',
|
||||
nd = 'nd',
|
||||
se = 'se',
|
||||
nb = 'nb',
|
||||
nn = 'nn',
|
||||
ny = 'ny',
|
||||
or = 'or',
|
||||
om = 'om',
|
||||
os = 'os',
|
||||
ps = 'ps',
|
||||
fa = 'fa',
|
||||
fa_AF = 'fa-AF',
|
||||
pl = 'pl',
|
||||
pt = 'pt',
|
||||
pt_BR = 'pt-BR',
|
||||
pt_PT = 'pt-PT',
|
||||
pa = 'pa',
|
||||
qu = 'qu',
|
||||
ro = 'ro',
|
||||
ro_MD = 'ro-MD',
|
||||
rm = 'rm',
|
||||
rn = 'rn',
|
||||
ru = 'ru',
|
||||
sm = 'sm',
|
||||
sg = 'sg',
|
||||
sa = 'sa',
|
||||
gd = 'gd',
|
||||
sr = 'sr',
|
||||
sn = 'sn',
|
||||
ii = 'ii',
|
||||
sd = 'sd',
|
||||
si = 'si',
|
||||
sk = 'sk',
|
||||
sl = 'sl',
|
||||
so = 'so',
|
||||
st = 'st',
|
||||
es = 'es',
|
||||
es_ES = 'es-ES',
|
||||
es_MX = 'es-MX',
|
||||
su = 'su',
|
||||
sw = 'sw',
|
||||
sw_CD = 'sw-CD',
|
||||
sv = 'sv',
|
||||
tg = 'tg',
|
||||
ta = 'ta',
|
||||
tt = 'tt',
|
||||
te = 'te',
|
||||
th = 'th',
|
||||
bo = 'bo',
|
||||
ti = 'ti',
|
||||
to = 'to',
|
||||
tr = 'tr',
|
||||
tk = 'tk',
|
||||
uk = 'uk',
|
||||
ur = 'ur',
|
||||
ug = 'ug',
|
||||
uz = 'uz',
|
||||
vi = 'vi',
|
||||
vo = 'vo',
|
||||
cy = 'cy',
|
||||
fy = 'fy',
|
||||
wo = 'wo',
|
||||
xh = 'xh',
|
||||
yi = 'yi',
|
||||
yo = 'yo',
|
||||
zu = 'zu',
|
||||
}
|
||||
|
||||
export enum CurrencyCode {
|
||||
AED = 'AED',
|
||||
AFN = 'AFN',
|
||||
ALL = 'ALL',
|
||||
AMD = 'AMD',
|
||||
ANG = 'ANG',
|
||||
AOA = 'AOA',
|
||||
ARS = 'ARS',
|
||||
AUD = 'AUD',
|
||||
AWG = 'AWG',
|
||||
AZN = 'AZN',
|
||||
BAM = 'BAM',
|
||||
BBD = 'BBD',
|
||||
BDT = 'BDT',
|
||||
BGN = 'BGN',
|
||||
BHD = 'BHD',
|
||||
BIF = 'BIF',
|
||||
BMD = 'BMD',
|
||||
BND = 'BND',
|
||||
BOB = 'BOB',
|
||||
BRL = 'BRL',
|
||||
BSD = 'BSD',
|
||||
BTN = 'BTN',
|
||||
BWP = 'BWP',
|
||||
BYN = 'BYN',
|
||||
BZD = 'BZD',
|
||||
CAD = 'CAD',
|
||||
CDF = 'CDF',
|
||||
CHF = 'CHF',
|
||||
CLP = 'CLP',
|
||||
CNY = 'CNY',
|
||||
COP = 'COP',
|
||||
CRC = 'CRC',
|
||||
CUC = 'CUC',
|
||||
CUP = 'CUP',
|
||||
CVE = 'CVE',
|
||||
CZK = 'CZK',
|
||||
DJF = 'DJF',
|
||||
DKK = 'DKK',
|
||||
DOP = 'DOP',
|
||||
DZD = 'DZD',
|
||||
EGP = 'EGP',
|
||||
ERN = 'ERN',
|
||||
ETB = 'ETB',
|
||||
EUR = 'EUR',
|
||||
FJD = 'FJD',
|
||||
FKP = 'FKP',
|
||||
GBP = 'GBP',
|
||||
GEL = 'GEL',
|
||||
GHS = 'GHS',
|
||||
GIP = 'GIP',
|
||||
GMD = 'GMD',
|
||||
GNF = 'GNF',
|
||||
GTQ = 'GTQ',
|
||||
GYD = 'GYD',
|
||||
HKD = 'HKD',
|
||||
HNL = 'HNL',
|
||||
HRK = 'HRK',
|
||||
HTG = 'HTG',
|
||||
HUF = 'HUF',
|
||||
IDR = 'IDR',
|
||||
ILS = 'ILS',
|
||||
INR = 'INR',
|
||||
IQD = 'IQD',
|
||||
IRR = 'IRR',
|
||||
ISK = 'ISK',
|
||||
JMD = 'JMD',
|
||||
JOD = 'JOD',
|
||||
JPY = 'JPY',
|
||||
KES = 'KES',
|
||||
KGS = 'KGS',
|
||||
KHR = 'KHR',
|
||||
KMF = 'KMF',
|
||||
KPW = 'KPW',
|
||||
KRW = 'KRW',
|
||||
KWD = 'KWD',
|
||||
KYD = 'KYD',
|
||||
KZT = 'KZT',
|
||||
LAK = 'LAK',
|
||||
LBP = 'LBP',
|
||||
LKR = 'LKR',
|
||||
LRD = 'LRD',
|
||||
LSL = 'LSL',
|
||||
LYD = 'LYD',
|
||||
MAD = 'MAD',
|
||||
MDL = 'MDL',
|
||||
MGA = 'MGA',
|
||||
MKD = 'MKD',
|
||||
MMK = 'MMK',
|
||||
MNT = 'MNT',
|
||||
MOP = 'MOP',
|
||||
MRU = 'MRU',
|
||||
MUR = 'MUR',
|
||||
MVR = 'MVR',
|
||||
MWK = 'MWK',
|
||||
MXN = 'MXN',
|
||||
MYR = 'MYR',
|
||||
MZN = 'MZN',
|
||||
NAD = 'NAD',
|
||||
NGN = 'NGN',
|
||||
NIO = 'NIO',
|
||||
NOK = 'NOK',
|
||||
NPR = 'NPR',
|
||||
NZD = 'NZD',
|
||||
OMR = 'OMR',
|
||||
PAB = 'PAB',
|
||||
PEN = 'PEN',
|
||||
PGK = 'PGK',
|
||||
PHP = 'PHP',
|
||||
PKR = 'PKR',
|
||||
PLN = 'PLN',
|
||||
PYG = 'PYG',
|
||||
QAR = 'QAR',
|
||||
RON = 'RON',
|
||||
RSD = 'RSD',
|
||||
RUB = 'RUB',
|
||||
RWF = 'RWF',
|
||||
SAR = 'SAR',
|
||||
SBD = 'SBD',
|
||||
SCR = 'SCR',
|
||||
SDG = 'SDG',
|
||||
SEK = 'SEK',
|
||||
SGD = 'SGD',
|
||||
SHP = 'SHP',
|
||||
SLL = 'SLL',
|
||||
SOS = 'SOS',
|
||||
SRD = 'SRD',
|
||||
SSP = 'SSP',
|
||||
STN = 'STN',
|
||||
SVC = 'SVC',
|
||||
SYP = 'SYP',
|
||||
SZL = 'SZL',
|
||||
THB = 'THB',
|
||||
TJS = 'TJS',
|
||||
TMT = 'TMT',
|
||||
TND = 'TND',
|
||||
TOP = 'TOP',
|
||||
TRY = 'TRY',
|
||||
TTD = 'TTD',
|
||||
TWD = 'TWD',
|
||||
TZS = 'TZS',
|
||||
UAH = 'UAH',
|
||||
UGX = 'UGX',
|
||||
USD = 'USD',
|
||||
UYU = 'UYU',
|
||||
UZS = 'UZS',
|
||||
VES = 'VES',
|
||||
VND = 'VND',
|
||||
VUV = 'VUV',
|
||||
WST = 'WST',
|
||||
XAF = 'XAF',
|
||||
XCD = 'XCD',
|
||||
XOF = 'XOF',
|
||||
XPF = 'XPF',
|
||||
YER = 'YER',
|
||||
ZAR = 'ZAR',
|
||||
ZMW = 'ZMW',
|
||||
ZWL = 'ZWL',
|
||||
}
|
||||
|
||||
export function NumericCell(props: NumericCellProps) {
|
||||
const { value, locale = LocaleCode.en_US, options } = props;
|
||||
let displayValue = value?.toString() ?? value;
|
||||
try {
|
||||
displayValue = value?.toLocaleString?.(locale, options);
|
||||
} catch (e) {
|
||||
logging.error(e);
|
||||
}
|
||||
|
||||
return <span>{displayValue}</span>;
|
||||
}
|
||||
|
||||
export default NumericCell;
|
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* 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 const exampleRow = {
|
||||
key: 1,
|
||||
buttonCell: 'Click Me',
|
||||
textCell: 'Some text',
|
||||
euroCell: 45.5,
|
||||
dollarCell: 45.5,
|
||||
};
|
|
@ -0,0 +1,326 @@
|
|||
/**
|
||||
* 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, useEffect, useRef, ReactElement } from 'react';
|
||||
import { Table as AntTable, ConfigProvider } from 'antd';
|
||||
import type {
|
||||
ColumnType,
|
||||
ColumnGroupType,
|
||||
TableProps as AntTableProps,
|
||||
} from 'antd/es/table';
|
||||
import { t, useTheme, logging } from '@superset-ui/core';
|
||||
import Loading from 'src/components/Loading';
|
||||
import styled, { StyledComponent } from '@emotion/styled';
|
||||
import InteractiveTableUtils from './utils/InteractiveTableUtils';
|
||||
|
||||
export const SUPERSET_TABLE_COLUMN = 'superset/table-column';
|
||||
export interface TableDataType {
|
||||
key: React.Key;
|
||||
}
|
||||
|
||||
export declare type ColumnsType<RecordType = unknown> = (
|
||||
| ColumnGroupType<RecordType>
|
||||
| ColumnType<RecordType>
|
||||
)[];
|
||||
|
||||
export enum SelectionType {
|
||||
'DISABLED' = 'disabled',
|
||||
'SINGLE' = 'single',
|
||||
'MULTI' = 'multi',
|
||||
}
|
||||
|
||||
export interface Locale {
|
||||
/**
|
||||
* Text contained within the Table UI.
|
||||
*/
|
||||
filterTitle: string;
|
||||
filterConfirm: string;
|
||||
filterReset: string;
|
||||
filterEmptyText: string;
|
||||
filterCheckall: string;
|
||||
filterSearchPlaceholder: string;
|
||||
emptyText: string;
|
||||
selectAll: string;
|
||||
selectInvert: string;
|
||||
selectNone: string;
|
||||
selectionAll: string;
|
||||
sortTitle: string;
|
||||
expand: string;
|
||||
collapse: string;
|
||||
triggerDesc: string;
|
||||
triggerAsc: string;
|
||||
cancelSort: string;
|
||||
}
|
||||
|
||||
export interface TableProps extends AntTableProps<TableProps> {
|
||||
/**
|
||||
* Data that will populate the each row and map to the column key.
|
||||
*/
|
||||
data: object[];
|
||||
/**
|
||||
* Table column definitions.
|
||||
*/
|
||||
columns: ColumnsType<any>;
|
||||
/**
|
||||
* Array of row keys to represent list of selected rows.
|
||||
*/
|
||||
selectedRows?: React.Key[];
|
||||
/**
|
||||
* Callback function invoked when a row is selected by user.
|
||||
*/
|
||||
handleRowSelection?: Function;
|
||||
/**
|
||||
* Controls the size of the table.
|
||||
*/
|
||||
size: TableSize;
|
||||
/**
|
||||
* Adjusts the padding around elements for different amounts of spacing between elements.
|
||||
*/
|
||||
selectionType?: SelectionType;
|
||||
/*
|
||||
* Places table in visual loading state. Use while waiting to retrieve data or perform an async operation that will update the table.
|
||||
*/
|
||||
loading?: boolean;
|
||||
/**
|
||||
* Uses a sticky header which always displays when vertically scrolling the table. Default: true
|
||||
*/
|
||||
sticky?: boolean;
|
||||
/**
|
||||
* Controls if columns are resizable by user.
|
||||
*/
|
||||
resizable?: boolean;
|
||||
/**
|
||||
* EXPERIMENTAL: Controls if columns are re-orderable by user drag drop.
|
||||
*/
|
||||
reorderable?: boolean;
|
||||
/**
|
||||
* Default number of rows table will display per page of data.
|
||||
*/
|
||||
defaultPageSize?: number;
|
||||
/**
|
||||
* Array of numeric options for the number of rows table will display per page of data.
|
||||
* The user can select from these options in the page size drop down menu.
|
||||
*/
|
||||
pageSizeOptions?: string[];
|
||||
/**
|
||||
* Set table to display no data even if data has been provided
|
||||
*/
|
||||
hideData?: boolean;
|
||||
/**
|
||||
* emptyComponent
|
||||
*/
|
||||
emptyComponent?: ReactElement;
|
||||
/**
|
||||
* Enables setting the text displayed in various components and tooltips within the Table UI.
|
||||
*/
|
||||
locale?: Locale;
|
||||
/**
|
||||
* Restricts the visible height of the table and allows for internal scrolling within the table
|
||||
* when the number of rows exceeds the visible space.
|
||||
*/
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export enum TableSize {
|
||||
SMALL = 'small',
|
||||
MIDDLE = 'middle',
|
||||
}
|
||||
|
||||
const defaultRowSelection: React.Key[] = [];
|
||||
// This accounts for the tables header and pagination if user gives table instance a height. this is a temp solution
|
||||
const HEIGHT_OFFSET = 108;
|
||||
|
||||
const StyledTable: StyledComponent<any> = styled(AntTable)<any>`
|
||||
${({ theme, height }) => `
|
||||
.ant-table-body {
|
||||
overflow: scroll;
|
||||
height: ${height ? `${height - HEIGHT_OFFSET}px` : undefined};
|
||||
}
|
||||
|
||||
th.ant-table-cell {
|
||||
font-weight: ${theme.typography.weights.bold};
|
||||
color: ${theme.colors.grayscale.dark1};
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.ant-pagination-item-active {
|
||||
border-color: ${theme.colors.primary.base};
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const defaultLocale = {
|
||||
filterTitle: t('Filter menu'),
|
||||
filterConfirm: t('OK'),
|
||||
filterReset: t('Reset'),
|
||||
filterEmptyText: t('No filters'),
|
||||
filterCheckall: t('Select all items'),
|
||||
filterSearchPlaceholder: t('Search in filters'),
|
||||
emptyText: t('No data'),
|
||||
selectAll: t('Select current page'),
|
||||
selectInvert: t('Invert current page'),
|
||||
selectNone: t('Clear all data'),
|
||||
selectionAll: t('Select all data'),
|
||||
sortTitle: t('Sort'),
|
||||
expand: t('Expand row'),
|
||||
collapse: t('Collapse row'),
|
||||
triggerDesc: t('Click to sort descending'),
|
||||
triggerAsc: t('Click to sort ascending'),
|
||||
cancelSort: t('Click to cancel sorting'),
|
||||
};
|
||||
|
||||
const selectionMap = {};
|
||||
selectionMap[SelectionType.MULTI] = 'checkbox';
|
||||
selectionMap[SelectionType.SINGLE] = 'radio';
|
||||
selectionMap[SelectionType.DISABLED] = null;
|
||||
|
||||
export function Table(props: TableProps) {
|
||||
const {
|
||||
data,
|
||||
columns,
|
||||
selectedRows = defaultRowSelection,
|
||||
handleRowSelection,
|
||||
size,
|
||||
selectionType = SelectionType.DISABLED,
|
||||
sticky = true,
|
||||
loading = false,
|
||||
resizable = false,
|
||||
reorderable = false,
|
||||
defaultPageSize = 15,
|
||||
pageSizeOptions = ['5', '15', '25', '50', '100'],
|
||||
hideData = false,
|
||||
emptyComponent,
|
||||
locale,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
||||
const [derivedColumns, setDerivedColumns] = useState(columns);
|
||||
const [pageSize, setPageSize] = useState(defaultPageSize);
|
||||
const [mergedLocale, setMergedLocale] = useState({ ...defaultLocale });
|
||||
const [selectedRowKeys, setSelectedRowKeys] =
|
||||
useState<React.Key[]>(selectedRows);
|
||||
const interactiveTableUtils = useRef<InteractiveTableUtils | null>(null);
|
||||
|
||||
const onSelectChange = (newSelectedRowKeys: React.Key[]) => {
|
||||
setSelectedRowKeys(newSelectedRowKeys);
|
||||
handleRowSelection?.(newSelectedRowKeys);
|
||||
};
|
||||
|
||||
const selectionTypeValue = selectionMap[selectionType];
|
||||
const rowSelection = {
|
||||
type: selectionTypeValue,
|
||||
selectedRowKeys,
|
||||
onChange: onSelectChange,
|
||||
};
|
||||
|
||||
const renderEmpty = () =>
|
||||
emptyComponent ?? <div>{mergedLocale.emptyText}</div>;
|
||||
|
||||
// Log use of experimental features
|
||||
useEffect(() => {
|
||||
if (reorderable === true) {
|
||||
logging.warn(
|
||||
'EXPERIMENTAL FEATURE ENABLED: The "reorderable" prop of Table is experimental and NOT recommended for use in production deployments.',
|
||||
);
|
||||
}
|
||||
if (resizable === true) {
|
||||
logging.warn(
|
||||
'EXPERIMENTAL FEATURE ENABLED: The "resizable" prop of Table is experimental and NOT recommended for use in production deployments.',
|
||||
);
|
||||
}
|
||||
}, [reorderable, resizable]);
|
||||
|
||||
useEffect(() => {
|
||||
let updatedLocale;
|
||||
if (locale) {
|
||||
// This spread allows for locale to only contain a subset of locale overrides on props
|
||||
updatedLocale = { ...defaultLocale, ...locale };
|
||||
} else {
|
||||
updatedLocale = { ...defaultLocale };
|
||||
}
|
||||
setMergedLocale(updatedLocale);
|
||||
}, [locale]);
|
||||
|
||||
useEffect(() => {
|
||||
if (interactiveTableUtils.current) {
|
||||
interactiveTableUtils.current?.clearListeners();
|
||||
}
|
||||
const table = wrapperRef.current?.getElementsByTagName('table')[0];
|
||||
if (table) {
|
||||
interactiveTableUtils.current = new InteractiveTableUtils(
|
||||
table,
|
||||
derivedColumns,
|
||||
setDerivedColumns,
|
||||
);
|
||||
if (reorderable) {
|
||||
interactiveTableUtils?.current?.initializeDragDropColumns(
|
||||
reorderable,
|
||||
table,
|
||||
);
|
||||
}
|
||||
if (resizable) {
|
||||
interactiveTableUtils?.current?.initializeResizableColumns(
|
||||
resizable,
|
||||
table,
|
||||
);
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
interactiveTableUtils?.current?.clearListeners?.();
|
||||
};
|
||||
/**
|
||||
* We DO NOT want this effect to trigger when derivedColumns changes as it will break functionality
|
||||
* The exclusion from the effect dependencies is intentional and should not be modified
|
||||
*/
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [wrapperRef, reorderable, resizable, interactiveTableUtils]);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<ConfigProvider renderEmpty={renderEmpty}>
|
||||
<div ref={wrapperRef}>
|
||||
<StyledTable
|
||||
{...rest}
|
||||
loading={{ spinning: loading ?? false, indicator: <Loading /> }}
|
||||
hasData={hideData ? false : data}
|
||||
rowSelection={selectionTypeValue ? rowSelection : undefined}
|
||||
columns={derivedColumns}
|
||||
dataSource={hideData ? [undefined] : data}
|
||||
size={size}
|
||||
sticky={sticky}
|
||||
pagination={{
|
||||
hideOnSinglePage: true,
|
||||
pageSize,
|
||||
pageSizeOptions,
|
||||
onShowSizeChange: (page: number, size: number) => setPageSize(size),
|
||||
}}
|
||||
showSorterTooltip={false}
|
||||
locale={mergedLocale}
|
||||
theme={theme}
|
||||
/>
|
||||
</div>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default Table;
|
|
@ -0,0 +1,100 @@
|
|||
/**
|
||||
* 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 { alphabeticalSort, numericalSort } from './sorters';
|
||||
|
||||
const rows = [
|
||||
{
|
||||
name: 'Deathstar Lamp',
|
||||
category: 'Lamp',
|
||||
cost: 75.99,
|
||||
},
|
||||
{
|
||||
name: 'Desk Lamp',
|
||||
category: 'Lamp',
|
||||
cost: 15.99,
|
||||
},
|
||||
{
|
||||
name: 'Bedside Lamp',
|
||||
category: 'Lamp',
|
||||
cost: 15.99,
|
||||
},
|
||||
{ name: 'Drafting Desk', category: 'Desk', cost: 125 },
|
||||
{ name: 'Sit / Stand Desk', category: 'Desk', cost: 275.99 },
|
||||
];
|
||||
|
||||
/**
|
||||
* NOTE: Sorters for antd table use < 0, 0, > 0 for sorting
|
||||
* -1 or less means the first item comes after the second item
|
||||
* 0 means the items sort values is equivalent
|
||||
* 1 or greater means the first item comes before the second item
|
||||
*/
|
||||
test('alphabeticalSort sorts correctly', () => {
|
||||
expect(alphabeticalSort('name', rows[0], rows[1])).toBe(-1);
|
||||
expect(alphabeticalSort('name', rows[1], rows[0])).toBe(1);
|
||||
expect(alphabeticalSort('category', rows[1], rows[0])).toBe(0);
|
||||
});
|
||||
|
||||
test('numericalSort sorts correctly', () => {
|
||||
expect(numericalSort('cost', rows[1], rows[2])).toBe(0);
|
||||
expect(numericalSort('cost', rows[1], rows[0])).toBeLessThan(0);
|
||||
expect(numericalSort('cost', rows[4], rows[1])).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
/**
|
||||
* We want to make sure our sorters do not throw runtime errors given bad inputs.
|
||||
* Runtime Errors in a sorter will cause a catastrophic React lifecycle error and produce white screen of death
|
||||
* In the case the sorter cannot perform the comparison it should return undefined and the next sort step will proceed without error
|
||||
*/
|
||||
test('alphabeticalSort bad inputs no errors', () => {
|
||||
// @ts-ignore
|
||||
expect(alphabeticalSort('name', null, null)).toBe(undefined);
|
||||
// incorrect non-object values
|
||||
// @ts-ignore
|
||||
expect(alphabeticalSort('name', 3, [])).toBe(undefined);
|
||||
// incorrect object values without specificed key
|
||||
expect(alphabeticalSort('name', {}, {})).toBe(undefined);
|
||||
// Object as value for name when it should be a string
|
||||
expect(
|
||||
alphabeticalSort(
|
||||
'name',
|
||||
{ name: { title: 'the name attribute should not be an object' } },
|
||||
{ name: 'Doug' },
|
||||
),
|
||||
).toBe(undefined);
|
||||
});
|
||||
|
||||
test('numericalSort bad inputs no errors', () => {
|
||||
// @ts-ignore
|
||||
expect(numericalSort('name', undefined, undefined)).toBe(NaN);
|
||||
// @ts-ignore
|
||||
expect(numericalSort('name', null, null)).toBe(NaN);
|
||||
// incorrect non-object values
|
||||
// @ts-ignore
|
||||
expect(numericalSort('name', 3, [])).toBe(NaN);
|
||||
// incorrect object values without specified key
|
||||
expect(numericalSort('name', {}, {})).toBe(NaN);
|
||||
// Object as value for name when it should be a string
|
||||
expect(
|
||||
numericalSort(
|
||||
'name',
|
||||
{ name: { title: 'the name attribute should not be an object' } },
|
||||
{ name: 'Doug' },
|
||||
),
|
||||
).toBe(NaN);
|
||||
});
|
|
@ -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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param key The name of the row's attribute used to compare values for alphabetical sorting
|
||||
* @param a First row object to compare
|
||||
* @param b Second row object to compare
|
||||
* @returns number
|
||||
*/
|
||||
export const alphabeticalSort = (key: string, a: object, b: object): number =>
|
||||
a?.[key]?.localeCompare?.(b?.[key]);
|
||||
|
||||
/**
|
||||
* @param key The name of the row's attribute used to compare values for numerical sorting
|
||||
* @param a First row object to compare
|
||||
* @param b Second row object to compare
|
||||
* @returns number
|
||||
*/
|
||||
export const numericalSort = (key: string, a: object, b: object): number =>
|
||||
a?.[key] - b?.[key];
|
|
@ -0,0 +1,233 @@
|
|||
/**
|
||||
* 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 type { ColumnsType } from 'antd/es/table';
|
||||
import { SUPERSET_TABLE_COLUMN } from 'src/components/Table';
|
||||
import { withinRange } from './utils';
|
||||
|
||||
interface IInteractiveColumn extends HTMLElement {
|
||||
mouseDown: boolean;
|
||||
oldX: number;
|
||||
oldWidth: number;
|
||||
draggable: boolean;
|
||||
}
|
||||
export default class InteractiveTableUtils {
|
||||
tableRef: HTMLTableElement | null;
|
||||
|
||||
columnRef: IInteractiveColumn | null;
|
||||
|
||||
setDerivedColumns: Function;
|
||||
|
||||
isDragging: boolean;
|
||||
|
||||
resizable: boolean;
|
||||
|
||||
reorderable: boolean;
|
||||
|
||||
derivedColumns: ColumnsType<any>;
|
||||
|
||||
RESIZE_INDICATOR_THRESHOLD: number;
|
||||
|
||||
constructor(
|
||||
tableRef: HTMLTableElement,
|
||||
derivedColumns: ColumnsType<any>,
|
||||
setDerivedColumns: Function,
|
||||
) {
|
||||
this.setDerivedColumns = setDerivedColumns;
|
||||
this.tableRef = tableRef;
|
||||
this.isDragging = false;
|
||||
this.RESIZE_INDICATOR_THRESHOLD = 8;
|
||||
this.resizable = false;
|
||||
this.reorderable = false;
|
||||
this.derivedColumns = [...derivedColumns];
|
||||
document.addEventListener('mouseup', this.handleMouseup);
|
||||
}
|
||||
|
||||
clearListeners = () => {
|
||||
document.removeEventListener('mouseup', this.handleMouseup);
|
||||
this.initializeResizableColumns(false, this.tableRef);
|
||||
this.initializeDragDropColumns(false, this.tableRef);
|
||||
};
|
||||
|
||||
setTableRef = (table: HTMLTableElement) => {
|
||||
this.tableRef = table;
|
||||
};
|
||||
|
||||
getColumnIndex = (): number => {
|
||||
let index = -1;
|
||||
const parent = this.columnRef?.parentNode;
|
||||
if (parent) {
|
||||
index = Array.prototype.indexOf.call(parent.children, this.columnRef);
|
||||
}
|
||||
return index;
|
||||
};
|
||||
|
||||
handleColumnDragStart = (ev: DragEvent): void => {
|
||||
const target = ev?.currentTarget as IInteractiveColumn;
|
||||
if (target) {
|
||||
this.columnRef = target;
|
||||
}
|
||||
this.isDragging = true;
|
||||
const index = this.getColumnIndex();
|
||||
const columnData = this.derivedColumns[index];
|
||||
const dragData = { index, columnData };
|
||||
ev?.dataTransfer?.setData(SUPERSET_TABLE_COLUMN, JSON.stringify(dragData));
|
||||
};
|
||||
|
||||
handleDragDrop = (ev: DragEvent): void => {
|
||||
const data = ev.dataTransfer?.getData?.(SUPERSET_TABLE_COLUMN);
|
||||
if (data) {
|
||||
ev.preventDefault();
|
||||
const parent = (ev.currentTarget as HTMLElement)
|
||||
?.parentNode as HTMLElement;
|
||||
const dropIndex = Array.prototype.indexOf.call(
|
||||
parent.children,
|
||||
ev.currentTarget,
|
||||
);
|
||||
const dragIndex = this.getColumnIndex();
|
||||
const columnsCopy = [...this.derivedColumns];
|
||||
const removedItem = columnsCopy.slice(dragIndex, dragIndex + 1);
|
||||
columnsCopy.splice(dragIndex, 1);
|
||||
columnsCopy.splice(dropIndex, 0, removedItem[0]);
|
||||
this.derivedColumns = [...columnsCopy];
|
||||
this.setDerivedColumns(columnsCopy);
|
||||
}
|
||||
};
|
||||
|
||||
allowDrop = (ev: DragEvent): void => {
|
||||
ev.preventDefault();
|
||||
};
|
||||
|
||||
handleMouseDown = (event: MouseEvent) => {
|
||||
const target = event?.currentTarget as IInteractiveColumn;
|
||||
if (target) {
|
||||
this.columnRef = target;
|
||||
if (
|
||||
event &&
|
||||
withinRange(
|
||||
event.offsetX,
|
||||
target.offsetWidth,
|
||||
this.RESIZE_INDICATOR_THRESHOLD,
|
||||
)
|
||||
) {
|
||||
target.mouseDown = true;
|
||||
target.oldX = event.x;
|
||||
target.oldWidth = target.offsetWidth;
|
||||
target.draggable = false;
|
||||
} else if (this.reorderable) {
|
||||
target.draggable = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleMouseMove = (event: MouseEvent) => {
|
||||
if (this.resizable === true && !this.isDragging) {
|
||||
const target = event.currentTarget as IInteractiveColumn;
|
||||
if (
|
||||
event &&
|
||||
withinRange(
|
||||
event.offsetX,
|
||||
target.offsetWidth,
|
||||
this.RESIZE_INDICATOR_THRESHOLD,
|
||||
)
|
||||
) {
|
||||
target.style.cursor = 'col-resize';
|
||||
} else {
|
||||
target.style.cursor = 'default';
|
||||
}
|
||||
|
||||
const column = this.columnRef;
|
||||
if (column?.mouseDown) {
|
||||
let width = column.oldWidth;
|
||||
const diff = event.x - column.oldX;
|
||||
if (column.oldWidth + (event.x - column.oldX) > 0) {
|
||||
width = column.oldWidth + diff;
|
||||
}
|
||||
const colIndex = this.getColumnIndex();
|
||||
if (!Number.isNaN(colIndex)) {
|
||||
const columnDef = { ...this.derivedColumns[colIndex] };
|
||||
columnDef.width = width;
|
||||
this.derivedColumns[colIndex] = columnDef;
|
||||
this.setDerivedColumns([...this.derivedColumns]);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleMouseup = () => {
|
||||
if (this.columnRef) {
|
||||
this.columnRef.mouseDown = false;
|
||||
this.columnRef.style.cursor = 'default';
|
||||
this.columnRef.draggable = false;
|
||||
}
|
||||
this.isDragging = false;
|
||||
};
|
||||
|
||||
initializeResizableColumns = (
|
||||
resizable = false,
|
||||
table: HTMLTableElement | null,
|
||||
) => {
|
||||
this.tableRef = table;
|
||||
const header: HTMLTableRowElement | undefined = this.tableRef?.rows?.[0];
|
||||
if (header) {
|
||||
const { cells } = header;
|
||||
const len = cells.length;
|
||||
for (let i = 0; i < len; i += 1) {
|
||||
const cell = cells[i];
|
||||
if (resizable === true) {
|
||||
this.resizable = true;
|
||||
cell.addEventListener('mousedown', this.handleMouseDown);
|
||||
cell.addEventListener('mousemove', this.handleMouseMove, true);
|
||||
} else {
|
||||
this.resizable = false;
|
||||
cell.removeEventListener('mousedown', this.handleMouseDown);
|
||||
cell.removeEventListener('mousemove', this.handleMouseMove, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
initializeDragDropColumns = (
|
||||
reorderable = false,
|
||||
table: HTMLTableElement | null,
|
||||
) => {
|
||||
this.tableRef = table;
|
||||
const header: HTMLTableRowElement | undefined = this.tableRef?.rows?.[0];
|
||||
if (header) {
|
||||
const { cells } = header;
|
||||
const len = cells.length;
|
||||
for (let i = 0; i < len; i += 1) {
|
||||
const cell = cells[i];
|
||||
if (reorderable === true) {
|
||||
this.reorderable = true;
|
||||
cell.addEventListener('mousedown', this.handleMouseDown);
|
||||
cell.addEventListener('dragover', this.allowDrop);
|
||||
cell.addEventListener('dragstart', this.handleColumnDragStart);
|
||||
cell.addEventListener('drop', this.handleDragDrop);
|
||||
} else {
|
||||
this.reorderable = false;
|
||||
cell.draggable = false;
|
||||
cell.removeEventListener('mousedown', this.handleMouseDown);
|
||||
cell.removeEventListener('dragover', this.allowDrop);
|
||||
cell.removeEventListener('dragstart', this.handleColumnDragStart);
|
||||
cell.removeEventListener('drop', this.handleDragDrop);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/**
|
||||
* 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 { withinRange } from './utils';
|
||||
|
||||
test('withinRange supported positive numbers', () => {
|
||||
// Valid inputs within range
|
||||
expect(withinRange(50, 60, 16)).toBeTruthy();
|
||||
|
||||
// Valid inputs outside of range
|
||||
expect(withinRange(40, 60, 16)).toBeFalsy();
|
||||
});
|
||||
|
||||
test('withinRange unsupported negative numbers', () => {
|
||||
// Negative numbers not supported
|
||||
expect(withinRange(65, 60, -16)).toBeFalsy();
|
||||
expect(withinRange(-60, -65, 16)).toBeFalsy();
|
||||
expect(withinRange(-60, -65, 16)).toBeFalsy();
|
||||
expect(withinRange(-60, 65, 16)).toBeFalsy();
|
||||
});
|
||||
|
||||
test('withinRange invalid inputs', () => {
|
||||
// Invalid inputs should return falsy and not throw an error
|
||||
// We need ts-ignore here to be able to pass invalid values and pass linting
|
||||
// @ts-ignore
|
||||
expect(withinRange(null, 60, undefined)).toBeFalsy();
|
||||
// @ts-ignore
|
||||
expect(withinRange([], 'hello', {})).toBeFalsy();
|
||||
// @ts-ignore
|
||||
expect(withinRange([], undefined, {})).toBeFalsy();
|
||||
// @ts-ignore
|
||||
expect(withinRange([], 'hello', {})).toBeFalsy();
|
||||
});
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Method to check if a number is within inclusive range between a maximum value minus a threshold
|
||||
* Invalid non numeric inputs will not error, but will return false
|
||||
*
|
||||
* @param value number coordinate to determine if it is within bounds of the targetCoordinate - threshold. Must be positive and less than maximum.
|
||||
* @param maximum number max value for the test range. Must be positive and greater than value
|
||||
* @param threshold number values to determine a range from maximum - threshold. Must be positive and greater than zero.
|
||||
* @returns boolean
|
||||
*/
|
||||
export const withinRange = (
|
||||
value: number,
|
||||
maximum: number,
|
||||
threshold: number,
|
||||
): boolean => {
|
||||
let within = false;
|
||||
const diff = maximum - value;
|
||||
if (diff > 0 && diff <= threshold) {
|
||||
within = true;
|
||||
}
|
||||
return within;
|
||||
};
|
Binary file not shown.
After Width: | Height: | Size: 159 KiB |
|
@ -18,3 +18,4 @@
|
|||
*/
|
||||
|
||||
declare module '*.svg';
|
||||
declare module '*.gif';
|
||||
|
|
|
@ -447,7 +447,7 @@ const config = {
|
|||
type: 'asset/resource',
|
||||
},
|
||||
{
|
||||
test: /\.(stories|story)\.mdx$/,
|
||||
test: /\.mdx$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'babel-loader',
|
||||
|
|
Loading…
Reference in New Issue