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
|
||||
```
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
@ -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…
Reference in new issue