airbnb/caravel

View on GitHub
superset-frontend/src/components/Table/Table.stories.tsx

Summary

Maintainability
F
3 days
Test Coverage
/**
 * 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 { useState, DragEvent } from 'react';

import type { Meta, StoryFn } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import {
  ColumnsType,
  ETableAction,
  OnChangeFunction,
  SUPERSET_TABLE_COLUMN,
  Table,
  TableSize,
} from './index';
import { alphabeticalSort, numericalSort } 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';
import HeaderWithRadioGroup from './header-renderers/HeaderWithRadioGroup';
import TimeCell from './cell-renderers/TimeCell';

export default {
  title: 'Design System/Components/Table/Examples',
  component: Table,
  argTypes: { onClick: { action: 'clicked' } },
} as Meta<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, row = 0): object {
  const cells = {};
  for (let i = 0; i < amount; i += 1) {
    cells[`col-${i}`] = i * row * 0.75;
  }
  return cells;
}

function generateColumns(amount: number): ColumnsType<ExampleData>[] {
  const newCols: any[] = [];
  for (let i = 0; i < amount; i += 1) {
    newCols.push({
      title: `C${i}`,
      dataIndex: `col-${i}`,
      key: `col-${i}`,
      width: 90,
      render: (value: number) => (
        <NumericCell
          options={{ style: Style.Currency, currency: CurrencyCode.EUR }}
          value={value}
          locale={LocaleCode.en_US}
        />
      ),
      sorter: (a: BasicData, b: BasicData) => numericalSort(`col-${i}`, a, b),
    });
  }
  return newCols as ColumnsType<ExampleData>[];
}
const recordCount = 500;
const columnCount = 500;
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: 'Harddrive',
    price: 49.99,
    description: 'Reliable and fast data storage',
  },
  {
    key: 4,
    name: '128 GB SSD',
    category: 'Harddrive',
    price: 49.99,
    description: 'Reliable and fast data storage',
  },
  {
    key: 5,
    name: '4GB 144mhz',
    category: 'Memory',
    price: 19.99,
    description: 'Laptop memory',
  },
  {
    key: 6,
    name: '1GB USB Flash Drive',
    category: 'Portable Storage',
    price: 9.99,
    description: 'USB Flash Drive portal data storage',
  },
  {
    key: 7,
    name: '256 GB SSD',
    category: 'Harddrive',
    price: 175,
    description: 'Reliable and fast data storage',
  },
  {
    key: 8,
    name: '1 TB SSD',
    category: 'Harddrive',
    price: 349.99,
    description: 'Reliable and fast data storage',
  },
];

const basicColumns: ColumnsType<BasicData> = [
  {
    title: 'Name',
    dataIndex: 'name',
    key: 'name',
    width: 100,
    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),
    width: 100,
  },
  {
    title: 'Description',
    dataIndex: 'description',
    key: 'description',
  },
];

const bigColumns: ColumnsType<ExampleData> = [
  {
    title: 'Name',
    dataIndex: 'name',
    key: 'name',
    width: 150,
  },
  {
    title: 'Age',
    dataIndex: 'age',
    key: 'age',
    sorter: (a: ExampleData, b: ExampleData) => numericalSort('age', a, b),
    width: 75,
  },
  {
    title: 'Address',
    dataIndex: 'address',
    key: 'address',
    width: 100,
  },
  ...(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, i),
  });
}

export const Basic: StoryFn<typeof Table> = args => <Table {...args} />;

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,
  usePagination: false,
};

export const Pagination: StoryFn<typeof Table> = args => <Table {...args} />;

Pagination.args = {
  data: basicData,
  columns: basicColumns,
  size: TableSize.Small,
  pageSizeOptions: ['5', '10', '15', '20', '25'],
  defaultPageSize: 5,
};

const generateData = (startIndex: number, pageSize: number): BasicData[] => {
  const data: BasicData[] = [];
  for (let i = 0; i < pageSize; i += 1) {
    const recordIndex = startIndex + i;
    data.push({
      key: recordIndex,
      name: `Dynamic Record ${recordIndex}`,
      category: 'Disk Storage',
      price: recordIndex * 2.59,
      description: 'A random description',
    });
  }
  return data;
};

const paginationColumns: ColumnsType<BasicData> = [
  {
    title: 'Name',
    dataIndex: 'name',
    key: 'name',
    width: 100,
  },
  {
    title: 'Category',
    dataIndex: 'category',
    key: 'category',
  },
  {
    title: 'Price',
    dataIndex: 'price',
    key: 'price',
    width: 100,
    render: (value: number) => (
      <NumericCell
        options={{ style: Style.Currency, currency: CurrencyCode.EUR }}
        value={value}
        locale={LocaleCode.en_US}
      />
    ),
    sorter: (a: BasicData, b: BasicData) => numericalSort('price', a, b),
  },
  {
    title: 'Description',
    dataIndex: 'description',
    key: 'description',
  },
  {
    dataIndex: 'actions',
    key: 'actions',
    render: (text: string, row: object) => (
      <ActionCell row={row} menuOptions={exampleMenuOptions} />
    ),
    width: 32,
    fixed: 'right',
  },
];

export const ServerPagination: StoryFn<typeof Table> = args => {
  const [data, setData] = useState(generateData(0, 5));
  const [loading, setLoading] = useState(false);

  const handleChange: OnChangeFunction<BasicData> = (
    pagination,
    filters,
    sorter,
    extra,
  ) => {
    const pageSize = pagination?.pageSize ?? 5;
    const current = pagination?.current ?? 0;
    switch (extra?.action) {
      case ETableAction.Paginate: {
        setLoading(true);
        // simulate a fetch
        setTimeout(() => {
          setData(generateData(current * pageSize, pageSize));
          setLoading(false);
        }, 1000);
        break;
      }
      case ETableAction.Sort: {
        action(`table-sort-change: ${JSON.stringify(sorter)}`);
        break;
      }
      case ETableAction.Filter: {
        action(`table-sort-change: ${JSON.stringify(filters)}`);
        break;
      }
      default: {
        action('table action unknown');
        break;
      }
    }
  };

  return (
    <Table
      {...args}
      data={data}
      recordCount={5000}
      onChange={handleChange}
      loading={loading}
    />
  );
};

ServerPagination.args = {
  columns: paginationColumns,
  size: TableSize.Small,
  pageSizeOptions: ['5', '20', '50'],
  defaultPageSize: 5,
};

export const VirtualizedPerformance: StoryFn<typeof Table> = args => (
  <Table {...args} />
);

VirtualizedPerformance.args = {
  data: bigdata,
  columns: bigColumns,
  size: TableSize.Small,
  resizable: true,
  reorderable: true,
  height: 350,
  virtualize: true,
  usePagination: false,
};

export const Loading: StoryFn<typeof Table> = args => <Table {...args} />;

Loading.args = {
  data: basicData,
  columns: basicColumns,
  size: TableSize.Small,
  loading: true,
};

export const ResizableColumns: StoryFn<typeof Table> = args => (
  <Table {...args} />
);

ResizableColumns.args = {
  data: basicData,
  columns: basicColumns,
  size: TableSize.Small,
  resizable: true,
};

export const ReorderableColumns: StoryFn<typeof Table> = args => {
  const [droppedItem, setDroppedItem] = useState<string | undefined>();
  const dragOver = (ev: DragEvent<HTMLDivElement>) => {
    ev.preventDefault();
    const element: HTMLElement | null = ev?.currentTarget as HTMLElement;
    if (element?.style) {
      element.style.border = '1px dashed green';
    }
  };

  const dragOut = (ev: DragEvent<HTMLDivElement>) => {
    ev.preventDefault();
    const element: HTMLElement | null = ev?.currentTarget as HTMLElement;
    if (element?.style) {
      element.style.border = '1px solid grey';
    }
  };

  const dragDrop = (ev: 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 (
    <div>
      <div
        onDragOver={(ev: DragEvent<HTMLDivElement>) => dragOver(ev)}
        onDragLeave={(ev: DragEvent<HTMLDivElement>) => dragOut(ev)}
        onDrop={(ev: 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>
  );
};

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: StoryFn<typeof Table> = args => <Table {...args} />;

CellRenderers.args = {
  data: rendererData,
  columns: rendererColumns,
  size: TableSize.Small,
  reorderable: true,
};

export interface ShoppingData {
  key: number;
  item: string;
  orderDate: number;
  price: number;
}

const shoppingData: ShoppingData[] = [
  {
    key: 1,
    item: 'Floppy Disk 10 pack',
    orderDate: new Date('2015-07-02T16:16:00Z').getTime(),
    price: 9.99,
  },
  {
    key: 2,
    item: 'DVD 100 pack',
    orderDate: new Date('2015-07-02T16:16:00Z').getTime(),
    price: 7.99,
  },
  {
    key: 3,
    item: '128 GB SSD',
    orderDate: new Date('2015-07-02T16:16:00Z').getTime(),
    price: 3.99,
  },
];

export const HeaderRenderers: StoryFn<typeof Table> = () => {
  const [orderDateFormatting, setOrderDateFormatting] = useState('formatted');
  const [priceLocale, setPriceLocale] = useState(LocaleCode.en_US);
  const shoppingColumns: ColumnsType<ShoppingData> = [
    {
      title: 'Item',
      dataIndex: 'item',
      key: 'item',
      width: 200,
    },
    {
      title: () => (
        <HeaderWithRadioGroup
          headerTitle="Order date"
          groupTitle="Formatting"
          groupOptions={[
            { label: 'Original value', value: 'original' },
            { label: 'Formatted value', value: 'formatted' },
          ]}
          value={orderDateFormatting}
          onChange={value => setOrderDateFormatting(value)}
        />
      ),
      dataIndex: 'orderDate',
      key: 'orderDate',
      width: 200,
      render: value =>
        orderDateFormatting === 'original' ? value : <TimeCell value={value} />,
    },
    {
      title: () => (
        <HeaderWithRadioGroup
          headerTitle="Price"
          groupTitle="Currency"
          groupOptions={[
            { label: 'US Dollar', value: LocaleCode.en_US },
            { label: 'Brazilian Real', value: LocaleCode.pt_BR },
          ]}
          value={priceLocale}
          onChange={value => setPriceLocale(value as LocaleCode)}
        />
      ),
      dataIndex: 'price',
      key: 'price',
      width: 200,
      render: value => (
        <NumericCell
          value={value}
          options={{
            style: Style.Currency,
            currency:
              priceLocale === LocaleCode.en_US
                ? CurrencyCode.USD
                : CurrencyCode.BRL,
          }}
          locale={priceLocale}
        />
      ),
    },
  ];

  return (
    <Table<ShoppingData>
      data={shoppingData}
      columns={shoppingColumns}
      size={TableSize.Small}
      resizable
    />
  );
};