feat: custom table with row children

main
jialin 2 years ago
parent f491b5dfb1
commit 405d95c81f

@ -1,3 +1,42 @@
# README
## Installation
gpustack ui
1. [Nodejs](https://nodejs.org/en) 16.0+(with NPM)
If you're on Mac
```
brew install node
```
2. [pnpm](https://pnpm.io/installation#using-npm)
```
npm install -g pnpm
```
3. Setup
```
git clone https://github.com/seal-io/gpustack-ui/
```
4. Install dependencies
```
cd gpustack-ui
pnpm install
```
## Usage
1. Run development server (at http://localhost:9000)
```
npm run dev
```
2. build release
```
npm run build
```

@ -1,12 +1,38 @@
import { RequestConfig } from '@umijs/max';
import { RequestConfig, history } from '@umijs/max';
import { requestConfig } from './request-config';
import { queryCurrentUserState } from './services/profile/apis';
const loginPath = '/login';
// 运行时配置
// 全局初始化数据配置,用于 Layout 用户信息和权限初始化
// 更多信息见文档https://umijs.org/docs/api/runtime-config#getinitialstate
export async function getInitialState(): Promise<{ name: string }> {
console.log('getInitialState+++++++++++++++');
export async function getInitialState() {
// 如果不是登录页面,执行
const { location } = history;
const fetchUserInfo = async () => {
try {
const data = await queryCurrentUserState({
skipErrorHandler: true
});
return data;
} catch (error) {
history.push(loginPath);
}
return undefined;
};
if (![loginPath].includes(location.pathname)) {
const currentUser = await fetchUserInfo();
return {
fetchUserInfo,
name: 'admin',
...currentUser
};
}
return {
fetchUserInfo,
name: 'admin'
};
}

@ -0,0 +1,27 @@
import classNames from 'classnames';
import React, { useContext } from 'react';
import RowContext from '../row-context';
import '../styles/cell.less';
import { SealColumnProps } from '../types';
const SealColumn: React.FC<SealColumnProps> = (props) => {
const row: Record<string, any> = useContext(RowContext);
const { dataIndex, render, align } = props;
return (
<div
className={classNames('cell', {
'cell-left': align === 'left',
'cell-center': align === 'center',
'cell-right': align === 'right'
})}
>
<span className="cell-content">
{render
? render(row[dataIndex], { ...row, dataIndex })
: row[dataIndex]}
</span>
</div>
);
};
export default React.memo(SealColumn);

@ -0,0 +1,24 @@
import classNames from 'classnames';
import React from 'react';
import '../styles/header.less';
import { TableHeaderProps } from '../types';
const TableHeader: React.FC<TableHeaderProps> = (props) => {
const { title, style, align, firstCell, lastCell } = props;
return (
<div
style={{ ...style }}
className={classNames('table-header', {
'table-header-left': align === 'left',
'table-header-center': align === 'center',
'table-header-right': align === 'right',
'table-header-first': firstCell,
'table-header-last': lastCell
})}
>
<div className="table-header-cell">{title}</div>
</div>
);
};
export default React.memo(TableHeader);

@ -0,0 +1,137 @@
import { DownOutlined, RightOutlined } from '@ant-design/icons';
import { Button, Checkbox, Col, Row } from 'antd';
import classNames from 'classnames';
import _ from 'lodash';
import React, { useEffect, useState } from 'react';
import RowContext from '../row-context';
import { RowContextProps, SealTableProps } from '../types';
const TableRow: React.FC<
RowContextProps &
Omit<SealTableProps, 'dataSource' | 'loading' | 'children' | 'empty'>
> = (props) => {
const {
record,
rowIndex,
expandable,
rowSelection,
rowKey,
columns,
onExpand
} = props;
const [expanded, setExpanded] = useState(false);
const [checked, setChecked] = useState(false);
useEffect(() => {
if (rowSelection) {
const { selectedRowKeys } = rowSelection;
if (selectedRowKeys.includes(record[rowKey])) {
setChecked(true);
} else {
setChecked(false);
}
}
}, [rowSelection]);
const handleRowExpand = () => {
setExpanded(!expanded);
onExpand?.(!expanded, record);
};
const handleSelectChange = (e: any) => {
if (e.target.checked) {
// update selectedRowKeys
rowSelection?.onChange(
_.uniq([...rowSelection?.selectedRowKeys, record[rowKey]])
);
} else {
// update selectedRowKeys
rowSelection?.onChange(
rowSelection?.selectedRowKeys.filter((key) => key !== record[rowKey])
);
}
};
const renderRowPrefix = () => {
if (expandable && rowSelection) {
return (
<div className="row-prefix-wrapper">
<span style={{ marginRight: 5 }}>
{_.isBoolean(expandable) ? (
<Button type="text" size="small" onClick={handleRowExpand}>
{expanded ? <DownOutlined /> : <RightOutlined />}
</Button>
) : (
expandable
)}
</span>
<Checkbox onChange={handleSelectChange} checked={checked}></Checkbox>
</div>
);
}
if (expandable) {
return (
<div className="row-prefix-wrapper">
{_.isBoolean(expandable) ? (
<Button type="text" size="small" onClick={handleRowExpand}>
{expanded ? <DownOutlined /> : <RightOutlined />}
</Button>
) : (
expandable
)}
</div>
);
}
if (rowSelection) {
return (
<div className="row-prefix-wrapper">
{
<Checkbox
onChange={handleSelectChange}
checked={checked}
></Checkbox>
}
</div>
);
}
return null;
};
return (
<RowContext.Provider value={{ ...record, rowIndex }}>
<div className="row-box">
<div
className={classNames('row-wrapper', {
'row-wrapper-selected': checked
})}
>
{renderRowPrefix()}
<Row className="seal-table-row">
{React.Children.map(columns, (child) => {
const { props: columnProps } = child as any;
if (React.isValidElement(child)) {
return (
<Col
key={`${columnProps.dataIndex}-${rowIndex}`}
span={columnProps.span}
>
{child}
</Col>
);
}
return <Col span={columnProps.span}></Col>;
})}
</Row>
</div>
{expanded && (
<div className="expanded-row">
<div className="expanded-row-content">rowchildren</div>
</div>
)}
</div>
</RowContext.Provider>
);
};
export default TableRow;

@ -0,0 +1,145 @@
import { RightOutlined } from '@ant-design/icons';
import { Button, Checkbox, Col, Empty, Row, Spin } from 'antd';
import _ from 'lodash';
import React, { useEffect, useState } from 'react';
import TableHeader from './components/table-header';
import TableRow from './components/table-row';
import './styles/index.less';
import { SealColumnProps, SealTableProps } from './types';
const SealTable: React.FC<SealTableProps> = (props) => {
const { children, rowKey, onExpand, loading, expandable, rowSelection } =
props;
const [selectAll, setSelectAll] = useState(false);
const [indeterminate, setIndeterminate] = useState(false);
useEffect(() => {
if (rowSelection) {
const { selectedRowKeys } = rowSelection;
if (selectedRowKeys.length === 0) {
setSelectAll(false);
setIndeterminate(false);
} else if (selectedRowKeys.length === props.dataSource.length) {
setSelectAll(true);
setIndeterminate(false);
} else {
setSelectAll(false);
setIndeterminate(true);
}
}
}, [rowSelection]);
const handleSelectAllChange = (e: any) => {
if (e.target.checked) {
// update selectedRowKeys
rowSelection?.onChange(props.dataSource.map((record) => record[rowKey]));
setSelectAll(true);
setIndeterminate(false);
} else {
// update selectedRowKeys
rowSelection?.onChange([]);
setSelectAll(false);
setIndeterminate(false);
}
};
const renderHeaderPrefix = () => {
if (expandable && rowSelection) {
return (
<div className="header-row-prefix-wrapper">
<span style={{ marginRight: 5, padding: '0 14px' }}></span>
<Checkbox
onChange={handleSelectAllChange}
indeterminate={indeterminate}
checked={selectAll}
></Checkbox>
</div>
);
}
if (expandable) {
return (
<div className="header-row-prefix-wrapper">
{_.isBoolean(expandable) ? (
<Button type="text" size="small">
<RightOutlined />
</Button>
) : (
expandable
)}
</div>
);
}
if (rowSelection) {
return (
<div className="header-row-prefix-wrapper">{<Checkbox></Checkbox>}</div>
);
}
return null;
};
const renderHeader = () => {
return (
<div className="header-row-wrapper">
{renderHeaderPrefix()}
<Row className="row">
{React.Children.map(props.children, (child, i) => {
const { props: columnProps } = child as any;
const { title, align, span, headerStyle } =
columnProps as SealColumnProps;
if (React.isValidElement(child)) {
return (
<Col span={span}>
<TableHeader
title={title}
style={headerStyle}
firstCell={i === 0}
lastCell={i === props.children.length - 1}
></TableHeader>
</Col>
);
}
return null;
})}
</Row>
</div>
);
};
const renderContent = () => {
if (!props.dataSource.length) {
return <Empty></Empty>;
}
return (
<div className="seal-table-content">
{props.dataSource.map((item, index) => {
return (
<TableRow
record={item}
rowIndex={index}
columns={children}
key={item[rowKey]}
rowSelection={rowSelection}
expandable={expandable}
rowKey={rowKey}
onExpand={onExpand}
></TableRow>
);
})}
</div>
);
};
return (
<div className="seal-table-container">
{renderHeader()}
{loading ? (
<div className="spin">
<Spin></Spin>
</div>
) : (
renderContent()
)}
</div>
);
};
export default SealTable;

@ -0,0 +1,5 @@
import React from 'react';
const RowContext = React.createContext({});
export default RowContext;

@ -0,0 +1,17 @@
.cell {
padding: var(--ant-table-cell-padding-block)
var(--ant-table-cell-padding-inline);
display: flex;
align-items: center;
justify-content: flex-start;
height: 70px;
&-left {
justify-content: flex-start;
}
&-right {
justify-content: flex-end;
}
&-center {
justify-content: center;
}
}

@ -0,0 +1,23 @@
.table-header {
display: flex;
align-items: center;
justify-content: flex-start;
padding-inline: var(--ant-table-cell-padding-inline);
border-right: 1px solid var(--ant-table-header-split-color);
&-last {
border-right: none;
}
&-cell {
font-weight: 600;
}
&-left {
justify-content: flex-start;
}
&-right {
justify-content: flex-end;
}
&-center {
justify-content: center;
}
}

@ -0,0 +1,64 @@
.seal-table-container {
display: flex;
flex-direction: column;
width: 100%;
.header-row-wrapper {
height: 40px;
margin-bottom: 10px;
display: flex;
justify-content: flex-start;
align-items: center;
.header-row-prefix-wrapper {
padding-left: var(--ant-table-cell-padding-inline);
}
.row {
flex: 1;
}
}
.row-box {
margin-bottom: 20px;
border-radius: var(--ant-table-header-border-radius);
overflow: hidden;
}
.expanded-row {
background-color: var(--color-white-1);
padding: 16px 20px;
border: 1px solid var(--color-fill-1);
border-top: 0;
border-radius: 0 0 var(--ant-table-header-border-radius)
var(--ant-table-header-border-radius);
}
.row-wrapper {
display: flex;
justify-content: flex-start;
align-items: center;
background-color: var(--color-fill-1);
transition: all 0.2s ease;
&:hover {
background-color: var(--ant-table-row-hover-bg);
transition: all 0.2s ease;
}
&-selected {
background-color: var(--ant-table-row-selected-bg);
transition: all 0.2s ease;
&:hover {
background-color: var(--ant-table-row-selected-hover-bg);
transition: all 0.2s ease;
}
}
.row-prefix-wrapper {
padding-left: var(--ant-table-cell-padding-inline);
}
}
.seal-table-row {
flex: 1;
}
.spin {
text-align: center;
display: flex;
justify-content: center;
align-items: center;
margin-top: 20px;
min-height: 100px;
}
}

@ -0,0 +1,41 @@
import React from 'react';
export interface SealColumnProps {
title: string;
render?: (text: any, record: any) => React.ReactNode;
dataIndex: string;
width?: number;
span: number;
align?: 'left' | 'center' | 'right';
headerStyle?: React.CSSProperties;
}
export interface TableHeaderProps {
children?: React.ReactNode;
title: string;
style?: React.CSSProperties;
firstCell?: boolean;
lastCell?: boolean;
align?: 'left' | 'center' | 'right';
}
export interface RowSelectionProps {
selectedRowKeys: React.Key[];
onChange: (selectedRowKeys: React.Key[]) => void;
}
export interface SealTableProps {
rowSelection?: RowSelectionProps;
children: React.ReactNode[];
empty?: React.ReactNode;
expandable?: React.ReactNode;
dataSource: any[];
loading?: boolean;
onExpand?: (expanded: boolean, record: any) => void;
rowKey: string;
}
export interface RowContextProps {
record: Record<string, any>;
columns: React.ReactNode[];
rowIndex: number;
}

@ -15,6 +15,13 @@ html {
--ant-color-fill-secondary: rgba(0, 0, 0, 0.06);
--table-td-radius: 24px;
--checkbox-border-radius: 4px;
--ant-table-cell-padding-inline: 16px;
--ant-table-cell-padding-block: 16px;
--ant-table-header-border-radius: 16px;
--ant-table-header-split-color: #f0f0f0;
--ant-table-row-selected-bg: #f0fff6;
--ant-table-row-selected-hover-bg: #d8f2e4;
--ant-table-row-hover-bg: #e6e6e6;
// --color-text-1: #000;
--seal-transition-func: cubic-bezier(0, 0, 1, 1);

@ -86,6 +86,7 @@ export default (props: any) => {
loading: false,
setInitialState: null
};
console.log('initialInfo==========', initialInfo);
const { initialState, loading, setInitialState } = initialInfo;
const userConfig = {

@ -0,0 +1,22 @@
import SealTable from '@/components/seal-table';
import SealColumn from '@/components/seal-table/components/seal-column';
import useTableRowSelection from '@/hooks/use-table-row-selection';
const Table = () => {
const dataSource = [{ name: 'test' }, { name: 'test2' }];
const rowSelection = useTableRowSelection();
return (
<SealTable
dataSource={dataSource}
rowKey="name"
loading={false}
rowSelection={rowSelection}
expandable={true}
>
<SealColumn title="Name" dataIndex="name" key="name" span={12} />
<SealColumn title="Status" dataIndex="status" key="status" span={12} />
</SealTable>
);
};
export default Table;

@ -4,8 +4,14 @@ import { Tabs } from 'antd';
import { useState } from 'react';
import GPUs from './components/gpus';
import Nodes from './components/nodes';
import Test from './components/test';
const items: TabsProps['items'] = [
{
key: 'test',
label: 'Test',
children: <Test />
},
{
key: 'nodes',
label: 'Nodes',
@ -18,7 +24,7 @@ const items: TabsProps['items'] = [
}
];
const Resources = () => {
const [activeKey, setActiveKey] = useState('nodes');
const [activeKey, setActiveKey] = useState('test');
const handleChangeTab = (key: string) => {
setActiveKey(key);

@ -1,4 +1,6 @@
import PageTools from '@/components/page-tools';
import SealTable from '@/components/seal-table';
import SealColumn from '@/components/seal-table/components/seal-column';
import { PageAction } from '@/config';
import type { PageActionType } from '@/config/types';
import useTableRowSelection from '@/hooks/use-table-row-selection';
@ -208,11 +210,12 @@ const Models: React.FC = () => {
</Space>
}
></PageTools>
<Table
<SealTable
dataSource={dataSource}
rowSelection={rowSelection}
loading={loading}
rowKey="id"
expandable={true}
onChange={handleTableChange}
pagination={{
showSizeChanger: true,
@ -224,11 +227,12 @@ const Models: React.FC = () => {
onChange: handlePageChange
}}
>
<Column
<SealColumn
title="Model Name"
dataIndex="name"
key="name"
width={400}
span={8}
render={(text, record) => {
return (
<>
@ -243,7 +247,8 @@ const Models: React.FC = () => {
);
}}
/>
<Column
<SealColumn
span={8}
title="Create Time"
dataIndex="created_at"
key="createTime"
@ -255,7 +260,8 @@ const Models: React.FC = () => {
return dayjs(val).format('YYYY-MM-DD HH:mm:ss');
}}
/>
<Column
<SealColumn
span={8}
title="Operation"
key="operation"
render={(text, record) => {
@ -282,7 +288,7 @@ const Models: React.FC = () => {
) : null;
}}
/>
</Table>
</SealTable>
</PageContainer>
<AddModal
open={openAddModal}

@ -243,7 +243,7 @@ const Models: React.FC = () => {
<Column
title="Operation"
key="operation"
render={(text, record) => {
render={(text, record: ListItem) => {
return (
<Space>
<Tooltip title="编辑">

@ -7,6 +7,7 @@ export const requestConfig: RequestConfig = {
console.log('errorThrower+++++++++++++++', res);
},
errorHandler: (error: any, opts: any) => {
if (opts?.skipErrorHandler) throw error;
const { message: errorMessage, response } = error;
const errMsg = response?.data?.message || errorMessage;
message.error(errMsg);

@ -0,0 +1,8 @@
import { request } from '@umijs/max';
export async function queryCurrentUserState(opts?: Record<string, any>) {
return request(`/users/me`, {
method: 'GET',
...opts
});
}
Loading…
Cancel
Save