增加用户组功能

This commit is contained in:
dushixiang
2021-01-17 00:39:39 +08:00
parent 11c1ac23e4
commit a0610b8ce0
14 changed files with 741 additions and 45 deletions

View File

@ -135,6 +135,12 @@ func Run() error {
if err := global.DB.AutoMigrate(&model.Resource{}); err != nil {
return err
}
if err := global.DB.AutoMigrate(&model.UserGroup{}); err != nil {
return err
}
if err := global.DB.AutoMigrate(&model.UserGroupMember{}); err != nil {
return err
}
if err := global.DB.AutoMigrate(&model.Num{}); err != nil {
return err
}

View File

@ -51,6 +51,17 @@ func SetupRoutes() *echo.Echo {
users.GET("/:id", UserGetEndpoint)
}
userGroups := e.Group("/user-groups")
{
userGroups.POST("", UserGroupCreateEndpoint)
userGroups.GET("/paging", UserGroupPagingEndpoint)
userGroups.PUT("/:id", UserGroupUpdateEndpoint)
userGroups.DELETE("/:id", UserGroupDeleteEndpoint)
userGroups.GET("/:id", UserGroupGetEndpoint)
userGroups.POST("/:id/members", UserGroupAddMembersEndpoint)
userGroups.DELETE("/:id/members/:memberId", UserGroupDelMembersEndpoint)
}
assets := e.Group("/assets", Auth)
{
assets.GET("", AssetAllEndpoint)

114
pkg/api/user-group.go Normal file
View File

@ -0,0 +1,114 @@
package api
import (
"github.com/labstack/echo/v4"
"next-terminal/pkg/global"
"next-terminal/pkg/model"
"next-terminal/pkg/utils"
"strconv"
"strings"
)
type UserGroup struct {
Name string `json:"name"`
Members []string `json:"members"`
}
func UserGroupCreateEndpoint(c echo.Context) error {
var item UserGroup
if err := c.Bind(&item); err != nil {
return err
}
userGroup := model.UserGroup{
ID: utils.UUID(),
Created: utils.NowJsonTime(),
Name: item.Name,
}
if err := model.CreateNewUserGroup(&userGroup, item.Members); err != nil {
return err
}
return Success(c, item)
}
func UserGroupPagingEndpoint(c echo.Context) error {
pageIndex, _ := strconv.Atoi(c.QueryParam("pageIndex"))
pageSize, _ := strconv.Atoi(c.QueryParam("pageSize"))
name := c.QueryParam("name")
items, total, err := model.FindPageUserGroup(pageIndex, pageSize, name)
if err != nil {
return err
}
return Success(c, H{
"total": total,
"items": items,
})
}
func UserGroupUpdateEndpoint(c echo.Context) error {
id := c.Param("id")
var item model.UserGroup
if err := c.Bind(&item); err != nil {
return err
}
model.UpdateUserGroupById(&item, id)
return Success(c, nil)
}
func UserGroupDeleteEndpoint(c echo.Context) error {
ids := c.Param("id")
split := strings.Split(ids, ",")
for i := range split {
userId := split[i]
model.DeleteUserGroupById(userId)
}
return Success(c, nil)
}
func UserGroupGetEndpoint(c echo.Context) error {
id := c.Param("id")
item, err := model.FindUserGroupById(id)
if err != nil {
return err
}
return Success(c, item)
}
func UserGroupAddMembersEndpoint(c echo.Context) error {
id := c.Param("id")
var items []string
if err := c.Bind(&items); err != nil {
return err
}
if err := model.AddUserGroupMembers(global.DB, items, id); err != nil {
return err
}
return Success(c, "")
}
func UserGroupDelMembersEndpoint(c echo.Context) (err error) {
id := c.Param("id")
memberIdsStr := c.Param("memberId")
memberIds := strings.Split(memberIdsStr, ",")
for i := range memberIds {
memberId := memberIds[i]
err = global.DB.Where("user_group_id = ? and user_id = ?", id, memberId).Delete(&model.UserGroupMember{}).Error
if err != nil {
return err
}
}
return Success(c, "")
}

1
pkg/model/asset-group.go Normal file
View File

@ -0,0 +1 @@
package model

View File

@ -43,11 +43,6 @@ func (r *Asset) TableName() string {
return "assets"
}
func FindAllAsset() (o []Asset, err error) {
err = global.DB.Find(&o).Error
return
}
func FindAssetByConditions(protocol string, account User) (o []Asset, err error) {
db := global.DB.Table("assets").Select("assets.id,assets.name,assets.ip,assets.port,assets.protocol,assets.active,assets.owner,assets.created, users.nickname as owner_name,COUNT(resources.user_id) as sharer_count").Joins("left join users on assets.owner = users.id").Joins("left join resources on assets.id = resources.resource_id").Group("assets.id")

View File

@ -0,0 +1,11 @@
package model
type UserGroupMember struct {
ID string `gorm:"primary_key" json:"name"`
UserId string `json:"userId"`
UserGroupId string `json:"userGroupId"`
}
func (r *UserGroupMember) TableName() string {
return "user_group_members"
}

97
pkg/model/user-group.go Normal file
View File

@ -0,0 +1,97 @@
package model
import (
"gorm.io/gorm"
"next-terminal/pkg/global"
"next-terminal/pkg/utils"
)
type UserGroup struct {
ID string `gorm:"primary_key" json:"id"`
Name string `json:"name"`
Created utils.JsonTime `json:"created"`
}
type UserGroupVo struct {
ID string `gorm:"primary_key" json:"id"`
Name string `json:"name"`
Created utils.JsonTime `json:"created"`
MemberCount int64 `json:"memberCount"`
}
func (r *UserGroup) TableName() string {
return "user_groups"
}
func FindPageUserGroup(pageIndex, pageSize int, name string) (o []UserGroupVo, total int64, err error) {
db := global.DB.Table("user_groups").Select("user_groups.id, user_groups.name, user_groups.created, count(user_group_members.user_id) as member_count").Joins("left join user_group_members on user_groups.id = user_group_members.user_group_id").Group("user_groups.id")
dbCounter := global.DB.Table("user_groups")
if len(name) > 0 {
db = db.Where("user_groups.name like ?", "%"+name+"%")
dbCounter = dbCounter.Where("name like ?", "%"+name+"%")
}
err = dbCounter.Count(&total).Error
if err != nil {
return nil, 0, err
}
err = db.Order("user_groups.created desc").Find(&o).Offset((pageIndex - 1) * pageSize).Limit(pageSize).Error
if o == nil {
o = make([]UserGroupVo, 0)
}
return
}
func CreateNewUserGroup(o *UserGroup, members []string) (err error) {
return global.DB.Transaction(func(tx *gorm.DB) error {
err = tx.Create(o).Error
if err != nil {
return err
}
if members != nil {
userGroupId := o.ID
err = AddUserGroupMembers(tx, members, userGroupId)
if err != nil {
return err
}
}
return err
})
}
func AddUserGroupMembers(tx *gorm.DB, userIds []string, userGroupId string) error {
for i := range userIds {
userId := userIds[i]
_, err := FindUserById(userId)
if err != nil {
return err
}
userGroupMember := UserGroupMember{
ID: utils.Sign([]string{userGroupId, userId}),
UserId: userId,
UserGroupId: userGroupId,
}
err = tx.Create(&userGroupMember).Error
if err != nil {
return err
}
}
return nil
}
func FindUserGroupById(id string) (o UserGroup, err error) {
err = global.DB.Where("id = ?", id).First(&o).Error
return
}
func UpdateUserGroupById(o *UserGroup, id string) {
o.ID = id
global.DB.Updates(o)
}
func DeleteUserGroupById(id string) {
global.DB.Where("id = ?", id).Delete(&UserGroup{})
global.DB.Where("user_group_id = ?", id).Delete(&UserGroupMember{})
}

View File

@ -35,6 +35,7 @@ import Setting from "./components/setting/Setting";
import BatchCommand from "./components/command/BatchCommand";
import {NT_PACKAGE} from "./utils/utils";
import {isAdmin} from "./service/permission";
import UserGroup from "./components/user/UserGroup";
const {Footer, Sider} = Layout;
@ -188,25 +189,18 @@ class App extends Component {
</Menu.Item>
</SubMenu>
{/*<SubMenu key='user-team' title='用户管理' icon={<UserSwitchOutlined />}>*/}
{/* <Menu.Item key="team" icon={<TeamOutlined />}>*/}
{/* <Link to={'/team'}>*/}
{/* 用户组管理*/}
{/* </Link>*/}
{/* </Menu.Item>*/}
{/* <Menu.Item key="user" icon={<UserOutlined/>}>*/}
{/* <Link to={'/user'}>*/}
{/* 用户管理*/}
{/* </Link>*/}
{/* </Menu.Item>*/}
{/*</SubMenu>*/}
<SubMenu key='user-group' title='用户管理' icon={<UserSwitchOutlined />}>
<Menu.Item key="user" icon={<UserOutlined/>}>
<Link to={'/user'}>
用户管理
</Link>
</Menu.Item>
<Menu.Item key="user-group" icon={<TeamOutlined />}>
<Link to={'/user-group'}>
用户组管理
</Link>
</Menu.Item>
</SubMenu>
</> : undefined
}
@ -246,6 +240,7 @@ class App extends Component {
<Route path="/" exact component={Dashboard}/>
<Route path="/user" component={User}/>
<Route path="/user-group" component={UserGroup}/>
<Route path="/asset" component={Asset}/>
<Route path="/credential" component={Credential}/>
<Route path="/dynamic-command" component={DynamicCommand}/>

View File

@ -466,7 +466,7 @@ class Asset extends Component {
</Menu.Item>
{isAdmin() ?
<Menu.Item key="1">
<Menu.Item key="3">
<Button type="text" size='small'
disabled={!hasPermission(record['owner'])}
onClick={() => {
@ -488,7 +488,7 @@ class Asset extends Component {
}
<Menu.Item key="2">
<Menu.Item key="4">
<Button type="text" size='small'
disabled={!hasPermission(record['owner'])}
onClick={async () => {
@ -497,7 +497,7 @@ class Asset extends Component {
</Menu.Item>
<Menu.Divider/>
<Menu.Item key="3">
<Menu.Item key="5">
<Button type="text" size='small' danger
disabled={!hasPermission(record['owner'])}
onClick={() => this.showDeleteConfirm(record.id, record.name)}>删除</Button>

View File

@ -165,7 +165,7 @@ class OfflineSession extends Component {
}
handleSearchByNickname = async nickname => {
const result = await request.get(`/users/paging?pageIndex=1&pageSize=100&nickname=${nickname}`);
const result = await request.get(`/users/paging?pageIndex=1&pageSize=1000&nickname=${nickname}`);
if (result.code !== 1) {
message.error(result.message, 10);
return;

View File

@ -369,7 +369,7 @@ class User extends Component {
<Button icon={<UndoOutlined/>} onClick={() => {
this.inputRefOfUsername.current.setValue('');
this.inputRefOfNickname.current.setValue('');
this.loadTableData({pageIndex: 1, pageSize: 10, protocol: ''})
this.loadTableData({pageIndex: 1, pageSize: 10})
}}>
</Button>
@ -483,8 +483,6 @@ class User extends Component {
loading={this.state.loading}
/>
{
this.state.modalVisible ?
<UserModal
visible={this.state.modalVisible}
title={this.state.modalTitle}
@ -494,10 +492,6 @@ class User extends Component {
model={this.state.model}
>
</UserModal>
: null
}
</Content>
</>
);

View File

@ -0,0 +1,403 @@
import React, {Component} from 'react';
import {itemRender} from '../../utils/utils'
import {Button, Col, Divider, Input, Layout, Modal, PageHeader, Row, Space, Table, Tooltip, Typography,} from "antd";
import qs from "qs";
import request from "../../common/request";
import {message} from "antd/es";
import {DeleteOutlined, ExclamationCircleOutlined, PlusOutlined, SyncOutlined, UndoOutlined} from '@ant-design/icons';
import Logout from "./Logout";
import UserGroupModal from "./UserGroupModal";
const confirm = Modal.confirm;
const {Search} = Input;
const {Title, Text} = Typography;
const {Content} = Layout;
const routes = [
{
path: '',
breadcrumbName: '首页',
},
{
path: 'user',
breadcrumbName: '用户组',
}
];
class UserGroup extends Component {
inputRefOfName = React.createRef();
state = {
items: [],
total: 0,
queryParams: {
pageIndex: 1,
pageSize: 10
},
loading: false,
modalVisible: false,
modalTitle: '',
modalConfirmLoading: false,
model: null,
selectedRowKeys: [],
delBtnLoading: false,
users: [],
members: []
};
componentDidMount() {
this.loadTableData();
}
async delete(id) {
let result = await request.delete('/user-groups/' + id);
if (result.code === 1) {
message.success('操作成功', 3);
await this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
}
}
async loadTableData(queryParams) {
this.setState({
loading: true
});
queryParams = queryParams || this.state.queryParams;
let paramsStr = qs.stringify(queryParams);
let data = {
items: [],
total: 0
};
try {
let result = await request.get('/user-groups/paging?' + paramsStr);
if (result.code === 1) {
data = result.data;
} else {
message.error(result.message, 10);
}
} catch (e) {
} finally {
const items = data.items.map(item => {
return {'key': item['id'], ...item}
})
this.setState({
items: items,
total: data.total,
queryParams: queryParams,
loading: false
});
}
}
handleChangPage = (pageIndex, pageSize) => {
let queryParams = this.state.queryParams;
queryParams.pageIndex = pageIndex;
queryParams.pageSize = pageSize;
this.setState({
queryParams: queryParams
});
this.loadTableData(queryParams).then(r => {
})
};
showDeleteConfirm(id, content) {
let self = this;
confirm({
title: '您确定要删除此用户组吗?',
content: content,
okText: '确定',
okType: 'danger',
cancelText: '取消',
onOk() {
self.delete(id);
}
});
};
showModal(title, user = {}) {
this.handleSearchByNickname('');
this.setState({
model: user,
modalVisible: true,
modalTitle: title
});
};
handleCancelModal = e => {
this.setState({
modalVisible: false,
modalTitle: ''
});
};
handleOk = async (formData) => {
// 弹窗 form 传来的数据
this.setState({
modalConfirmLoading: true
});
if (formData.id) {
// 向后台提交数据
const result = await request.put('/user-groups/' + formData.id, formData);
if (result.code === 1) {
message.success('操作成功', 3);
this.setState({
modalVisible: false
});
await this.loadTableData(this.state.queryParams);
} else {
message.error('操作失败 :( ' + result.message, 10);
}
} else {
// 向后台提交数据
const result = await request.post('/user-groups', formData);
if (result.code === 1) {
message.success('操作成功', 3);
this.setState({
modalVisible: false
});
await this.loadTableData(this.state.queryParams);
} else {
message.error('操作失败 :( ' + result.message, 10);
}
}
this.setState({
modalConfirmLoading: false
});
};
handleSearchByName = name => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'name': name,
}
this.loadTableData(query);
};
batchDelete = async () => {
this.setState({
delBtnLoading: true
})
try {
let result = await request.delete('/user-groups/' + this.state.selectedRowKeys.join(','));
if (result.code === 1) {
message.success('操作成功', 3);
this.setState({
selectedRowKeys: []
})
await this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
}
} finally {
this.setState({
delBtnLoading: false
})
}
}
handleSearchByNickname = async nickname => {
const result = await request.get(`/users/paging?pageIndex=1&pageSize=1000&nickname=${nickname}`);
if (result.code !== 1) {
message.error(result.message, 10);
return;
}
this.setState({
users: result.data.items
})
}
render() {
const columns = [{
title: '序号',
dataIndex: 'id',
key: 'id',
render: (id, record, index) => {
return index + 1;
}
}, {
title: '名称',
dataIndex: 'name',
}, {
title: '成员人数',
dataIndex: 'memberCount',
key: 'memberCount',
render: (text, record, index) => {
return <Button type='link' onClick={async () => {
await this.handleShowSharer(record, true);
}}>{text}</Button>
}
}, {
title: '创建日期',
dataIndex: 'created',
key: 'created'
},
{
title: '操作',
key: 'action',
render: (text, record) => {
return (
<div>
<Button type="link" size='small'
onClick={() => this.showModal('更新用户组', record)}>编辑</Button>
<Button type="link" size='small'
onClick={() => this.showDeleteConfirm(record.id, record.name)}>删除</Button>
</div>
)
},
}
];
const selectedRowKeys = this.state.selectedRowKeys;
const rowSelection = {
selectedRowKeys: this.state.selectedRowKeys,
onChange: (selectedRowKeys, selectedRows) => {
this.setState({selectedRowKeys});
},
};
const hasSelected = selectedRowKeys.length > 0;
return (
<>
<PageHeader
className="site-page-header-ghost-wrapper page-herder"
title="用户组管理"
breadcrumb={{
routes: routes,
itemRender: itemRender
}}
extra={[
<Logout key='logout'/>
]}
subTitle="平台用户管理"
>
</PageHeader>
<Content className="site-layout-background page-content">
<div style={{marginBottom: 20}}>
<Row justify="space-around" align="middle" gutter={24}>
<Col span={8} key={1}>
<Title level={3}>用户组列表</Title>
</Col>
<Col span={16} key={2} style={{textAlign: 'right'}}>
<Space>
<Search
ref={this.inputRefOfName}
placeholder="名称"
allowClear
onSearch={this.handleSearchByName}
/>
<Tooltip title='重置查询'>
<Button icon={<UndoOutlined/>} onClick={() => {
this.inputRefOfName.current.setValue('');
this.loadTableData({pageIndex: 1, pageSize: 10})
}}>
</Button>
</Tooltip>
<Divider type="vertical"/>
<Tooltip title="新增">
<Button type="dashed" icon={<PlusOutlined/>}
onClick={() => this.showModal('新增用户组', {})}>
</Button>
</Tooltip>
<Tooltip title="刷新列表">
<Button icon={<SyncOutlined/>} onClick={() => {
this.loadTableData(this.state.queryParams)
}}>
</Button>
</Tooltip>
<Tooltip title="批量删除">
<Button type="primary" danger disabled={!hasSelected} icon={<DeleteOutlined/>}
loading={this.state.delBtnLoading}
onClick={() => {
const content = <div>
您确定要删除选中的<Text style={{color: '#1890FF'}}
strong>{this.state.selectedRowKeys.length}</Text>
</div>;
confirm({
icon: <ExclamationCircleOutlined/>,
content: content,
onOk: () => {
this.batchDelete()
},
onCancel() {
},
});
}}>
</Button>
</Tooltip>
</Space>
</Col>
</Row>
</div>
<Table rowSelection={rowSelection}
dataSource={this.state.items}
columns={columns}
position={'both'}
pagination={{
showSizeChanger: true,
current: this.state.queryParams.pageIndex,
pageSize: this.state.queryParams.pageSize,
onChange: this.handleChangPage,
onShowSizeChange: this.handleChangPage,
total: this.state.total,
showTotal: total => `总计 ${total}`
}}
loading={this.state.loading}
/>
<UserGroupModal
visible={this.state.modalVisible}
title={this.state.modalTitle}
handleOk={this.handleOk}
handleCancel={this.handleCancelModal}
confirmLoading={this.state.modalConfirmLoading}
model={this.state.model}
users={this.state.users}
members={this.state.members}
>
</UserGroupModal>
</Content>
</>
);
}
}
export default UserGroup;

View File

@ -0,0 +1,69 @@
import React, {useState} from 'react';
import {Form, Input, Modal, Select} from "antd/lib/index";
const UserGroupModal = ({
title,
visible,
handleOk,
handleCancel,
confirmLoading,
model,
users,
members,
}) => {
const [form] = Form.useForm();
const formItemLayout = {
labelCol: {span: 6},
wrapperCol: {span: 14},
};
return (
<Modal
title={title}
visible={visible}
maskClosable={false}
onOk={() => {
form
.validateFields()
.then(values => {
form.resetFields();
handleOk(values);
})
.catch(info => {
});
}}
onCancel={handleCancel}
confirmLoading={confirmLoading}
okText='确定'
cancelText='取消'
>
<Form form={form} {...formItemLayout} initialValues={model}>
<Form.Item name='id' noStyle>
<Input hidden={true}/>
</Form.Item>
<Form.Item label="名称" name='name' rules={[{required: true, message: '请输入用户组名称'}]}>
<Input autoComplete="off" placeholder="请输入用户组名称"/>
</Form.Item>
<Form.Item label="用户组成员" name='members'>
<Select
// showSearch
mode="multiple"
allowClear
placeholder='用户组成员'
filterOption={false}
>
{users.map(d => <Select.Option key={d.id}
value={d.id}>{d.nickname}</Select.Option>)}
</Select>
</Form.Item>
</Form>
</Modal>
)
};
export default UserGroupModal;

View File

@ -19,7 +19,7 @@ const UserModal = ({title, visible, handleOk, handleCancel, confirmLoading, mode
form
.validateFields()
.then(values => {
// form.resetFields();
form.resetFields();
handleOk(values);
})
.catch(info => {