diff --git a/spug_web/src/components/AuthButton.js b/spug_web/src/components/AuthButton.js index 9fe5a7e..5b22516 100644 --- a/spug_web/src/components/AuthButton.js +++ b/spug_web/src/components/AuthButton.js @@ -5,7 +5,7 @@ */ import React from 'react'; import { Button } from 'antd'; -import { hasPermission } from "../libs"; +import { hasPermission } from 'libs'; export default function AuthButton(props) { diff --git a/spug_web/src/components/AuthDiv.js b/spug_web/src/components/AuthDiv.js index 8d7fbad..73fa88f 100644 --- a/spug_web/src/components/AuthDiv.js +++ b/spug_web/src/components/AuthDiv.js @@ -4,7 +4,7 @@ * Released under the AGPL-3.0 License. */ import React from 'react'; -import { hasPermission } from "../libs"; +import { hasPermission } from 'libs'; export default function AuthDiv(props) { diff --git a/spug_web/src/components/AuthFragment.js b/spug_web/src/components/AuthFragment.js index 06aafbf..3f4dcf2 100644 --- a/spug_web/src/components/AuthFragment.js +++ b/spug_web/src/components/AuthFragment.js @@ -4,7 +4,7 @@ * Released under the AGPL-3.0 License. */ import React from 'react'; -import { hasPermission } from "../libs"; +import { hasPermission } from 'libs'; export default function AuthFragment(props) { diff --git a/spug_web/src/components/Breadcrumb.js b/spug_web/src/components/Breadcrumb.js new file mode 100644 index 0000000..6f14328 --- /dev/null +++ b/spug_web/src/components/Breadcrumb.js @@ -0,0 +1,34 @@ +/** + * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug + * Copyright (c) + * Released under the AGPL-3.0 License. + */ +import React from 'react'; +import { Breadcrumb } from 'antd'; +import styles from './index.module.less'; + + +export default class extends React.Component { + static Item = Breadcrumb.Item + + render() { + let title = this.props.title; + if (!title) { + const rawChildren = this.props.children; + if (Array.isArray(rawChildren)) { + title = rawChildren[rawChildren.length - 1].props.children + } else { + title = rawChildren.props.children + } + } + + return ( +
+ + {this.props.children} + +
{title}
+
+ ) + } +} \ No newline at end of file diff --git a/spug_web/src/components/SearchForm.js b/spug_web/src/components/SearchForm.js index baccda9..d316508 100644 --- a/spug_web/src/components/SearchForm.js +++ b/spug_web/src/components/SearchForm.js @@ -5,32 +5,28 @@ */ import React from 'react'; import { Row, Col, Form } from 'antd'; -import styles from './index.module.css'; -import lodash from "lodash"; - +import styles from './index.module.less'; export default class extends React.Component { static Item(props) { return ( - - {props.children} - + + + {props.children} + + ) } render() { - let items = lodash.get(this.props, 'children', []); - if (!lodash.isArray(items)) items = [items]; return ( -
- - {items.filter(item => item).map((item, index) => ( - - {item} - - ))} - -
+
+
+ + {this.props.children} + +
+
) } } diff --git a/spug_web/src/components/StatisticsCard.js b/spug_web/src/components/StatisticsCard.js index f7ebf7f..bedd23e 100644 --- a/spug_web/src/components/StatisticsCard.js +++ b/spug_web/src/components/StatisticsCard.js @@ -4,9 +4,9 @@ * Released under the AGPL-3.0 License. */ import React from 'react'; -import { Card, Col, Row } from "antd"; +import { Card, Col, Row } from 'antd'; import lodash from 'lodash'; -import styles from './index.module.css'; +import styles from './index.module.less'; class StatisticsCard extends React.Component { diff --git a/spug_web/src/components/TableCard.js b/spug_web/src/components/TableCard.js new file mode 100644 index 0000000..48c70c6 --- /dev/null +++ b/spug_web/src/components/TableCard.js @@ -0,0 +1,148 @@ +/** + * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug + * Copyright (c) + * Released under the AGPL-3.0 License. + */ +import React, { useState, useEffect, useRef } from 'react'; +import { Table, Space, Divider, Popover, Checkbox, Button } from 'antd'; +import { ReloadOutlined, SettingOutlined, FullscreenOutlined } from '@ant-design/icons'; +import styles from './index.module.less'; + +function Footer(props) { + const actions = props.actions || []; + const length = props.selected.length; + return length > 0 ? ( +
+
已选择 {length}
+ + {actions.map((item, index) => ( + {item} + ))} + +
+ ) : null +} + +function Header(props) { + const columns = props.columns || []; + const actions = props.actions || []; + const fields = props.fields || []; + const onFieldsChange = props.onFieldsChange; + + const Fields = () => { + return ( + + {columns.map((item, index) => ( + {item.title} + ))} + + ) + } + + function handleCheckAll(e) { + if (e.target.checked) { + onFieldsChange(columns.map((_, index) => index)) + } else { + onFieldsChange([]) + } + } + + function handleFullscreen() { + if (props.rootRef.current && document.fullscreenEnabled) { + if (document.fullscreenElement) { + document.exitFullscreen() + } else { + props.rootRef.current.requestFullscreen() + } + } + } + + return ( +
+
{props.title}
+
+ + {actions.map((item, index) => ( + {item} + ))} + + {actions.length ? : null} + + + 列展示, + + ]} + overlayClassName={styles.tableFields} + trigger="click" + placement="bottomRight" + content={}> + + + + +
+
+ ) +} + +function TableCard(props) { + const rootRef = useRef(); + const batchActions = props.batchActions || []; + const selected = props.selected || []; + const [fields, setFields] = useState([]); + const [defaultFields, setDefaultFields] = useState([]); + const [columns, setColumns] = useState([]); + + useEffect(() => { + let [_columns, _fields] = [props.columns, []]; + if (props.children) { + if (Array.isArray(props.children)) { + _columns = props.children.filter(x => x.props).map(x => x.props) + } else { + _columns = [props.children.props] + } + } + for (let [index, item] of _columns.entries()) { + if (!item.hide) _fields.push(index) + } + setFields(_fields); + setColumns(_columns); + setDefaultFields(_fields); + }, [props.columns, props.children]) + + return ( +
+
+ fields.includes(index))} + dataSource={props.dataSource} + rowSelection={props.rowSelection} + expandable={props.expandable} + pagination={props.pagination}/> + {selected.length ?
: null} + + ) +} + +export default TableCard \ No newline at end of file diff --git a/spug_web/src/components/index.js b/spug_web/src/components/index.js index 0cd38c8..0d38fb5 100644 --- a/spug_web/src/components/index.js +++ b/spug_web/src/components/index.js @@ -12,6 +12,8 @@ import AuthCard from './AuthCard'; import AuthDiv from './AuthDiv'; import ACEditor from './ACEditor'; import Action from './Action'; +import TableCard from './TableCard'; +import Breadcrumb from './Breadcrumb'; export { StatisticsCard, @@ -23,4 +25,6 @@ export { AuthDiv, ACEditor, Action, + TableCard, + Breadcrumb, } diff --git a/spug_web/src/components/index.module.css b/spug_web/src/components/index.module.css deleted file mode 100644 index 4555710..0000000 --- a/spug_web/src/components/index.module.css +++ /dev/null @@ -1,38 +0,0 @@ -.searchForm :global(.ant-form-item) { - display: flex; -} - -.searchForm :global(.ant-form-item-control-wrapper) { - flex: 1; -} - -.searchForm :global(.ant-form-item-label) { - padding-right: 8px; -} - -.statisticsCard { - position: relative; - text-align: center; -} - -.statisticsCard span { - color: rgba(0, 0, 0, .45); - display: inline-block; - line-height: 22px; - margin-bottom: 4px; -} - -.statisticsCard p { - font-size: 32px; - line-height: 32px; - margin: 0; -} - -.statisticsCard em { - background-color: #e8e8e8; - position: absolute; - height: 56px; - width: 1px; - top: 0; - right: 0; -} diff --git a/spug_web/src/components/index.module.less b/spug_web/src/components/index.module.less new file mode 100644 index 0000000..22ea0ee --- /dev/null +++ b/spug_web/src/components/index.module.less @@ -0,0 +1,144 @@ +.searchForm { + padding: 24px 24px 0 24px; + background-color: #fff; + border-radius: 2px; +} + +.searchForm :global(.ant-form-item) { + display: flex; +} + +.searchForm :global(.ant-form-item-control-wrapper) { + flex: 1; +} + +.searchForm :global(.ant-form-item-label) { + padding-right: 8px; +} + +.statisticsCard { + position: relative; + text-align: center; +} + +.statisticsCard span { + color: rgba(0, 0, 0, .45); + display: inline-block; + line-height: 22px; + margin-bottom: 4px; +} + +.statisticsCard p { + font-size: 32px; + line-height: 32px; + margin: 0; +} + +.statisticsCard em { + background-color: #e8e8e8; + position: absolute; + height: 56px; + width: 1px; + top: 0; + right: 0; +} + +.tableCard { + border: 1px solid #f0f0f0; + background: #fff; + border-radius: 2px; + + :global(.ant-pagination) { + padding: 0 24px; + } + + .toolbar { + display: flex; + align-items: center; + justify-content: space-between; + height: 64px; + padding: 0 24px; + + .title { + flex: 1; + font-weight: 500; + font-size: 16px; + opacity: 0.8; + } + + .option { + display: flex; + align-items: center; + justify-content: flex-end; + + :global(.anticon) { + font-size: 16px; + margin-left: 8px; + } + } + } +} + +.tableFields { + :global(.ant-popover-title) { + padding: 10px 16px; + display: flex; + justify-content: space-between; + align-items: center; + } + + :global(.ant-popover-inner-content) { + padding: 8px 0; + + :global(.ant-checkbox-group) { + display: block; + } + + + :global(.ant-checkbox-wrapper) { + display: block; + height: 30px; + line-height: 30px; + margin: 0; + padding: 0 16px; + } + + :global(.ant-checkbox-wrapper):hover { + background: rgba(0, 0, 0, 0.025) + } + } +} + +.tableFooter { + position: fixed; + right: 0; + bottom: 0; + display: flex; + align-items: center; + height: 48px; + width: calc(100% - 208px); + padding: 0 24px; + background: #fff; + + .left { + flex: 1; + + span { + color: #1890ff; + font-weight: 600; + } + } +} + +.breadcrumb { + margin: -24px -24px 24px -24px; + padding: 16px 24px 0 24px; + background: #fff; + border-bottom: 1px solid #e8e8e8; + + .title { + margin-bottom: 9px; + font-size: 20px; + line-height: 50px; + } +} diff --git a/spug_web/src/pages/host/Form.js b/spug_web/src/pages/host/Form.js index 4f9fd0d..522c3e9 100644 --- a/spug_web/src/pages/host/Form.js +++ b/spug_web/src/pages/host/Form.js @@ -3,39 +3,31 @@ * Copyright (c) * Released under the AGPL-3.0 License. */ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { observer } from 'mobx-react'; -import { Modal, Form, Input, Select, Col, Button, Upload, message } from 'antd'; +import { ExclamationCircleOutlined, UploadOutlined } from '@ant-design/icons'; +import { Modal, Form, Input, Select, Button, Upload, message } from 'antd'; import { http, X_TOKEN } from 'libs'; import store from './store'; -@observer -class ComForm extends React.Component { - constructor(props) { - super(props); - this.state = { - loading: false, - uploading: false, - password: null, - addZone: null, - fileList: [], - editZone: store.record.zone, - } - } +export default observer(function () { + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); + const [uploading, setUploading] = useState(false); + const [password, setPassword] = useState(); + const [fileList, setFileList] = useState([]); - componentDidMount() { + useEffect(() => { if (store.record.pkey) { - this.setState({ - fileList: [{uid: '0', name: '独立密钥', data: store.record.pkey}] - }) + setFileList([{uid: '0', name: '独立密钥', data: store.record.pkey}]) } - } + }, []) - handleSubmit = () => { - this.setState({loading: true}); - const formData = this.props.form.getFieldsValue(); + function handleSubmit() { + setLoading(true); + const formData = form.getFieldsValue(); formData['id'] = store.record.id; - const file = this.state.fileList[0]; + const file = fileList[0]; if (file && file.data) formData['pkey'] = file.data; http.post('/api/host/', formData) .then(res => { @@ -43,12 +35,12 @@ class ComForm extends React.Component { if (formData.pkey) { message.error('独立密钥认证失败') } else { - this.setState({loading: false}); + setLoading(false) Modal.confirm({ - icon: 'exclamation-circle', + icon: , title: '首次验证请输入密码', - content: this.confirmForm(formData.username), - onOk: () => this.handleConfirm(formData), + content: , + onOk: () => handleConfirm(formData), }) } } else { @@ -56,12 +48,12 @@ class ComForm extends React.Component { store.formVisible = false; store.fetchRecords() } - }, () => this.setState({loading: false})) - }; + }, () => setLoading(false)) + } - handleConfirm = (formData) => { - if (this.state.password) { - formData['password'] = this.state.password; + function handleConfirm(formData) { + if (password) { + formData['password'] = password; return http.post('/api/host/', formData).then(res => { message.success('验证成功'); store.formVisible = false; @@ -69,153 +61,81 @@ class ComForm extends React.Component { }) } message.error('请输入授权密码') - }; + } - confirmForm = (username) => { - return ( -
- - this.setState({password: val.target.value})}/> - - - ) - }; + const ConfirmForm = (props) => ( +
+ + setPassword(e.target.value)}/> + + + ) - handleAddZone = () => { - this.setState({zone: ''}, () => { - Modal.confirm({ - icon: 'exclamation-circle', - title: '添加主机类别', - content: ( -
- - this.setState({addZone: e.target.value})}/> - - - ), - onOk: () => { - if (this.state.addZone) { - store.zones.push(this.state.addZone); - this.props.form.setFieldsValue({'zone': this.state.addZone}) - } - }, - }) - }); - }; - - handleEditZone = () => { - this.setState({zone: store.record.zone}, () => { - Modal.confirm({ - icon: 'exclamation-circle', - title: '编辑主机类别', - content: ( -
- - this.setState({editZone: e.target.value})}/> - - - ), - onOk: () => http.patch('/api/host/', {id: store.record.id, zone: this.state.editZone}) - .then(res => { - message.success(`成功修改${res}条记录`); - store.fetchRecords(); - this.props.form.setFieldsValue({'zone': this.state.editZone}) - }) - }) - }); - }; - - handleUploadChange = (v) => { + function handleUploadChange(v) { if (v.fileList.length === 0) { - this.setState({fileList: []}) + setFileList([]) } - }; + } - handleUpload = (file, fileList) => { - this.setState({uploading: true}); + function handleUpload(file, fileList) { + setUploading(true); const formData = new FormData(); formData.append('file', file); http.post('/api/host/parse/', formData) .then(res => { file.data = res; - this.setState({fileList: [file]}) + setFileList([file]) }) - .finally(() => this.setState({uploading: false})) + .finally(() => setUploading(false)) return false - }; - - render() { - const info = store.record; - const {fileList, loading, uploading} = this.state; - const {getFieldDecorator} = this.props.form; - return ( - store.formVisible = false} - confirmLoading={loading} - onOk={this.handleSubmit}> -
- -
- {getFieldDecorator('zone', {initialValue: info['zone']})( - - )} - - - - - - - - - - {getFieldDecorator('name', {initialValue: info['name']})( - - )} - - - - {getFieldDecorator('username', {initialValue: info['username']})( - - )} - - - {getFieldDecorator('hostname', {initialValue: info['hostname']})( - - )} - - - {getFieldDecorator('port', {initialValue: info['port']})( - - )} - - - - - {fileList.length === 0 ? : null} - - - - {getFieldDecorator('desc', {initialValue: info['desc']})( - - )} - - - ⚠️ 首次验证时需要输入登录用户名对应的密码,但不会存储该密码。 - - - - ) } -} -export default Form.create()(ComForm) + const info = store.record; + return ( + store.formVisible = false} + confirmLoading={loading} + onOk={handleSubmit}> +
+ + + + + + + + + + + + + + + + + + + + {fileList.length === 0 ? : null} + + + + + + + ⚠️ 首次验证时需要输入登录用户名对应的密码,但不会存储该密码。 + + +
+ ) +}) diff --git a/spug_web/src/pages/host/Import.js b/spug_web/src/pages/host/Import.js index 20d7cbc..a7e44dc 100644 --- a/spug_web/src/pages/host/Import.js +++ b/spug_web/src/pages/host/Import.js @@ -3,28 +3,23 @@ * Copyright (c) * Released under the AGPL-3.0 License. */ -import React from 'react'; +import React, { useState } from 'react'; import { observer } from 'mobx-react'; -import { Modal, Form, Input, Upload, Icon, Button, Tooltip, Alert } from 'antd'; +import { UploadOutlined } from '@ant-design/icons'; +import { Modal, Form, Input, Upload, Button, Tooltip, Alert } from 'antd'; import http from 'libs/http'; import store from './store'; -@observer -class ComImport extends React.Component { - constructor(props) { - super(props); - this.state = { - loading: false, - password: null, - fileList: [], - } - } +export default observer(function () { + const [loading, setLoading] = useState(false); + const [password, setPassword] = useState(''); + const [fileList, setFileList] = useState([]); - handleSubmit = () => { - this.setState({loading: true}); + function handleSubmit() { + setLoading(true); const formData = new FormData(); - formData.append('file', this.state.fileList[0]); - if (this.state.password) formData.append('password', this.state.password); + formData.append('file', fileList[0]); + if (password) formData.append('password', password); http.post('/api/host/import/', formData, {timeout: 120000}) .then(res => { Modal.info({ @@ -52,54 +47,50 @@ class ComImport extends React.Component { }) }) - .finally(() => this.setState({loading: false})) - }; - - handleUpload = (v) => { - if (v.fileList.length === 0) { - this.setState({fileList: []}) - } else { - this.setState({fileList: [v.file]}) - } - }; - - render() { - return ( - store.importVisible = false} - confirmLoading={this.state.loading} - okButtonProps={{disabled: !this.state.fileList.length}} - onOk={this.handleSubmit}> - -
- - 主机导入模板.xlsx - - - this.setState({password: e.target.value})} - placeholder="请输入默认主机密码"/> - - - false} - onChange={this.handleUpload}> - - - - -
- ) + .finally(() => setLoading(false)) } -} -export default ComImport + function handleUpload(v) { + if (v.fileList.length === 0) { + setFileList([]) + } else { + setFileList([v.file]) + } + } + + return ( + store.importVisible = false} + confirmLoading={loading} + okButtonProps={{disabled: !fileList.length}} + onOk={handleSubmit}> + +
+ + 主机导入模板.xlsx + + + setPassword(e.target.value)} + placeholder="请输入默认主机密码"/> + + + false} + onChange={handleUpload}> + + + + +
+ ); +}) diff --git a/spug_web/src/pages/host/Table.js b/spug_web/src/pages/host/Table.js index 3808a04..a721d81 100644 --- a/spug_web/src/pages/host/Table.js +++ b/spug_web/src/pages/host/Table.js @@ -6,9 +6,8 @@ import React from 'react'; import { observer } from 'mobx-react'; import { Table, Modal, message } from 'antd'; -import { Action } from 'components'; -import ComForm from './Form'; -import ComImport from './Import'; +import { PlusOutlined, ImportOutlined } from '@ant-design/icons'; +import { Action, TableCard, AuthButton } from 'components'; import { http, hasPermission } from 'libs'; import store from './store'; @@ -48,36 +47,46 @@ class ComTable extends React.Component { data = data.filter(item => item['hostname'].toLowerCase().includes(store.f_host.toLowerCase())) } return ( - -
`共 ${total} 条`, - pageSizeOptions: ['10', '20', '50', '100'] - }}> - - a.name.localeCompare(b.name)}/> - a.name.localeCompare(b.name)}/> - - - {hasPermission('host.host.edit|host.host.del|host.host.console') && ( - ( - - store.showForm(info)}>编辑 - this.handleDelete(info)}>删除 - this.handleConsole(info)}>Console - - )}/> - )} -
- {store.formVisible && } - {store.importVisible && } - + } + onClick={() => store.showForm()}>新建, + } + onClick={() => store.importVisible = true}>批量导入 + ]} + pagination={{ + showSizeChanger: true, + showLessItems: true, + hideOnSinglePage: true, + showTotal: total => `共 ${total} 条`, + pageSizeOptions: ['10', '20', '50', '100'] + }}> + + a.name.localeCompare(b.name)}/> + a.name.localeCompare(b.name)}/> + + + {hasPermission('host.host.edit|host.host.del|host.host.console') && ( + ( + + store.showForm(info)}>编辑 + this.handleDelete(info)}>删除 + this.handleConsole(info)}>Console + + )}/> + )} + ) } } diff --git a/spug_web/src/pages/host/index.js b/spug_web/src/pages/host/index.js index 8916329..0d84cdc 100644 --- a/spug_web/src/pages/host/index.js +++ b/spug_web/src/pages/host/index.js @@ -5,15 +5,21 @@ */ import React from 'react'; import { observer } from 'mobx-react'; -import { Input, Button, Select } from 'antd'; -import { SearchForm, AuthDiv, AuthCard } from 'components'; +import { Input, Select } from 'antd'; +import { SearchForm, AuthDiv, Breadcrumb } from 'components'; import ComTable from './Table'; +import ComForm from './Form'; +import ComImport from './Import'; import store from './store'; export default observer(function () { return ( - - + + + 首页 + 主机管理 + + store.f_host = e.target.value} placeholder="请输入"/> - - - - - - - - - ) + {store.formVisible && } + {store.importVisible && } + + ); }) diff --git a/spug_web/src/pages/host/routes.js b/spug_web/src/pages/host/routes.js deleted file mode 100644 index 43e6585..0000000 --- a/spug_web/src/pages/host/routes.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug - * Copyright (c) - * Released under the AGPL-3.0 License. - */ -import { makeRoute } from "../../libs/router"; -import Index from './index'; - - -export default [ - makeRoute('', Index), -]