mirror of
https://github.com/apache/superset.git
synced 2026-04-19 16:14:52 +00:00
refactor(monorepo): move superset-ui to superset(stage 2) (#17552)
This commit is contained in:
@@ -0,0 +1,351 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, {
|
||||
useCallback,
|
||||
useRef,
|
||||
ReactNode,
|
||||
HTMLProps,
|
||||
MutableRefObject,
|
||||
CSSProperties,
|
||||
} from 'react';
|
||||
import {
|
||||
useTable,
|
||||
usePagination,
|
||||
useSortBy,
|
||||
useGlobalFilter,
|
||||
PluginHook,
|
||||
TableOptions,
|
||||
FilterType,
|
||||
IdType,
|
||||
Row,
|
||||
} from 'react-table';
|
||||
import { matchSorter, rankings } from 'match-sorter';
|
||||
import GlobalFilter, { GlobalFilterProps } from './components/GlobalFilter';
|
||||
import SelectPageSize, {
|
||||
SelectPageSizeProps,
|
||||
SizeOption,
|
||||
} from './components/SelectPageSize';
|
||||
import SimplePagination from './components/Pagination';
|
||||
import useSticky from './hooks/useSticky';
|
||||
import { PAGE_SIZE_OPTIONS } from '../consts';
|
||||
|
||||
export interface DataTableProps<D extends object> extends TableOptions<D> {
|
||||
tableClassName?: string;
|
||||
searchInput?: boolean | GlobalFilterProps<D>['searchInput'];
|
||||
selectPageSize?: boolean | SelectPageSizeProps['selectRenderer'];
|
||||
pageSizeOptions?: SizeOption[]; // available page size options
|
||||
maxPageItemCount?: number;
|
||||
hooks?: PluginHook<D>[]; // any additional hooks
|
||||
width?: string | number;
|
||||
height?: string | number;
|
||||
serverPagination?: boolean;
|
||||
onServerPaginationChange: (pageNumber: number, pageSize: number) => void;
|
||||
serverPaginationData: { pageSize?: number; currentPage?: number };
|
||||
pageSize?: number;
|
||||
noResults?: string | ((filterString: string) => ReactNode);
|
||||
sticky?: boolean;
|
||||
rowCount: number;
|
||||
wrapperRef?: MutableRefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
export interface RenderHTMLCellProps extends HTMLProps<HTMLTableCellElement> {
|
||||
cellContent: ReactNode;
|
||||
}
|
||||
|
||||
// Be sure to pass our updateMyData and the skipReset option
|
||||
export default function DataTable<D extends object>({
|
||||
tableClassName,
|
||||
columns,
|
||||
data,
|
||||
serverPaginationData,
|
||||
width: initialWidth = '100%',
|
||||
height: initialHeight = 300,
|
||||
pageSize: initialPageSize = 0,
|
||||
initialState: initialState_ = {},
|
||||
pageSizeOptions = PAGE_SIZE_OPTIONS,
|
||||
maxPageItemCount = 9,
|
||||
sticky: doSticky,
|
||||
searchInput = true,
|
||||
onServerPaginationChange,
|
||||
rowCount,
|
||||
selectPageSize,
|
||||
noResults: noResultsText = 'No data found',
|
||||
hooks,
|
||||
serverPagination,
|
||||
wrapperRef: userWrapperRef,
|
||||
...moreUseTableOptions
|
||||
}: DataTableProps<D>): JSX.Element {
|
||||
const tableHooks: PluginHook<D>[] = [
|
||||
useGlobalFilter,
|
||||
useSortBy,
|
||||
usePagination,
|
||||
doSticky ? useSticky : [],
|
||||
hooks || [],
|
||||
].flat();
|
||||
const resultsSize = serverPagination ? rowCount : data.length;
|
||||
const sortByRef = useRef([]); // cache initial `sortby` so sorting doesn't trigger page reset
|
||||
const pageSizeRef = useRef([initialPageSize, resultsSize]);
|
||||
const hasPagination = initialPageSize > 0 && resultsSize > 0; // pageSize == 0 means no pagination
|
||||
const hasGlobalControl = hasPagination || !!searchInput;
|
||||
const initialState = {
|
||||
...initialState_,
|
||||
// zero length means all pages, the `usePagination` plugin does not
|
||||
// understand pageSize = 0
|
||||
sortBy: sortByRef.current,
|
||||
pageSize: initialPageSize > 0 ? initialPageSize : resultsSize || 10,
|
||||
};
|
||||
const defaultWrapperRef = useRef<HTMLDivElement>(null);
|
||||
const globalControlRef = useRef<HTMLDivElement>(null);
|
||||
const paginationRef = useRef<HTMLDivElement>(null);
|
||||
const wrapperRef = userWrapperRef || defaultWrapperRef;
|
||||
const paginationData = JSON.stringify(serverPaginationData);
|
||||
|
||||
const defaultGetTableSize = useCallback(() => {
|
||||
if (wrapperRef.current) {
|
||||
// `initialWidth` and `initialHeight` could be also parameters like `100%`
|
||||
// `Number` reaturns `NaN` on them, then we fallback to computed size
|
||||
const width = Number(initialWidth) || wrapperRef.current.clientWidth;
|
||||
const height =
|
||||
(Number(initialHeight) || wrapperRef.current.clientHeight) -
|
||||
(globalControlRef.current?.clientHeight || 0) -
|
||||
(paginationRef.current?.clientHeight || 0);
|
||||
return { width, height };
|
||||
}
|
||||
return undefined;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
initialHeight,
|
||||
initialWidth,
|
||||
wrapperRef,
|
||||
hasPagination,
|
||||
hasGlobalControl,
|
||||
paginationRef,
|
||||
resultsSize,
|
||||
paginationData,
|
||||
]);
|
||||
|
||||
const defaultGlobalFilter: FilterType<D> = useCallback(
|
||||
(rows: Row<D>[], columnIds: IdType<D>[], filterValue: string) => {
|
||||
// allow searching by "col1_value col2_value"
|
||||
const joinedString = (row: Row<D>) =>
|
||||
columnIds.map(x => row.values[x]).join(' ');
|
||||
return matchSorter(rows, filterValue, {
|
||||
keys: [...columnIds, joinedString],
|
||||
threshold: rankings.ACRONYM,
|
||||
}) as typeof rows;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const {
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
prepareRow,
|
||||
headerGroups,
|
||||
footerGroups,
|
||||
page,
|
||||
pageCount,
|
||||
gotoPage,
|
||||
preGlobalFilteredRows,
|
||||
setGlobalFilter,
|
||||
setPageSize: setPageSize_,
|
||||
wrapStickyTable,
|
||||
state: { pageIndex, pageSize, globalFilter: filterValue, sticky = {} },
|
||||
} = useTable<D>(
|
||||
{
|
||||
columns,
|
||||
data,
|
||||
initialState,
|
||||
getTableSize: defaultGetTableSize,
|
||||
globalFilter: defaultGlobalFilter,
|
||||
...moreUseTableOptions,
|
||||
},
|
||||
...tableHooks,
|
||||
);
|
||||
// make setPageSize accept 0
|
||||
const setPageSize = (size: number) => {
|
||||
if (serverPagination) {
|
||||
onServerPaginationChange(0, size);
|
||||
}
|
||||
// keep the original size if data is empty
|
||||
if (size || resultsSize !== 0) {
|
||||
setPageSize_(size === 0 ? resultsSize : size);
|
||||
}
|
||||
};
|
||||
|
||||
const noResults =
|
||||
typeof noResultsText === 'function'
|
||||
? noResultsText(filterValue as string)
|
||||
: noResultsText;
|
||||
|
||||
const getNoResults = () => <div className="dt-no-results">{noResults}</div>;
|
||||
|
||||
if (!columns || columns.length === 0) {
|
||||
return (
|
||||
wrapStickyTable ? wrapStickyTable(getNoResults) : getNoResults()
|
||||
) as JSX.Element;
|
||||
}
|
||||
|
||||
const shouldRenderFooter = columns.some(x => !!x.Footer);
|
||||
|
||||
const renderTable = () => (
|
||||
<table {...getTableProps({ className: tableClassName })}>
|
||||
<thead>
|
||||
{headerGroups.map(headerGroup => {
|
||||
const { key: headerGroupKey, ...headerGroupProps } =
|
||||
headerGroup.getHeaderGroupProps();
|
||||
return (
|
||||
<tr key={headerGroupKey || headerGroup.id} {...headerGroupProps}>
|
||||
{headerGroup.headers.map(column =>
|
||||
column.render('Header', {
|
||||
key: column.id,
|
||||
...column.getSortByToggleProps(),
|
||||
}),
|
||||
)}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</thead>
|
||||
<tbody {...getTableBodyProps()}>
|
||||
{page && page.length > 0 ? (
|
||||
page.map(row => {
|
||||
prepareRow(row);
|
||||
const { key: rowKey, ...rowProps } = row.getRowProps();
|
||||
return (
|
||||
<tr key={rowKey || row.id} {...rowProps}>
|
||||
{row.cells.map(cell =>
|
||||
cell.render('Cell', { key: cell.column.id }),
|
||||
)}
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<tr>
|
||||
<td className="dt-no-results" colSpan={columns.length}>
|
||||
{noResults}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
{shouldRenderFooter && (
|
||||
<tfoot>
|
||||
{footerGroups.map(footerGroup => {
|
||||
const { key: footerGroupKey, ...footerGroupProps } =
|
||||
footerGroup.getHeaderGroupProps();
|
||||
return (
|
||||
<tr key={footerGroupKey || footerGroup.id} {...footerGroupProps}>
|
||||
{footerGroup.headers.map(column =>
|
||||
column.render('Footer', { key: column.id }),
|
||||
)}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tfoot>
|
||||
)}
|
||||
</table>
|
||||
);
|
||||
|
||||
// force update the pageSize when it's been update from the initial state
|
||||
if (
|
||||
pageSizeRef.current[0] !== initialPageSize ||
|
||||
// when initialPageSize stays as zero, but total number of records changed,
|
||||
// we'd also need to update page size
|
||||
(initialPageSize === 0 && pageSizeRef.current[1] !== resultsSize)
|
||||
) {
|
||||
pageSizeRef.current = [initialPageSize, resultsSize];
|
||||
setPageSize(initialPageSize);
|
||||
}
|
||||
|
||||
const paginationStyle: CSSProperties = sticky.height
|
||||
? {}
|
||||
: { visibility: 'hidden' };
|
||||
|
||||
let resultPageCount = pageCount;
|
||||
let resultCurrentPageSize = pageSize;
|
||||
let resultCurrentPage = pageIndex;
|
||||
let resultOnPageChange: (page: number) => void = gotoPage;
|
||||
if (serverPagination) {
|
||||
const serverPageSize = serverPaginationData.pageSize ?? initialPageSize;
|
||||
resultPageCount = Math.ceil(rowCount / serverPageSize);
|
||||
if (!Number.isFinite(resultPageCount)) {
|
||||
resultPageCount = 0;
|
||||
}
|
||||
resultCurrentPageSize = serverPageSize;
|
||||
const foundPageSizeIndex = pageSizeOptions.findIndex(
|
||||
([option]) => option >= resultCurrentPageSize,
|
||||
);
|
||||
if (foundPageSizeIndex === -1) {
|
||||
resultCurrentPageSize = 0;
|
||||
}
|
||||
resultCurrentPage = serverPaginationData.currentPage ?? 0;
|
||||
resultOnPageChange = (pageNumber: number) =>
|
||||
onServerPaginationChange(pageNumber, serverPageSize);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
ref={wrapperRef}
|
||||
style={{ width: initialWidth, height: initialHeight }}
|
||||
>
|
||||
{hasGlobalControl ? (
|
||||
<div ref={globalControlRef} className="form-inline dt-controls">
|
||||
<div className="row">
|
||||
<div className="col-sm-6">
|
||||
{hasPagination ? (
|
||||
<SelectPageSize
|
||||
total={resultsSize}
|
||||
current={resultCurrentPageSize}
|
||||
options={pageSizeOptions}
|
||||
selectRenderer={
|
||||
typeof selectPageSize === 'boolean'
|
||||
? undefined
|
||||
: selectPageSize
|
||||
}
|
||||
onChange={setPageSize}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{searchInput ? (
|
||||
<div className="col-sm-6">
|
||||
<GlobalFilter<D>
|
||||
searchInput={
|
||||
typeof searchInput === 'boolean' ? undefined : searchInput
|
||||
}
|
||||
preGlobalFilteredRows={preGlobalFilteredRows}
|
||||
setGlobalFilter={setGlobalFilter}
|
||||
filterValue={filterValue}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{wrapStickyTable ? wrapStickyTable(renderTable) : renderTable()}
|
||||
{hasPagination && resultPageCount > 1 ? (
|
||||
<SimplePagination
|
||||
ref={paginationRef}
|
||||
style={paginationStyle}
|
||||
maxPageItemCount={maxPageItemCount}
|
||||
pageCount={resultPageCount}
|
||||
currentPage={resultCurrentPage}
|
||||
onPageChange={resultOnPageChange}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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.
|
||||
-->
|
||||
|
||||
# Superset UI Data Table
|
||||
|
||||
Reusable data table based on [react-table](https://github.com/tannerlinsley/react-table), with
|
||||
built-in support for sorting, filtering, and pagination.
|
||||
|
||||
Intended to be used as a standalone UI component in the future.
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* 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, { ComponentType, ChangeEventHandler } from 'react';
|
||||
import { Row, FilterValue } from 'react-table';
|
||||
import useAsyncState from '../utils/useAsyncState';
|
||||
|
||||
export interface SearchInputProps {
|
||||
count: number;
|
||||
value: string;
|
||||
onChange: ChangeEventHandler<HTMLInputElement>;
|
||||
}
|
||||
|
||||
export interface GlobalFilterProps<D extends object> {
|
||||
preGlobalFilteredRows: Row<D>[];
|
||||
// filter value cannot be `undefined` otherwise React will report component
|
||||
// control type undefined error
|
||||
filterValue: string;
|
||||
setGlobalFilter: (filterValue: FilterValue) => void;
|
||||
searchInput?: ComponentType<SearchInputProps>;
|
||||
}
|
||||
|
||||
function DefaultSearchInput({ count, value, onChange }: SearchInputProps) {
|
||||
return (
|
||||
<span className="dt-global-filter">
|
||||
Search{' '}
|
||||
<input
|
||||
className="form-control input-sm"
|
||||
placeholder={`${count} records...`}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default (React.memo as <T>(fn: T) => T)(function GlobalFilter<
|
||||
D extends object,
|
||||
>({
|
||||
preGlobalFilteredRows,
|
||||
filterValue = '',
|
||||
searchInput,
|
||||
setGlobalFilter,
|
||||
}: GlobalFilterProps<D>) {
|
||||
const count = preGlobalFilteredRows.length;
|
||||
const [value, setValue] = useAsyncState(
|
||||
filterValue,
|
||||
(newValue: string) => {
|
||||
setGlobalFilter(newValue || undefined);
|
||||
},
|
||||
200,
|
||||
);
|
||||
|
||||
const SearchInput = searchInput || DefaultSearchInput;
|
||||
|
||||
return (
|
||||
<SearchInput
|
||||
count={count}
|
||||
value={value}
|
||||
onChange={e => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
e.preventDefault();
|
||||
setValue(target.value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* 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, { CSSProperties } from 'react';
|
||||
|
||||
export interface PaginationProps {
|
||||
pageCount: number; // number of pages
|
||||
currentPage?: number; // index of current page, zero-based
|
||||
maxPageItemCount?: number;
|
||||
ellipsis?: string; // content for ellipsis item
|
||||
onPageChange: (page: number) => void; // `page` is zero-based
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
// first, ..., prev, current, next, ..., last
|
||||
const MINIMAL_PAGE_ITEM_COUNT = 7;
|
||||
|
||||
/**
|
||||
* Generate numeric page items around current page.
|
||||
* - Always include first and last page
|
||||
* - Add ellipsis if needed
|
||||
*/
|
||||
export function generatePageItems(
|
||||
total: number,
|
||||
current: number,
|
||||
width: number,
|
||||
) {
|
||||
if (width < MINIMAL_PAGE_ITEM_COUNT) {
|
||||
throw new Error(
|
||||
`Must allow at least ${MINIMAL_PAGE_ITEM_COUNT} page items`,
|
||||
);
|
||||
}
|
||||
if (width % 2 === 0) {
|
||||
throw new Error(`Must allow odd number of page items`);
|
||||
}
|
||||
if (total < width) {
|
||||
return [...new Array(total).keys()];
|
||||
}
|
||||
const left = Math.max(
|
||||
0,
|
||||
Math.min(total - width, current - Math.floor(width / 2)),
|
||||
);
|
||||
const items: (string | number)[] = new Array(width);
|
||||
for (let i = 0; i < width; i += 1) {
|
||||
items[i] = i + left;
|
||||
}
|
||||
// replace non-ending items with placeholders
|
||||
if (items[0] > 0) {
|
||||
items[0] = 0;
|
||||
items[1] = 'prev-more';
|
||||
}
|
||||
if (items[items.length - 1] < total - 1) {
|
||||
items[items.length - 1] = total - 1;
|
||||
items[items.length - 2] = 'next-more';
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
export default React.memo(
|
||||
React.forwardRef(function Pagination(
|
||||
{
|
||||
style,
|
||||
pageCount,
|
||||
currentPage = 0,
|
||||
maxPageItemCount = 9,
|
||||
onPageChange,
|
||||
}: PaginationProps,
|
||||
ref: React.Ref<HTMLDivElement>,
|
||||
) {
|
||||
const pageItems = generatePageItems(
|
||||
pageCount,
|
||||
currentPage,
|
||||
maxPageItemCount,
|
||||
);
|
||||
return (
|
||||
<div ref={ref} className="dt-pagination" style={style}>
|
||||
<ul className="pagination pagination-sm">
|
||||
{pageItems.map(item =>
|
||||
typeof item === 'number' ? (
|
||||
// actual page number
|
||||
<li
|
||||
key={item}
|
||||
className={currentPage === item ? 'active' : undefined}
|
||||
>
|
||||
<a
|
||||
href={`#page-${item}`}
|
||||
role="button"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
onPageChange(item);
|
||||
}}
|
||||
>
|
||||
{item + 1}
|
||||
</a>
|
||||
</li>
|
||||
) : (
|
||||
<li key={item} className="dt-pagination-ellipsis">
|
||||
<span>…</span>
|
||||
</li>
|
||||
),
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* 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 { formatSelectOptions } from '@superset-ui/chart-controls';
|
||||
|
||||
export type SizeOption = [number, string];
|
||||
|
||||
export interface SelectPageSizeRendererProps {
|
||||
current: number;
|
||||
options: SizeOption[];
|
||||
onChange: SelectPageSizeProps['onChange'];
|
||||
}
|
||||
|
||||
function DefaultSelectRenderer({
|
||||
current,
|
||||
options,
|
||||
onChange,
|
||||
}: SelectPageSizeRendererProps) {
|
||||
return (
|
||||
<span className="dt-select-page-size form-inline">
|
||||
Show{' '}
|
||||
<select
|
||||
className="form-control input-sm"
|
||||
value={current}
|
||||
onBlur={() => {}}
|
||||
onChange={e => {
|
||||
onChange(Number((e.target as HTMLSelectElement).value));
|
||||
}}
|
||||
>
|
||||
{options.map(option => {
|
||||
const [size, text] = Array.isArray(option)
|
||||
? option
|
||||
: [option, option];
|
||||
return (
|
||||
<option key={size} value={size}>
|
||||
{text}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>{' '}
|
||||
entries
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export interface SelectPageSizeProps extends SelectPageSizeRendererProps {
|
||||
total?: number;
|
||||
selectRenderer?: typeof DefaultSelectRenderer;
|
||||
onChange: (pageSize: number) => void;
|
||||
}
|
||||
|
||||
function getOptionValue(x: SizeOption) {
|
||||
return Array.isArray(x) ? x[0] : x;
|
||||
}
|
||||
|
||||
export default React.memo(function SelectPageSize({
|
||||
total,
|
||||
options: sizeOptions,
|
||||
current: currentSize,
|
||||
selectRenderer,
|
||||
onChange,
|
||||
}: SelectPageSizeProps) {
|
||||
const sizeOptionValues = sizeOptions.map(getOptionValue);
|
||||
let options = [...sizeOptions];
|
||||
// insert current size to list
|
||||
if (
|
||||
currentSize !== undefined &&
|
||||
(currentSize !== total || !sizeOptionValues.includes(0)) &&
|
||||
!sizeOptionValues.includes(currentSize)
|
||||
) {
|
||||
options = [...sizeOptions];
|
||||
options.splice(
|
||||
sizeOptionValues.findIndex(x => x > currentSize),
|
||||
0,
|
||||
formatSelectOptions([currentSize])[0],
|
||||
);
|
||||
}
|
||||
const current = currentSize === undefined ? sizeOptionValues[0] : currentSize;
|
||||
const SelectRenderer = selectRenderer || DefaultSelectRenderer;
|
||||
return (
|
||||
<SelectRenderer current={current} options={options} onChange={onChange} />
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,435 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, {
|
||||
useRef,
|
||||
useMemo,
|
||||
useLayoutEffect,
|
||||
useCallback,
|
||||
ReactNode,
|
||||
ReactElement,
|
||||
ComponentPropsWithRef,
|
||||
CSSProperties,
|
||||
UIEventHandler,
|
||||
} from 'react';
|
||||
import { TableInstance, Hooks } from 'react-table';
|
||||
import getScrollBarSize from '../utils/getScrollBarSize';
|
||||
import needScrollBar from '../utils/needScrollBar';
|
||||
import useMountedMemo from '../utils/useMountedMemo';
|
||||
|
||||
type ReactElementWithChildren<
|
||||
T extends keyof JSX.IntrinsicElements,
|
||||
C extends ReactNode = ReactNode,
|
||||
> = ReactElement<ComponentPropsWithRef<T> & { children: C }, T>;
|
||||
|
||||
type Th = ReactElementWithChildren<'th'>;
|
||||
type Td = ReactElementWithChildren<'td'>;
|
||||
type TrWithTh = ReactElementWithChildren<'tr', Th[]>;
|
||||
type TrWithTd = ReactElementWithChildren<'tr', Td[]>;
|
||||
type Thead = ReactElementWithChildren<'thead', TrWithTh>;
|
||||
type Tbody = ReactElementWithChildren<'tbody', TrWithTd>;
|
||||
type Tfoot = ReactElementWithChildren<'tfoot', TrWithTd>;
|
||||
type Col = ReactElementWithChildren<'col', null>;
|
||||
type ColGroup = ReactElementWithChildren<'colgroup', Col>;
|
||||
|
||||
export type Table = ReactElementWithChildren<
|
||||
'table',
|
||||
(Thead | Tbody | Tfoot | ColGroup)[]
|
||||
>;
|
||||
export type TableRenderer = () => Table;
|
||||
export type GetTableSize = () => Partial<StickyState> | undefined;
|
||||
export type SetStickyState = (size?: Partial<StickyState>) => void;
|
||||
|
||||
export enum ReducerActions {
|
||||
init = 'init', // this is from global reducer
|
||||
setStickyState = 'setStickyState',
|
||||
}
|
||||
|
||||
export type ReducerAction<
|
||||
T extends string,
|
||||
P extends Record<string, unknown>,
|
||||
> = P & { type: T };
|
||||
|
||||
export type ColumnWidths = number[];
|
||||
|
||||
export interface StickyState {
|
||||
width?: number; // maximum full table width
|
||||
height?: number; // maximum full table height
|
||||
realHeight?: number; // actual table viewport height (header + scrollable area)
|
||||
bodyHeight?: number; // scrollable area height
|
||||
tableHeight?: number; // the full table height
|
||||
columnWidths?: ColumnWidths;
|
||||
hasHorizontalScroll?: boolean;
|
||||
hasVerticalScroll?: boolean;
|
||||
rendering?: boolean;
|
||||
setStickyState?: SetStickyState;
|
||||
}
|
||||
|
||||
export interface UseStickyTableOptions {
|
||||
getTableSize?: GetTableSize;
|
||||
}
|
||||
|
||||
export interface UseStickyInstanceProps {
|
||||
// manipulate DOMs in <table> to make the header sticky
|
||||
wrapStickyTable: (renderer: TableRenderer) => ReactNode;
|
||||
// update or recompute the sticky table size
|
||||
setStickyState: SetStickyState;
|
||||
}
|
||||
|
||||
export type UseStickyState = {
|
||||
sticky: StickyState;
|
||||
};
|
||||
|
||||
const sum = (a: number, b: number) => a + b;
|
||||
const mergeStyleProp = (
|
||||
node: ReactElement<{ style?: CSSProperties }>,
|
||||
style: CSSProperties,
|
||||
) => ({
|
||||
style: {
|
||||
...node.props.style,
|
||||
...style,
|
||||
},
|
||||
});
|
||||
const fixedTableLayout: CSSProperties = { tableLayout: 'fixed' };
|
||||
|
||||
/**
|
||||
* An HOC for generating sticky header and fixed-height scrollable area
|
||||
*/
|
||||
function StickyWrap({
|
||||
sticky = {},
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
children: table,
|
||||
setStickyState,
|
||||
}: {
|
||||
width: number;
|
||||
height: number;
|
||||
setStickyState: SetStickyState;
|
||||
children: Table;
|
||||
sticky?: StickyState; // current sticky element sizes
|
||||
}) {
|
||||
if (!table || table.type !== 'table') {
|
||||
throw new Error('<StickyWrap> must have only one <table> element as child');
|
||||
}
|
||||
let thead: Thead | undefined;
|
||||
let tbody: Tbody | undefined;
|
||||
let tfoot: Tfoot | undefined;
|
||||
|
||||
React.Children.forEach(table.props.children, node => {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
if (node.type === 'thead') {
|
||||
thead = node;
|
||||
} else if (node.type === 'tbody') {
|
||||
tbody = node;
|
||||
} else if (node.type === 'tfoot') {
|
||||
tfoot = node;
|
||||
}
|
||||
});
|
||||
if (!thead || !tbody) {
|
||||
throw new Error(
|
||||
'<table> in <StickyWrap> must contain both thead and tbody.',
|
||||
);
|
||||
}
|
||||
const columnCount = useMemo(() => {
|
||||
const headerRows = React.Children.toArray(
|
||||
thead?.props.children,
|
||||
).pop() as TrWithTh;
|
||||
return headerRows.props.children.length;
|
||||
}, [thead]);
|
||||
|
||||
const theadRef = useRef<HTMLTableSectionElement>(null); // original thead for layout computation
|
||||
const tfootRef = useRef<HTMLTableSectionElement>(null); // original tfoot for layout computation
|
||||
const scrollHeaderRef = useRef<HTMLDivElement>(null); // fixed header
|
||||
const scrollFooterRef = useRef<HTMLDivElement>(null); // fixed footer
|
||||
const scrollBodyRef = useRef<HTMLDivElement>(null); // main body
|
||||
|
||||
const scrollBarSize = getScrollBarSize();
|
||||
const { bodyHeight, columnWidths } = sticky;
|
||||
const needSizer =
|
||||
!columnWidths ||
|
||||
sticky.width !== maxWidth ||
|
||||
sticky.height !== maxHeight ||
|
||||
sticky.setStickyState !== setStickyState;
|
||||
|
||||
// update scrollable area and header column sizes when mounted
|
||||
useLayoutEffect(() => {
|
||||
if (!theadRef.current) {
|
||||
return;
|
||||
}
|
||||
const bodyThead = theadRef.current;
|
||||
const theadHeight = bodyThead.clientHeight;
|
||||
const tfootHeight = tfootRef.current ? tfootRef.current.clientHeight : 0;
|
||||
if (!theadHeight) {
|
||||
return;
|
||||
}
|
||||
const fullTableHeight = (bodyThead.parentNode as HTMLTableElement)
|
||||
.clientHeight;
|
||||
const ths = bodyThead.childNodes[0]
|
||||
.childNodes as NodeListOf<HTMLTableHeaderCellElement>;
|
||||
const widths = Array.from(ths).map(th => th.clientWidth);
|
||||
const [hasVerticalScroll, hasHorizontalScroll] = needScrollBar({
|
||||
width: maxWidth,
|
||||
height: maxHeight - theadHeight - tfootHeight,
|
||||
innerHeight: fullTableHeight,
|
||||
innerWidth: widths.reduce(sum),
|
||||
scrollBarSize,
|
||||
});
|
||||
// real container height, include table header, footer and space for
|
||||
// horizontal scroll bar
|
||||
const realHeight = Math.min(
|
||||
maxHeight,
|
||||
hasHorizontalScroll ? fullTableHeight + scrollBarSize : fullTableHeight,
|
||||
);
|
||||
setStickyState({
|
||||
hasVerticalScroll,
|
||||
hasHorizontalScroll,
|
||||
setStickyState,
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
realHeight,
|
||||
tableHeight: fullTableHeight,
|
||||
bodyHeight: realHeight - theadHeight - tfootHeight,
|
||||
columnWidths: widths,
|
||||
});
|
||||
}, [maxWidth, maxHeight, setStickyState, scrollBarSize]);
|
||||
|
||||
let sizerTable: ReactElement | undefined;
|
||||
let headerTable: ReactElement | undefined;
|
||||
let footerTable: ReactElement | undefined;
|
||||
let bodyTable: ReactElement | undefined;
|
||||
if (needSizer) {
|
||||
const theadWithRef = React.cloneElement(thead, { ref: theadRef });
|
||||
const tfootWithRef = tfoot && React.cloneElement(tfoot, { ref: tfootRef });
|
||||
sizerTable = (
|
||||
<div
|
||||
key="sizer"
|
||||
style={{
|
||||
height: maxHeight,
|
||||
overflow: 'auto',
|
||||
visibility: 'hidden',
|
||||
}}
|
||||
>
|
||||
{React.cloneElement(table, {}, theadWithRef, tbody, tfootWithRef)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// reuse previously column widths, will be updated by `useLayoutEffect` above
|
||||
const colWidths = columnWidths?.slice(0, columnCount);
|
||||
|
||||
if (colWidths && bodyHeight) {
|
||||
const bodyColgroup = (
|
||||
<colgroup>
|
||||
{colWidths.map((w, i) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<col key={i} width={w} />
|
||||
))}
|
||||
</colgroup>
|
||||
);
|
||||
|
||||
// header columns do not have vertical scroll bars,
|
||||
// so we add scroll bar size to the last column
|
||||
const headerColgroup =
|
||||
sticky.hasVerticalScroll && scrollBarSize ? (
|
||||
<colgroup>
|
||||
{colWidths.map((x, i) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<col
|
||||
key={i}
|
||||
width={x + (i === colWidths.length - 1 ? scrollBarSize : 0)}
|
||||
/>
|
||||
))}
|
||||
</colgroup>
|
||||
) : (
|
||||
bodyColgroup
|
||||
);
|
||||
|
||||
headerTable = (
|
||||
<div
|
||||
key="header"
|
||||
ref={scrollHeaderRef}
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{React.cloneElement(
|
||||
table,
|
||||
mergeStyleProp(table, fixedTableLayout),
|
||||
headerColgroup,
|
||||
thead,
|
||||
)}
|
||||
{headerTable}
|
||||
</div>
|
||||
);
|
||||
|
||||
footerTable = tfoot && (
|
||||
<div
|
||||
key="footer"
|
||||
ref={scrollFooterRef}
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{React.cloneElement(
|
||||
table,
|
||||
mergeStyleProp(table, fixedTableLayout),
|
||||
headerColgroup,
|
||||
tfoot,
|
||||
)}
|
||||
{footerTable}
|
||||
</div>
|
||||
);
|
||||
|
||||
const onScroll: UIEventHandler<HTMLDivElement> = e => {
|
||||
if (scrollHeaderRef.current) {
|
||||
scrollHeaderRef.current.scrollLeft = e.currentTarget.scrollLeft;
|
||||
}
|
||||
if (scrollFooterRef.current) {
|
||||
scrollFooterRef.current.scrollLeft = e.currentTarget.scrollLeft;
|
||||
}
|
||||
};
|
||||
bodyTable = (
|
||||
<div
|
||||
key="body"
|
||||
ref={scrollBodyRef}
|
||||
style={{
|
||||
height: bodyHeight,
|
||||
overflow: 'auto',
|
||||
}}
|
||||
onScroll={sticky.hasHorizontalScroll ? onScroll : undefined}
|
||||
>
|
||||
{React.cloneElement(
|
||||
table,
|
||||
mergeStyleProp(table, fixedTableLayout),
|
||||
bodyColgroup,
|
||||
tbody,
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: maxWidth,
|
||||
height: sticky.realHeight || maxHeight,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{headerTable}
|
||||
{bodyTable}
|
||||
{footerTable}
|
||||
{sizerTable}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useInstance<D extends object>(instance: TableInstance<D>) {
|
||||
const {
|
||||
dispatch,
|
||||
state: { sticky },
|
||||
data,
|
||||
page,
|
||||
rows,
|
||||
getTableSize = () => undefined,
|
||||
} = instance;
|
||||
|
||||
const setStickyState = useCallback(
|
||||
(size?: Partial<StickyState>) => {
|
||||
dispatch({
|
||||
type: ReducerActions.setStickyState,
|
||||
size,
|
||||
});
|
||||
},
|
||||
// turning pages would also trigger a resize
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[dispatch, getTableSize, page, rows],
|
||||
);
|
||||
|
||||
const useStickyWrap = (renderer: TableRenderer) => {
|
||||
const { width, height } =
|
||||
useMountedMemo(getTableSize, [getTableSize]) || sticky;
|
||||
// only change of data should trigger re-render
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const table = useMemo(renderer, [page, rows]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!width || !height) {
|
||||
setStickyState();
|
||||
}
|
||||
}, [width, height]);
|
||||
|
||||
if (!width || !height) {
|
||||
return null;
|
||||
}
|
||||
if (data.length === 0) {
|
||||
return table;
|
||||
}
|
||||
return (
|
||||
<StickyWrap
|
||||
width={width}
|
||||
height={height}
|
||||
sticky={sticky}
|
||||
setStickyState={setStickyState}
|
||||
>
|
||||
{table}
|
||||
</StickyWrap>
|
||||
);
|
||||
};
|
||||
|
||||
Object.assign(instance, {
|
||||
setStickyState,
|
||||
wrapStickyTable: useStickyWrap,
|
||||
});
|
||||
}
|
||||
|
||||
export default function useSticky<D extends object>(hooks: Hooks<D>) {
|
||||
hooks.useInstance.push(useInstance);
|
||||
hooks.stateReducers.push((newState, action_, prevState) => {
|
||||
const action = action_ as ReducerAction<
|
||||
ReducerActions,
|
||||
{ size: StickyState }
|
||||
>;
|
||||
if (action.type === ReducerActions.init) {
|
||||
return {
|
||||
...newState,
|
||||
sticky: {
|
||||
...prevState?.sticky,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (action.type === ReducerActions.setStickyState) {
|
||||
const { size } = action;
|
||||
if (!size) {
|
||||
return { ...newState };
|
||||
}
|
||||
return {
|
||||
...newState,
|
||||
sticky: {
|
||||
...prevState?.sticky,
|
||||
...newState?.sticky,
|
||||
...action.size,
|
||||
},
|
||||
};
|
||||
}
|
||||
return newState;
|
||||
});
|
||||
}
|
||||
useSticky.pluginName = 'useSticky';
|
||||
@@ -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 * from './hooks/useSticky';
|
||||
export * from './components/GlobalFilter';
|
||||
export * from './components/Pagination';
|
||||
export * from './components/SelectPageSize';
|
||||
export * from './DataTable';
|
||||
|
||||
export { default } from './DataTable';
|
||||
105
superset-frontend/plugins/plugin-chart-table/src/DataTable/types/react-table.d.ts
vendored
Normal file
105
superset-frontend/plugins/plugin-chart-table/src/DataTable/types/react-table.d.ts
vendored
Normal file
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Merge typing interfaces for UseTable hooks.
|
||||
*
|
||||
* Ref: https://gist.github.com/ggascoigne/646e14c9d54258e40588a13aabf0102d
|
||||
*/
|
||||
import {
|
||||
UseGlobalFiltersState,
|
||||
UseGlobalFiltersOptions,
|
||||
UseGlobalFiltersInstanceProps,
|
||||
UsePaginationInstanceProps,
|
||||
UsePaginationOptions,
|
||||
UsePaginationState,
|
||||
UseSortByColumnOptions,
|
||||
UseSortByColumnProps,
|
||||
UseSortByInstanceProps,
|
||||
UseSortByOptions,
|
||||
UseSortByState,
|
||||
UseTableHooks,
|
||||
UseSortByHooks,
|
||||
Renderer,
|
||||
HeaderProps,
|
||||
TableFooterProps,
|
||||
} from 'react-table';
|
||||
|
||||
import {
|
||||
UseStickyState,
|
||||
UseStickyTableOptions,
|
||||
UseStickyInstanceProps,
|
||||
} from '../hooks/useSticky';
|
||||
|
||||
declare module 'react-table' {
|
||||
export interface TableOptions<D extends object>
|
||||
extends UseExpandedOptions<D>,
|
||||
UseGlobalFiltersOptions<D>,
|
||||
UsePaginationOptions<D>,
|
||||
UseRowSelectOptions<D>,
|
||||
UseSortByOptions<D>,
|
||||
UseStickyTableOptions {}
|
||||
|
||||
export interface TableInstance<D extends object>
|
||||
extends UseColumnOrderInstanceProps<D>,
|
||||
UseExpandedInstanceProps<D>,
|
||||
UseGlobalFiltersInstanceProps<D>,
|
||||
UsePaginationInstanceProps<D>,
|
||||
UseRowSelectInstanceProps<D>,
|
||||
UseRowStateInstanceProps<D>,
|
||||
UseSortByInstanceProps<D>,
|
||||
UseStickyInstanceProps {}
|
||||
|
||||
export interface TableState<D extends object>
|
||||
extends UseColumnOrderState<D>,
|
||||
UseExpandedState<D>,
|
||||
UseGlobalFiltersState<D>,
|
||||
UsePaginationState<D>,
|
||||
UseRowSelectState<D>,
|
||||
UseSortByState<D>,
|
||||
UseStickyState {}
|
||||
|
||||
// Typing from @types/react-table is incomplete
|
||||
interface TableSortByToggleProps {
|
||||
style?: React.CSSProperties;
|
||||
title?: string;
|
||||
onClick?: React.MouseEventHandler;
|
||||
}
|
||||
|
||||
export interface ColumnInterface<D extends object>
|
||||
extends UseGlobalFiltersColumnOptions<D>,
|
||||
UseSortByColumnOptions<D> {
|
||||
// must define as a new property because it's not possible to override
|
||||
// the existing `Header` renderer option
|
||||
Header?: Renderer<TableSortByToggleProps & HeaderProps<D>>;
|
||||
Footer?: Renderer<TableFooterProps<D>>;
|
||||
}
|
||||
|
||||
export interface ColumnInstance<D extends object>
|
||||
extends UseGlobalFiltersColumnOptions<D>,
|
||||
UseSortByColumnProps<D> {
|
||||
getSortByToggleProps: (
|
||||
props?: Partial<TableSortByToggleProps>,
|
||||
) => TableSortByToggleProps;
|
||||
}
|
||||
|
||||
export interface Hooks<D extends object>
|
||||
extends UseTableHooks<D>,
|
||||
UseSortByHooks<D> {}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 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 { SetDataMaskHook } from '@superset-ui/core';
|
||||
|
||||
export const updateExternalFormData = (
|
||||
setDataMask: SetDataMaskHook = () => {},
|
||||
pageNumber: number,
|
||||
pageSize: number,
|
||||
) =>
|
||||
setDataMask({
|
||||
ownState: {
|
||||
currentPage: pageNumber,
|
||||
pageSize,
|
||||
},
|
||||
});
|
||||
@@ -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.
|
||||
*/
|
||||
let cached: number | undefined;
|
||||
|
||||
const css = (x: TemplateStringsArray) => x.join('\n');
|
||||
|
||||
export default function getScrollBarSize(forceRefresh = false) {
|
||||
if (typeof document === 'undefined') {
|
||||
return 0;
|
||||
}
|
||||
if (cached === undefined || forceRefresh) {
|
||||
const inner = document.createElement('div');
|
||||
const outer = document.createElement('div');
|
||||
inner.style.cssText = css`
|
||||
width: auto;
|
||||
height: 100%;
|
||||
overflow: scroll;
|
||||
`;
|
||||
outer.style.cssText = css`
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
overflow: hidden;
|
||||
width: 100px;
|
||||
height: 50px;
|
||||
`;
|
||||
outer.append(inner);
|
||||
document.body.append(outer);
|
||||
cached = outer.clientWidth - inner.clientWidth;
|
||||
outer.remove();
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
/**
|
||||
* Whether a container need scroll bars when in another container.
|
||||
*/
|
||||
export default function needScrollBar({
|
||||
width,
|
||||
height,
|
||||
innerHeight,
|
||||
innerWidth,
|
||||
scrollBarSize,
|
||||
}: {
|
||||
width: number;
|
||||
height: number;
|
||||
innerHeight: number;
|
||||
scrollBarSize: number;
|
||||
innerWidth: number;
|
||||
}): [boolean, boolean] {
|
||||
const hasVerticalScroll = innerHeight > height;
|
||||
const hasHorizontalScroll =
|
||||
innerWidth > width - (hasVerticalScroll ? scrollBarSize : 0);
|
||||
return [hasVerticalScroll, hasHorizontalScroll];
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* 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 { useRef, useState } from 'react';
|
||||
import { useAsyncDebounce } from 'react-table';
|
||||
|
||||
// useAsyncDebounce in dist build of `react-table` requires regeneratorRuntime
|
||||
import 'regenerator-runtime/runtime';
|
||||
|
||||
/**
|
||||
* Hook useState to allow always return latest initialValue
|
||||
*/
|
||||
export default function useAsyncState<T, F extends (newValue: T) => unknown>(
|
||||
initialValue: T,
|
||||
callback: F,
|
||||
wait = 200,
|
||||
) {
|
||||
const [value, setValue] = useState(initialValue);
|
||||
const valueRef = useRef(initialValue);
|
||||
const onChange = useAsyncDebounce(callback, wait);
|
||||
|
||||
// sync updated initialValue
|
||||
if (valueRef.current !== initialValue) {
|
||||
valueRef.current = initialValue;
|
||||
if (value !== initialValue) {
|
||||
setValue(initialValue);
|
||||
}
|
||||
}
|
||||
|
||||
const setBoth = (newValue: T) => {
|
||||
setValue(newValue);
|
||||
onChange(newValue);
|
||||
};
|
||||
|
||||
return [value, setBoth] as [typeof value, typeof setValue];
|
||||
}
|
||||
@@ -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 { useLayoutEffect, useRef, useMemo } from 'react';
|
||||
|
||||
/**
|
||||
* Execute a memoized callback only when mounted. Execute again when factory updated.
|
||||
* Returns undefined if not mounted yet.
|
||||
*/
|
||||
export default function useMountedMemo<T>(
|
||||
factory: () => T,
|
||||
deps?: unknown[],
|
||||
): T | undefined {
|
||||
const mounted = useRef<typeof factory>();
|
||||
useLayoutEffect(() => {
|
||||
mounted.current = factory;
|
||||
});
|
||||
return useMemo(() => {
|
||||
if (mounted.current) {
|
||||
return factory();
|
||||
}
|
||||
return undefined;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [mounted.current, mounted.current === factory, ...(deps || [])]);
|
||||
}
|
||||
Reference in New Issue
Block a user