mirror of
https://github.com/casdoor/casdoor.git
synced 2025-05-23 02:35:49 +08:00
feat: Add tree structure to organization page (#1910)
* rebase master * feat: add group in userEditPage * feat: use id as the pk * feat: add groups item in user * feat: add tree component * rebase * feat: ui * fix: fix some bug * fix: route * fix: ui * fix: improve ui
This commit is contained in:
parent
ff87c4ea33
commit
0e14a2597e
@ -528,7 +528,7 @@ func (c *ApiController) Login() {
|
||||
}
|
||||
|
||||
properties := map[string]string{}
|
||||
count, err := object.GetUserCount(application.Organization, "", "")
|
||||
count, err := object.GetUserCount(application.Organization, "", "", "")
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
|
147
controllers/group.go
Normal file
147
controllers/group.go
Normal file
@ -0,0 +1,147 @@
|
||||
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed 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
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/beego/beego/utils/pagination"
|
||||
"github.com/casdoor/casdoor/object"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
// GetGroups
|
||||
// @Title GetGroups
|
||||
// @Tag Group API
|
||||
// @Description get groups
|
||||
// @Param owner query string true "The owner of groups"
|
||||
// @Success 200 {array} object.Group The Response object
|
||||
// @router /get-groups [get]
|
||||
func (c *ApiController) GetGroups() {
|
||||
owner := c.Input().Get("owner")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
value := c.Input().Get("value")
|
||||
sortField := c.Input().Get("sortField")
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
withTree := c.Input().Get("withTree")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
groups, err := object.GetGroups(owner)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
} else {
|
||||
if withTree == "true" {
|
||||
c.ResponseOk(object.ConvertToTreeData(groups, owner))
|
||||
return
|
||||
}
|
||||
c.ResponseOk(groups)
|
||||
}
|
||||
} else {
|
||||
limit := util.ParseInt(limit)
|
||||
count, err := object.GetGroupCount(owner, field, value)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
groups, err := object.GetPaginationGroups(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
} else {
|
||||
c.ResponseOk(groups, paginator.Nums())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetGroup
|
||||
// @Title GetGroup
|
||||
// @Tag Group API
|
||||
// @Description get group
|
||||
// @Param id query string true "The id ( owner/name ) of the group"
|
||||
// @Success 200 {object} object.Group The Response object
|
||||
// @router /get-group [get]
|
||||
func (c *ApiController) GetGroup() {
|
||||
id := c.Input().Get("id")
|
||||
|
||||
group, err := object.GetGroup(id)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
} else {
|
||||
c.ResponseOk(group)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateGroup
|
||||
// @Title UpdateGroup
|
||||
// @Tag Group API
|
||||
// @Description update group
|
||||
// @Param id query string true "The id ( owner/name ) of the group"
|
||||
// @Param body body object.Group true "The details of the group"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /update-group [post]
|
||||
func (c *ApiController) UpdateGroup() {
|
||||
id := c.Input().Get("id")
|
||||
|
||||
var group object.Group
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &group)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.UpdateGroup(id, &group))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// AddGroup
|
||||
// @Title AddGroup
|
||||
// @Tag Group API
|
||||
// @Description add group
|
||||
// @Param body body object.Group true "The details of the group"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /add-group [post]
|
||||
func (c *ApiController) AddGroup() {
|
||||
var group object.Group
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &group)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = wrapActionResponse(object.AddGroup(&group))
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// DeleteGroup
|
||||
// @Title DeleteGroup
|
||||
// @Tag Group API
|
||||
// @Description delete group
|
||||
// @Param body body object.Group true "The details of the group"
|
||||
// @Success 200 {object} controllers.Response The Response object
|
||||
// @router /delete-group [post]
|
||||
func (c *ApiController) DeleteGroup() {
|
||||
var group object.Group
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &group)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.ResponseOk(wrapActionResponse(object.DeleteGroup(&group)))
|
||||
}
|
@ -180,12 +180,12 @@ func (c *ApiController) GetDefaultApplication() {
|
||||
// @Title GetOrganizationNames
|
||||
// @Tag Organization API
|
||||
// @Param owner query string true "owner"
|
||||
// @Description get all organization names
|
||||
// @Description get all organization name and displayName
|
||||
// @Success 200 {array} object.Organization The Response object
|
||||
// @router /get-organization-names [get]
|
||||
func (c *ApiController) GetOrganizationNames() {
|
||||
owner := c.Input().Get("owner")
|
||||
organizationNames, err := object.GetOrganizationsByFields(owner, "name")
|
||||
organizationNames, err := object.GetOrganizationsByFields(owner, []string{"name", "display_name"}...)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
|
@ -80,6 +80,7 @@ func (c *ApiController) GetGlobalUsers() {
|
||||
// @router /get-users [get]
|
||||
func (c *ApiController) GetUsers() {
|
||||
owner := c.Input().Get("owner")
|
||||
groupId := c.Input().Get("groupId")
|
||||
limit := c.Input().Get("pageSize")
|
||||
page := c.Input().Get("p")
|
||||
field := c.Input().Get("field")
|
||||
@ -88,6 +89,16 @@ func (c *ApiController) GetUsers() {
|
||||
sortOrder := c.Input().Get("sortOrder")
|
||||
|
||||
if limit == "" || page == "" {
|
||||
if groupId != "" {
|
||||
maskedUsers, err := object.GetMaskedUsers(object.GetUsersByGroup(groupId))
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
c.ResponseOk(maskedUsers)
|
||||
return
|
||||
}
|
||||
|
||||
maskedUsers, err := object.GetMaskedUsers(object.GetUsers(owner))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@ -97,14 +108,14 @@ func (c *ApiController) GetUsers() {
|
||||
c.ServeJSON()
|
||||
} else {
|
||||
limit := util.ParseInt(limit)
|
||||
count, err := object.GetUserCount(owner, field, value)
|
||||
count, err := object.GetUserCount(owner, field, value, groupId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
paginator := pagination.SetPaginator(c.Ctx, limit, count)
|
||||
users, err := object.GetPaginationUsers(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
|
||||
users, err := object.GetPaginationUsers(owner, paginator.Offset(), limit, field, value, sortField, sortOrder, groupId)
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@ -287,7 +298,7 @@ func (c *ApiController) AddUser() {
|
||||
return
|
||||
}
|
||||
|
||||
count, err := object.GetUserCount("", "", "")
|
||||
count, err := object.GetUserCount("", "", "", "")
|
||||
if err != nil {
|
||||
c.ResponseError(err.Error())
|
||||
return
|
||||
@ -505,7 +516,7 @@ func (c *ApiController) GetUserCount() {
|
||||
var count int64
|
||||
var err error
|
||||
if isOnline == "" {
|
||||
count, err = object.GetUserCount(owner, "", "")
|
||||
count, err = object.GetUserCount(owner, "", "", "")
|
||||
} else {
|
||||
count, err = object.GetOnlineUserCount(owner, util.ParseInt(isOnline))
|
||||
}
|
||||
|
@ -140,6 +140,16 @@ func (a *Adapter) createTable() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = a.Engine.Sync2(new(Group))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = a.Engine.Sync2(new(UserGroupRelation))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = a.Engine.Sync2(new(Role))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
193
object/group.go
Normal file
193
object/group.go
Normal file
@ -0,0 +1,193 @@
|
||||
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed 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.
|
||||
|
||||
package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
"github.com/xorm-io/core"
|
||||
)
|
||||
|
||||
type Group struct {
|
||||
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
|
||||
Name string `xorm:"varchar(100) notnull pk unique" json:"name"`
|
||||
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
|
||||
UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"`
|
||||
|
||||
Id string `xorm:"varchar(100) not null index" json:"id"`
|
||||
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
||||
Manager string `xorm:"varchar(100)" json:"manager"`
|
||||
ContactEmail string `xorm:"varchar(100)" json:"contactEmail"`
|
||||
Type string `xorm:"varchar(100)" json:"type"`
|
||||
ParentGroupId string `xorm:"varchar(100)" json:"parentGroupId"`
|
||||
IsTopGroup bool `xorm:"bool" json:"isTopGroup"`
|
||||
Users *[]string `xorm:"-" json:"users"`
|
||||
|
||||
Title string `json:"title,omitempty"`
|
||||
Key string `json:"key,omitempty"`
|
||||
Children []*Group `json:"children,omitempty"`
|
||||
|
||||
IsEnabled bool `json:"isEnabled"`
|
||||
}
|
||||
|
||||
type GroupNode struct{}
|
||||
|
||||
func GetGroupCount(owner, field, value string) (int64, error) {
|
||||
session := GetSession(owner, -1, -1, field, value, "", "")
|
||||
count, err := session.Count(&Group{})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func GetGroups(owner string) ([]*Group, error) {
|
||||
groups := []*Group{}
|
||||
err := adapter.Engine.Desc("created_time").Find(&groups, &Group{Owner: owner})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
func GetPaginationGroups(owner string, offset, limit int, field, value, sortField, sortOrder string) ([]*Group, error) {
|
||||
groups := []*Group{}
|
||||
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
|
||||
err := session.Find(&groups)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
func getGroup(owner string, name string) (*Group, error) {
|
||||
if owner == "" || name == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
group := Group{Owner: owner, Name: name}
|
||||
existed, err := adapter.Engine.Get(&group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if existed {
|
||||
return &group, nil
|
||||
} else {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func getGroupById(id string) (*Group, error) {
|
||||
if id == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
group := Group{Id: id}
|
||||
existed, err := adapter.Engine.Get(&group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if existed {
|
||||
return &group, nil
|
||||
} else {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetGroup(id string) (*Group, error) {
|
||||
owner, name := util.GetOwnerAndNameFromId(id)
|
||||
return getGroup(owner, name)
|
||||
}
|
||||
|
||||
func UpdateGroup(id string, group *Group) (bool, error) {
|
||||
owner, name := util.GetOwnerAndNameFromId(id)
|
||||
oldGroup, err := getGroup(owner, name)
|
||||
if oldGroup == nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
group.UpdatedTime = util.GetCurrentTime()
|
||||
affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(group)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
func AddGroup(group *Group) (bool, error) {
|
||||
if group.Id == "" {
|
||||
group.Id = util.GenerateId()
|
||||
}
|
||||
|
||||
affected, err := adapter.Engine.Insert(group)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
func AddGroups(groups []*Group) (bool, error) {
|
||||
if len(groups) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
affected, err := adapter.Engine.Insert(groups)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
func DeleteGroup(group *Group) (bool, error) {
|
||||
affected, err := adapter.Engine.ID(core.PK{group.Owner, group.Name}).Delete(&Group{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
func (group *Group) GetId() string {
|
||||
return fmt.Sprintf("%s/%s", group.Owner, group.Name)
|
||||
}
|
||||
|
||||
func ConvertToTreeData(groups []*Group, parentGroupId string) []*Group {
|
||||
treeData := []*Group{}
|
||||
|
||||
for _, group := range groups {
|
||||
if group.ParentGroupId == parentGroupId {
|
||||
node := &Group{
|
||||
Title: group.DisplayName,
|
||||
Key: group.Name,
|
||||
Type: group.Type,
|
||||
Owner: group.Owner,
|
||||
Id: group.Id,
|
||||
}
|
||||
children := ConvertToTreeData(groups, group.Id)
|
||||
if len(children) > 0 {
|
||||
node.Children = children
|
||||
}
|
||||
treeData = append(treeData, node)
|
||||
}
|
||||
}
|
||||
return treeData
|
||||
}
|
@ -61,6 +61,7 @@ func getBuiltInAccountItems() []*AccountItem {
|
||||
{Name: "Signup application", Visible: true, ViewRule: "Public", ModifyRule: "Admin"},
|
||||
{Name: "Roles", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"},
|
||||
{Name: "Permissions", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"},
|
||||
{Name: "Groups", Visible: true, ViewRule: "Public", ModifyRule: "Immutable"},
|
||||
{Name: "3rd-party logins", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
|
||||
{Name: "Properties", Visible: false, ViewRule: "Admin", ModifyRule: "Admin"},
|
||||
{Name: "Is admin", Visible: true, ViewRule: "Admin", ModifyRule: "Admin"},
|
||||
|
@ -334,6 +334,13 @@ func organizationChangeTrigger(oldName string, newName string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
group := new(Group)
|
||||
group.Owner = newName
|
||||
_, err = session.Where("owner=?", oldName).Update(group)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
role := new(Role)
|
||||
_, err = adapter.Engine.Where("owner=?", oldName).Get(role)
|
||||
if err != nil {
|
||||
|
@ -77,6 +77,7 @@ type User struct {
|
||||
SignupApplication string `xorm:"varchar(100)" json:"signupApplication"`
|
||||
Hash string `xorm:"varchar(100)" json:"hash"`
|
||||
PreHash string `xorm:"varchar(100)" json:"preHash"`
|
||||
Groups []string `xorm:"varchar(1000)" json:"groups"`
|
||||
|
||||
CreatedIp string `xorm:"varchar(100)" json:"createdIp"`
|
||||
LastSigninTime string `xorm:"varchar(100)" json:"lastSigninTime"`
|
||||
@ -218,8 +219,20 @@ func GetPaginationGlobalUsers(offset, limit int, field, value, sortField, sortOr
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func GetUserCount(owner, field, value string) (int64, error) {
|
||||
func GetUserCount(owner, field, value string, groupId string) (int64, error) {
|
||||
session := GetSession(owner, -1, -1, field, value, "", "")
|
||||
|
||||
if groupId != "" {
|
||||
group, err := GetGroup(groupId)
|
||||
if group == nil || err != nil {
|
||||
return 0, err
|
||||
}
|
||||
// users count in group
|
||||
return adapter.Engine.Table("user_group_relation").Join("INNER", "user AS u", "user_group_relation.user_id = u.id").
|
||||
Where("user_group_relation.group_id = ?", group.Id).
|
||||
Count(&UserGroupRelation{})
|
||||
}
|
||||
|
||||
return session.Count(&User{})
|
||||
}
|
||||
|
||||
@ -257,13 +270,47 @@ func GetSortedUsers(owner string, sorter string, limit int) ([]*User, error) {
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func GetPaginationUsers(owner string, offset, limit int, field, value, sortField, sortOrder string) ([]*User, error) {
|
||||
func GetPaginationUsers(owner string, offset, limit int, field, value, sortField, sortOrder string, groupId string) ([]*User, error) {
|
||||
users := []*User{}
|
||||
|
||||
if groupId != "" {
|
||||
group, err := GetGroup(groupId)
|
||||
if group == nil || err != nil {
|
||||
return []*User{}, err
|
||||
}
|
||||
|
||||
session := adapter.Engine.Prepare()
|
||||
if offset != -1 && limit != -1 {
|
||||
session.Limit(limit, offset)
|
||||
}
|
||||
|
||||
err = session.Table("user_group_relation").Join("INNER", "user AS u", "user_group_relation.user_id = u.id").
|
||||
Where("user_group_relation.group_id = ?", group.Id).
|
||||
Find(&users)
|
||||
return users, err
|
||||
}
|
||||
|
||||
session := GetSessionForUser(owner, offset, limit, field, value, sortField, sortOrder)
|
||||
err := session.Find(&users)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func GetUsersByGroup(groupId string) ([]*User, error) {
|
||||
group, err := GetGroup(groupId)
|
||||
if group == nil || err != nil {
|
||||
return []*User{}, err
|
||||
}
|
||||
|
||||
users := []*User{}
|
||||
err = adapter.Engine.Table("user_group_relation").Join("INNER", "user AS u", "user_group_relation.user_id = u.id").
|
||||
Where("user_group_relation.group_id = ?", group.Id).
|
||||
Find(&users)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
@ -479,7 +526,7 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
|
||||
"owner", "display_name", "avatar",
|
||||
"location", "address", "country_code", "region", "language", "affiliation", "title", "homepage", "bio", "tag", "language", "gender", "birthday", "education", "score", "karma", "ranking", "signup_application",
|
||||
"is_admin", "is_global_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts",
|
||||
"signin_wrong_times", "last_signin_wrong_time",
|
||||
"signin_wrong_times", "last_signin_wrong_time", "groups",
|
||||
"github", "google", "qq", "wechat", "facebook", "dingtalk", "weibo", "gitee", "linkedin", "wecom", "lark", "gitlab", "adfs",
|
||||
"baidu", "alipay", "casdoor", "infoflow", "apple", "azuread", "slack", "steam", "bilibili", "okta", "douyin", "line", "amazon",
|
||||
"auth0", "battlenet", "bitbucket", "box", "cloudfoundry", "dailymotion", "deezer", "digitalocean", "discord", "dropbox",
|
||||
@ -493,7 +540,7 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
|
||||
columns = append(columns, "name", "email", "phone", "country_code")
|
||||
}
|
||||
|
||||
affected, err := adapter.Engine.ID(core.PK{owner, name}).Cols(columns...).Update(user)
|
||||
affected, err := updateUser(oldUser, user, columns)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -501,6 +548,35 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
|
||||
return affected != 0, nil
|
||||
}
|
||||
|
||||
func updateUser(oldUser, user *User, columns []string) (int64, error) {
|
||||
session := adapter.Engine.NewSession()
|
||||
defer session.Close()
|
||||
|
||||
session.Begin()
|
||||
|
||||
if util.ContainsString(columns, "groups") {
|
||||
affected, err := updateGroupRelation(session, user)
|
||||
if err != nil {
|
||||
session.Rollback()
|
||||
return affected, err
|
||||
}
|
||||
}
|
||||
|
||||
affected, err := session.ID(core.PK{oldUser.Owner, oldUser.Name}).Cols(columns...).Update(user)
|
||||
if err != nil {
|
||||
session.Rollback()
|
||||
return affected, err
|
||||
}
|
||||
|
||||
err = session.Commit()
|
||||
if err != nil {
|
||||
session.Rollback()
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return affected, nil
|
||||
}
|
||||
|
||||
func UpdateUserForAllFields(id string, user *User) (bool, error) {
|
||||
var err error
|
||||
owner, name := util.GetOwnerAndNameFromId(id)
|
||||
@ -580,7 +656,7 @@ func AddUser(user *User) (bool, error) {
|
||||
}
|
||||
}
|
||||
|
||||
count, err := GetUserCount(user.Owner, "", "")
|
||||
count, err := GetUserCount(user.Owner, "", "", "")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
52
object/user_group.go
Normal file
52
object/user_group.go
Normal file
@ -0,0 +1,52 @@
|
||||
package object
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/xorm-io/xorm"
|
||||
)
|
||||
|
||||
type UserGroupRelation struct {
|
||||
UserId string `xorm:"varchar(100) notnull pk" json:"userId"`
|
||||
GroupId string `xorm:"varchar(100) notnull pk" json:"groupId"`
|
||||
|
||||
CreatedTime string `xorm:"created" json:"createdTime"`
|
||||
UpdatedTime string `xorm:"updated" json:"updatedTime"`
|
||||
}
|
||||
|
||||
func updateGroupRelation(session *xorm.Session, user *User) (int64, error) {
|
||||
groupIds := user.Groups
|
||||
|
||||
physicalGroupCount, err := session.Where("type = ?", "Physical").In("id", user.Groups).Count(Group{})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if physicalGroupCount > 1 {
|
||||
return 0, errors.New("user can only be in one physical group")
|
||||
}
|
||||
|
||||
groups := []*Group{}
|
||||
err = session.In("id", groupIds).Find(&groups)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(groups) == 0 || len(groups) != len(groupIds) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
_, err = session.Delete(&UserGroupRelation{UserId: user.Id})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
relations := []*UserGroupRelation{}
|
||||
for _, group := range groups {
|
||||
relations = append(relations, &UserGroupRelation{UserId: user.Id, GroupId: group.Id})
|
||||
}
|
||||
_, err = session.Insert(relations)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return 1, nil
|
||||
}
|
@ -295,6 +295,13 @@ func CheckPermissionForUpdateUser(oldUser, newUser *User, isAdmin bool, lang str
|
||||
itemsChanged = append(itemsChanged, item)
|
||||
}
|
||||
|
||||
oldUserGroupsJson, _ := json.Marshal(oldUser.Groups)
|
||||
newUserGroupsJson, _ := json.Marshal(newUser.Groups)
|
||||
if string(oldUserGroupsJson) != string(newUserGroupsJson) {
|
||||
item := GetAccountItemByName("Groups", organization)
|
||||
itemsChanged = append(itemsChanged, item)
|
||||
}
|
||||
|
||||
if oldUser.IsAdmin != newUser.IsAdmin {
|
||||
item := GetAccountItemByName("Is admin", organization)
|
||||
itemsChanged = append(itemsChanged, item)
|
||||
|
@ -77,6 +77,12 @@ func initAPI() {
|
||||
beego.Router("/api/delete-user", &controllers.ApiController{}, "POST:DeleteUser")
|
||||
beego.Router("/api/upload-users", &controllers.ApiController{}, "POST:UploadUsers")
|
||||
|
||||
beego.Router("/api/get-groups", &controllers.ApiController{}, "GET:GetGroups")
|
||||
beego.Router("/api/get-group", &controllers.ApiController{}, "GET:GetGroup")
|
||||
beego.Router("/api/update-group", &controllers.ApiController{}, "POST:UpdateGroup")
|
||||
beego.Router("/api/add-group", &controllers.ApiController{}, "POST:AddGroup")
|
||||
beego.Router("/api/delete-group", &controllers.ApiController{}, "POST:DeleteGroup")
|
||||
|
||||
beego.Router("/api/get-roles", &controllers.ApiController{}, "GET:GetRoles")
|
||||
beego.Router("/api/get-role", &controllers.ApiController{}, "GET:GetRole")
|
||||
beego.Router("/api/update-role", &controllers.ApiController{}, "POST:UpdateRole")
|
||||
|
@ -15,6 +15,9 @@
|
||||
import React, {Component} from "react";
|
||||
import "./App.less";
|
||||
import {Helmet} from "react-helmet";
|
||||
import GroupTreePage from "./GroupTreePage";
|
||||
import GroupEditPage from "./GroupEdit";
|
||||
import GroupListPage from "./GroupList";
|
||||
import * as Setting from "./Setting";
|
||||
import {StyleProvider, legacyLogicalPropertiesTransformer} from "@ant-design/cssinjs";
|
||||
import {BarsOutlined, CommentOutlined, DownOutlined, InfoCircleFilled, LogoutOutlined, SettingOutlined} from "@ant-design/icons";
|
||||
@ -132,6 +135,8 @@ class App extends Component {
|
||||
this.setState({selectedMenuKey: "/organizations"});
|
||||
} else if (uri.includes("/users")) {
|
||||
this.setState({selectedMenuKey: "/users"});
|
||||
} else if (uri.includes("/groups")) {
|
||||
this.setState({selectedMenuKey: "/groups"});
|
||||
} else if (uri.includes("/roles")) {
|
||||
this.setState({selectedMenuKey: "/roles"});
|
||||
} else if (uri.includes("/permissions")) {
|
||||
@ -408,6 +413,9 @@ class App extends Component {
|
||||
if (Setting.isAdminUser(this.state.account)) {
|
||||
res.push(Setting.getItem(<Link to="/organizations">{i18next.t("general:Organizations")}</Link>,
|
||||
"/organizations"));
|
||||
|
||||
res.push(Setting.getItem(<Link to="/groups">{i18next.t("general:Groups")}</Link>,
|
||||
"/groups"));
|
||||
}
|
||||
|
||||
if (Setting.isLocalAdminUser(this.state.account)) {
|
||||
@ -552,6 +560,10 @@ class App extends Component {
|
||||
<Route exact path="/organizations" render={(props) => this.renderLoginIfNotLoggedIn(<OrganizationListPage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/organizations/:organizationName" render={(props) => this.renderLoginIfNotLoggedIn(<OrganizationEditPage account={this.state.account} onChangeTheme={this.setTheme} {...props} />)} />
|
||||
<Route exact path="/organizations/:organizationName/users" render={(props) => this.renderLoginIfNotLoggedIn(<UserListPage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/group-tree/:organizationName" render={(props) => this.renderLoginIfNotLoggedIn(<GroupTreePage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/group-tree/:organizationName/:groupName" render={(props) => this.renderLoginIfNotLoggedIn(<GroupTreePage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/groups" render={(props) => this.renderLoginIfNotLoggedIn(<GroupListPage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/groups/:organizationName/:groupName" render={(props) => this.renderLoginIfNotLoggedIn(<GroupEditPage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/users" render={(props) => this.renderLoginIfNotLoggedIn(<UserListPage account={this.state.account} {...props} />)} />
|
||||
<Route exact path="/users/:organizationName/:userName" render={(props) => <UserEditPage account={this.state.account} {...props} />} />
|
||||
<Route exact path="/roles" render={(props) => this.renderLoginIfNotLoggedIn(<RoleListPage account={this.state.account} {...props} />)} />
|
||||
@ -618,6 +630,11 @@ class App extends Component {
|
||||
});
|
||||
};
|
||||
|
||||
isWithoutCard() {
|
||||
return Setting.isMobile() || window.location.pathname === "/chat" ||
|
||||
window.location.pathname.startsWith("/group-tree");
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
const onClick = ({key}) => {
|
||||
if (key === "/swagger") {
|
||||
@ -628,7 +645,6 @@ class App extends Component {
|
||||
};
|
||||
return (
|
||||
<Layout id="parent-area">
|
||||
{/* https://github.com/ant-design/ant-design/issues/40394 ant design bug. If it will be fixed, we can delete the code for control the color of Header*/}
|
||||
<Header style={{padding: "0", marginBottom: "3px", backgroundColor: this.state.themeAlgorithm.includes("dark") ? "black" : "white"}}>
|
||||
{Setting.isMobile() ? null : (
|
||||
<Link to={"/"}>
|
||||
@ -664,7 +680,7 @@ class App extends Component {
|
||||
}
|
||||
</Header>
|
||||
<Content style={{display: "flex", flexDirection: "column"}} >
|
||||
{(Setting.isMobile() || window.location.pathname === "/chat") ?
|
||||
{this.isWithoutCard() ?
|
||||
this.renderRouter() :
|
||||
<Card className="content-warp-card">
|
||||
{this.renderRouter()}
|
||||
|
@ -74,7 +74,6 @@ img {
|
||||
|
||||
.content-warp-card {
|
||||
box-shadow: 0 1px 5px 0 rgb(51 51 51 / 14%);
|
||||
margin: 5px;
|
||||
flex: 1;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
263
web/src/GroupEdit.js
Normal file
263
web/src/GroupEdit.js
Normal file
@ -0,0 +1,263 @@
|
||||
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed 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 {Button, Card, Col, Input, Row, Select, Switch} from "antd";
|
||||
import * as GroupBackend from "./backend/GroupBackend";
|
||||
import * as OrganizationBackend from "./backend/OrganizationBackend";
|
||||
import * as Setting from "./Setting";
|
||||
import i18next from "i18next";
|
||||
|
||||
class GroupEditPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
groupName: props.match.params.groupName,
|
||||
organizationName: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
|
||||
group: null,
|
||||
users: [],
|
||||
groups: [],
|
||||
organizations: [],
|
||||
mode: props.location.mode !== undefined ? props.location.mode : "edit",
|
||||
};
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.getGroup();
|
||||
this.getGroups(this.state.organizationName);
|
||||
this.getOrganizations();
|
||||
}
|
||||
|
||||
getGroup() {
|
||||
GroupBackend.getGroup(this.state.organizationName, this.state.groupName)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
group: res.data,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getGroups(organizationName) {
|
||||
GroupBackend.getGroups(organizationName)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
groups: res.data,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getOrganizations() {
|
||||
OrganizationBackend.getOrganizationNames("admin")
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
organizations: res.data,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
parseGroupField(key, value) {
|
||||
if ([""].includes(key)) {
|
||||
value = Setting.myParseInt(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
updateGroupField(key, value) {
|
||||
value = this.parseGroupField(key, value);
|
||||
|
||||
const group = this.state.group;
|
||||
group[key] = value;
|
||||
this.setState({
|
||||
group: group,
|
||||
});
|
||||
}
|
||||
|
||||
getParentIdOptions() {
|
||||
const groups = this.state.groups.filter((group) => group.id !== this.state.group.id);
|
||||
const organization = this.state.organizations.find((organization) => organization.name === this.state.group.owner);
|
||||
if (organization !== undefined) {
|
||||
groups.push({id: organization.name, displayName: organization.displayName});
|
||||
}
|
||||
return groups.map((group) => ({label: group.displayName, value: group.id}));
|
||||
}
|
||||
|
||||
renderGroup() {
|
||||
return (
|
||||
<Card size="small" title={
|
||||
<div>
|
||||
{this.state.mode === "add" ? i18next.t("group:New Group") : i18next.t("group:Edit Group")}
|
||||
<Button onClick={() => this.submitGroupEdit(false)}>{i18next.t("general:Save")}</Button>
|
||||
<Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitGroupEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
||||
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteGroup()}>{i18next.t("general:Cancel")}</Button> : null}
|
||||
</div>
|
||||
}
|
||||
style={(Setting.isMobile()) ? {margin: "5px"} : {}}
|
||||
type="inner"
|
||||
>
|
||||
<Row style={{marginTop: "10px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} style={{width: "100%"}} disabled={!Setting.isAdminUser(this.props.account)} value={this.state.group.owner}
|
||||
onChange={(value => {
|
||||
this.updateGroupField("owner", value);
|
||||
this.getGroups(value);
|
||||
})}
|
||||
options={this.state.organizations.map((organization) => Setting.getOption(organization.displayName, organization.name))
|
||||
} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.group.name} onChange={e => {
|
||||
this.updateGroupField("name", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input value={this.state.group.displayName} onChange={e => {
|
||||
this.updateGroupField("displayName", e.target.value);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Type"), i18next.t("general:Type - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select style={{width: "100%"}}
|
||||
options={
|
||||
[
|
||||
{label: i18next.t("group:Virtual"), value: "Virtual"},
|
||||
{label: i18next.t("group:Physical"), value: "Physical"},
|
||||
].map((item) => ({label: item.label, value: item.value}))
|
||||
}
|
||||
value={this.state.group.type} onChange={(value => {
|
||||
this.updateGroupField("type", value);
|
||||
}
|
||||
)} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("group:Parent group"), i18next.t("group:Parent group - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select style={{width: "100%"}}
|
||||
options={this.getParentIdOptions()}
|
||||
value={this.state.group.parentGroupId} onChange={(value => {
|
||||
this.updateGroupField("parentGroupId", value);
|
||||
}
|
||||
)} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 19 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Is enabled"), i18next.t("general:Is enabled - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={1} >
|
||||
<Switch checked={this.state.group.isEnabled} onChange={checked => {
|
||||
this.updateGroupField("isEnabled", checked);
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
submitGroupEdit(willExist) {
|
||||
const group = Setting.deepCopy(this.state.group);
|
||||
group["isTopGroup"] = this.state.organizations.some((organization) => organization.name === group.parentGroupId);
|
||||
|
||||
GroupBackend.updateGroup(this.state.organizationName, this.state.groupName, group)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("general:Successfully saved"));
|
||||
this.setState({
|
||||
groupName: this.state.group.name,
|
||||
});
|
||||
|
||||
if (willExist) {
|
||||
const groupTreeUrl = sessionStorage.getItem("groupTreeUrl");
|
||||
if (groupTreeUrl !== null) {
|
||||
sessionStorage.removeItem("groupTreeUrl");
|
||||
this.props.history.push(groupTreeUrl);
|
||||
} else {
|
||||
this.props.history.push("/groups");
|
||||
}
|
||||
} else {
|
||||
this.props.history.push(`/groups/${this.state.group.owner}/${this.state.group.name}`);
|
||||
}
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to save")}: ${res.msg}`);
|
||||
this.updateGroupField("name", this.state.groupName);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
deleteGroup() {
|
||||
GroupBackend.deleteGroup(this.state.group)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
const groupTreeUrl = sessionStorage.getItem("groupTreeUrl");
|
||||
if (groupTreeUrl !== null) {
|
||||
sessionStorage.removeItem("groupTreeUrl");
|
||||
this.props.history.push(groupTreeUrl);
|
||||
} else {
|
||||
this.props.history.push("/groups");
|
||||
}
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
this.state.group !== null ? this.renderGroup() : null
|
||||
}
|
||||
<div style={{marginTop: "20px", marginLeft: "40px"}}>
|
||||
<Button size="large" onClick={() => this.submitGroupEdit(false)}>{i18next.t("general:Save")}</Button>
|
||||
<Button style={{marginLeft: "20px"}} type="primary" size="large" onClick={() => this.submitGroupEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
|
||||
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} size="large" onClick={() => this.deleteGroup()}>{i18next.t("general:Cancel")}</Button> : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default GroupEditPage;
|
284
web/src/GroupList.js
Normal file
284
web/src/GroupList.js
Normal file
@ -0,0 +1,284 @@
|
||||
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed 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 {Link} from "react-router-dom";
|
||||
import {Button, Table} from "antd";
|
||||
import moment from "moment";
|
||||
import * as Setting from "./Setting";
|
||||
import * as GroupBackend from "./backend/GroupBackend";
|
||||
import i18next from "i18next";
|
||||
import BaseListPage from "./BaseListPage";
|
||||
import PopconfirmModal from "./common/modal/PopconfirmModal";
|
||||
|
||||
class GroupListPage extends BaseListPage {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
...this.state,
|
||||
owner: Setting.isAdminUser(this.props.account) ? "" : this.props.account.owner,
|
||||
groups: [],
|
||||
};
|
||||
}
|
||||
UNSAFE_componentWillMount() {
|
||||
super.UNSAFE_componentWillMount();
|
||||
this.getGroups(this.state.owner);
|
||||
}
|
||||
|
||||
getGroups(organizationName) {
|
||||
GroupBackend.getGroups(organizationName)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
groups: res.data,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
newGroup() {
|
||||
const randomName = Setting.getRandomName();
|
||||
return {
|
||||
owner: this.props.account.owner,
|
||||
name: `group_${randomName}`,
|
||||
createdTime: moment().format(),
|
||||
updatedTime: moment().format(),
|
||||
displayName: `New Group - ${randomName}`,
|
||||
type: "Virtual",
|
||||
parentGroupId: this.props.account.owner,
|
||||
isTopGroup: true,
|
||||
isEnabled: true,
|
||||
};
|
||||
}
|
||||
|
||||
addGroup() {
|
||||
const newGroup = this.newGroup();
|
||||
GroupBackend.addGroup(newGroup)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.props.history.push({pathname: `/groups/${newGroup.owner}/${newGroup.name}`, mode: "add"});
|
||||
Setting.showMessage("success", i18next.t("general:Successfully added"));
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
deleteGroup(i) {
|
||||
GroupBackend.deleteGroup(this.state.data[i])
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
|
||||
this.setState({
|
||||
data: Setting.deleteRow(this.state.data, i),
|
||||
pagination: {total: this.state.pagination.total - 1},
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
renderTable(groups) {
|
||||
const columns = [
|
||||
{
|
||||
title: i18next.t("general:Name"),
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: "120px",
|
||||
fixed: "left",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("name"),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/groups/${record.owner}/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Organization"),
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
width: "120px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("owner"),
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Link to={`/organizations/${text}`}>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Created time"),
|
||||
dataIndex: "createdTime",
|
||||
key: "createdTime",
|
||||
width: "150px",
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
return Setting.getFormattedDate(text);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Updated time"),
|
||||
dataIndex: "updatedTime",
|
||||
key: "updatedTime",
|
||||
width: "150px",
|
||||
sorter: true,
|
||||
render: (text, record, index) => {
|
||||
return Setting.getFormattedDate(text);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Display name"),
|
||||
dataIndex: "displayName",
|
||||
key: "displayName",
|
||||
width: "100px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("displayName"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Type"),
|
||||
dataIndex: "type",
|
||||
key: "type",
|
||||
width: "110px",
|
||||
sorter: true,
|
||||
filterMultiple: false,
|
||||
filters: [
|
||||
{text: i18next.t("group:Virtual"), value: "Virtual"},
|
||||
{text: i18next.t("group:Physical"), value: "Physical"},
|
||||
],
|
||||
render: (text, record, index) => {
|
||||
return i18next.t("group:" + text);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("group:Parent group"),
|
||||
dataIndex: "parentGroupId",
|
||||
key: "parentGroupId",
|
||||
width: "110px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("parentGroupId"),
|
||||
render: (text, record, index) => {
|
||||
if (record.isTopGroup) {
|
||||
return <Link to={`/organizations/${record.parentGroupId}`}>
|
||||
{record.parentGroupId}
|
||||
</Link>;
|
||||
}
|
||||
const parentGroup = this.state.groups.find((group) => group.id === text);
|
||||
if (parentGroup === undefined) {
|
||||
return "";
|
||||
}
|
||||
return <Link to={`/groups/${parentGroup.owner}/${parentGroup.name}`}>
|
||||
{parentGroup?.displayName}
|
||||
</Link>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Action"),
|
||||
dataIndex: "",
|
||||
key: "op",
|
||||
width: "170px",
|
||||
fixed: (Setting.isMobile()) ? "false" : "right",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/groups/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
|
||||
<PopconfirmModal
|
||||
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
|
||||
onConfirm={() => this.deleteGroup(index)}
|
||||
>
|
||||
</PopconfirmModal>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const paginationProps = {
|
||||
total: this.state.pagination.total,
|
||||
showQuickJumper: true,
|
||||
showSizeChanger: true,
|
||||
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.pagination.total),
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table scroll={{x: "max-content"}} columns={columns} dataSource={groups} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
|
||||
title={() => (
|
||||
<div>
|
||||
{i18next.t("general:Groups")}
|
||||
<Button type="primary" size="small" onClick={this.addGroup.bind(this)}>{i18next.t("general:Add")}</Button>
|
||||
</div>
|
||||
)}
|
||||
loading={this.state.loading}
|
||||
onChange={this.handleTableChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
fetch = (params = {}) => {
|
||||
let field = params.searchedColumn, value = params.searchText;
|
||||
const sortField = params.sortField, sortOrder = params.sortOrder;
|
||||
if (params.category !== undefined && params.category !== null) {
|
||||
field = "category";
|
||||
value = params.category;
|
||||
} else if (params.type !== undefined && params.type !== null) {
|
||||
field = "type";
|
||||
value = params.type;
|
||||
}
|
||||
this.setState({loading: true});
|
||||
GroupBackend.getGroups(this.state.owner, false, params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
});
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
data: res.data,
|
||||
pagination: {
|
||||
...params.pagination,
|
||||
total: res.data2,
|
||||
},
|
||||
searchText: params.searchText,
|
||||
searchedColumn: params.searchedColumn,
|
||||
});
|
||||
} else {
|
||||
if (Setting.isResponseDenied(res)) {
|
||||
this.setState({
|
||||
isAuthorized: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
});
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default GroupListPage;
|
312
web/src/GroupTreePage.js
Normal file
312
web/src/GroupTreePage.js
Normal file
@ -0,0 +1,312 @@
|
||||
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed 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 {DeleteOutlined, EditOutlined, HolderOutlined, PlusOutlined, UsergroupAddOutlined} from "@ant-design/icons";
|
||||
import {Button, Col, Empty, Row, Space, Tree} from "antd";
|
||||
import i18next from "i18next";
|
||||
import moment from "moment/moment";
|
||||
import React from "react";
|
||||
import * as GroupBackend from "./backend/GroupBackend";
|
||||
import * as Setting from "./Setting";
|
||||
import OrganizationSelect from "./common/select/OrganizationSelect";
|
||||
import UserListPage from "./UserListPage";
|
||||
|
||||
class GroupTreePage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
organizationName: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
|
||||
groupName: this.props.match?.params.groupName,
|
||||
groupId: "",
|
||||
treeData: [],
|
||||
selectedKeys: [],
|
||||
};
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.getTreeData();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState, snapshot) {
|
||||
if (this.state.organizationName !== prevState.organizationName) {
|
||||
this.getTreeData();
|
||||
}
|
||||
|
||||
if (prevState.treeData !== this.state.treeData) {
|
||||
this.setTreeExpandedKeys();
|
||||
}
|
||||
}
|
||||
|
||||
getTreeData() {
|
||||
GroupBackend.getGroups(this.state.organizationName, true).then((res) => {
|
||||
if (res.status === "ok") {
|
||||
const tree = res.data;
|
||||
this.setState({
|
||||
treeData: tree,
|
||||
});
|
||||
} else {
|
||||
Setting.showMessage("error", res.msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setTreeTitle(treeData) {
|
||||
const haveChildren = Array.isArray(treeData.children) && treeData.children.length > 0;
|
||||
const isSelected = this.state.groupName === treeData.key;
|
||||
return {
|
||||
id: treeData.id,
|
||||
key: treeData.key,
|
||||
title: <Space>
|
||||
{treeData.type === "Physical" ? <UsergroupAddOutlined /> : <HolderOutlined />}
|
||||
<span>{treeData.title}</span>
|
||||
{isSelected && (
|
||||
<React.Fragment>
|
||||
<PlusOutlined
|
||||
style={{
|
||||
visibility: "visible",
|
||||
color: "inherit",
|
||||
transition: "color 0.3s",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.color = "rgba(89,54,213,0.6)";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.color = "inherit";
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
e.currentTarget.style.color = "rgba(89,54,213,0.4)";
|
||||
}}
|
||||
onMouseUp={(e) => {
|
||||
e.currentTarget.style.color = "rgba(89,54,213,0.6)";
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
sessionStorage.setItem("groupTreeUrl", window.location.pathname);
|
||||
this.addGroup();
|
||||
}}
|
||||
/>
|
||||
<EditOutlined
|
||||
style={{
|
||||
visibility: "visible",
|
||||
color: "inherit",
|
||||
transition: "color 0.3s",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.color = "rgba(89,54,213,0.6)";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.color = "inherit";
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
e.currentTarget.style.color = "rgba(89,54,213,0.4)";
|
||||
}}
|
||||
onMouseUp={(e) => {
|
||||
e.currentTarget.style.color = "rgba(89,54,213,0.6)";
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
sessionStorage.setItem("groupTreeUrl", window.location.pathname);
|
||||
this.props.history.push(`/groups/${this.state.organizationName}/${treeData.key}`);
|
||||
}}
|
||||
/>
|
||||
<DeleteOutlined
|
||||
style={{
|
||||
visibility: "visible",
|
||||
color: "inherit",
|
||||
transition: "color 0.3s",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.color = "rgba(89,54,213,0.6)";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.color = "inherit";
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
e.currentTarget.style.color = "rgba(89,54,213,0.4)";
|
||||
}}
|
||||
onMouseUp={(e) => {
|
||||
e.currentTarget.style.color = "rgba(89,54,213,0.6)";
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
GroupBackend.deleteGroup({owner: treeData.owner, name: treeData.key})
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
|
||||
this.getTreeData();
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</Space>,
|
||||
children: haveChildren ? treeData.children.map(i => this.setTreeTitle(i)) : [],
|
||||
};
|
||||
}
|
||||
|
||||
setTreeExpandedKeys = () => {
|
||||
const expandedKeys = [];
|
||||
const setExpandedKeys = (nodes) => {
|
||||
for (const node of nodes) {
|
||||
expandedKeys.push(node.key);
|
||||
if (node.children) {
|
||||
setExpandedKeys(node.children);
|
||||
}
|
||||
}
|
||||
};
|
||||
setExpandedKeys(this.state.treeData);
|
||||
this.setState({
|
||||
expandedKeys: expandedKeys,
|
||||
});
|
||||
};
|
||||
|
||||
renderTree() {
|
||||
const onSelect = (selectedKeys, info) => {
|
||||
this.setState({
|
||||
selectedKeys: selectedKeys,
|
||||
groupName: info.node.key,
|
||||
groupId: info.node.id,
|
||||
});
|
||||
this.props.history.push(`/group-tree/${this.state.organizationName}/${info.node.key}`);
|
||||
};
|
||||
const onExpand = (expandedKeysValue) => {
|
||||
this.setState({
|
||||
expandedKeys: expandedKeysValue,
|
||||
});
|
||||
};
|
||||
|
||||
if (this.state.treeData.length === 0) {
|
||||
return <Empty />;
|
||||
}
|
||||
|
||||
const treeData = this.state.treeData.map(i => this.setTreeTitle(i));
|
||||
return (
|
||||
<Tree
|
||||
blockNode={true}
|
||||
defaultSelectedKeys={[this.state.groupName]}
|
||||
defaultExpandAll={true}
|
||||
expandedKeys={this.state.expandedKeys}
|
||||
onSelect={onSelect}
|
||||
onExpand={onExpand}
|
||||
showIcon={true}
|
||||
treeData={treeData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderOrganizationSelect() {
|
||||
return <OrganizationSelect
|
||||
initValue={this.state.organizationName}
|
||||
style={{width: "100%"}}
|
||||
onChange={(value) => {
|
||||
this.setState({
|
||||
organizationName: value,
|
||||
});
|
||||
this.props.history.push(`/group-tree/${value}`);
|
||||
}}
|
||||
/>;
|
||||
}
|
||||
|
||||
newGroup(isRoot) {
|
||||
const randomName = Setting.getRandomName();
|
||||
return {
|
||||
owner: this.state.organizationName,
|
||||
name: `group_${randomName}`,
|
||||
createdTime: moment().format(),
|
||||
updatedTime: moment().format(),
|
||||
displayName: `New Group - ${randomName}`,
|
||||
type: "Virtual",
|
||||
parentGroupId: isRoot ? this.state.organizationName : this.state.groupId,
|
||||
isTopGroup: isRoot,
|
||||
isEnabled: true,
|
||||
};
|
||||
}
|
||||
|
||||
addGroup(isRoot = false) {
|
||||
const newGroup = this.newGroup(isRoot);
|
||||
GroupBackend.addGroup(newGroup)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
sessionStorage.setItem("groupTreeUrl", window.location.pathname);
|
||||
this.props.history.push({pathname: `/groups/${newGroup.owner}/${newGroup.name}`, mode: "add"});
|
||||
Setting.showMessage("success", i18next.t("general:Successfully added"));
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div style={{
|
||||
flex: 1,
|
||||
backgroundColor: "white",
|
||||
padding: "5px 5px 2px 5px",
|
||||
}}>
|
||||
<Row>
|
||||
<Col span={5}>
|
||||
<Row>
|
||||
<Col span={24} style={{textAlign: "left"}}>
|
||||
{this.renderOrganizationSelect()}
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col span={24} style={{marginTop: "10px", textAlign: "left"}}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
this.setState({
|
||||
groupName: null,
|
||||
groupId: null,
|
||||
});
|
||||
this.props.history.push(`/group-tree/${this.state.organizationName}`);
|
||||
}}>
|
||||
{i18next.t("group:Show organization users")}
|
||||
</Button>
|
||||
<Button size={"small"} type={"primary"} style={{marginLeft: "10px"}}
|
||||
onClick={() => this.addGroup(true)}
|
||||
>
|
||||
{i18next.t("general:Add")}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: 10}}>
|
||||
<Col span={24} style={{textAlign: "left"}}>
|
||||
{this.renderTree()}
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col span={19}>
|
||||
<UserListPage
|
||||
organizationName={this.state.organizationName}
|
||||
groupName={this.state.groupName}
|
||||
groupId={this.state.groupId}
|
||||
{...this.props} />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default GroupTreePage;
|
@ -62,6 +62,7 @@ class OrganizationListPage extends BaseListPage {
|
||||
{name: "Signup application", visible: true, viewRule: "Public", modifyRule: "Admin"},
|
||||
{name: "Roles", visible: true, viewRule: "Public", modifyRule: "Immutable"},
|
||||
{name: "Permissions", visible: true, viewRule: "Public", modifyRule: "Immutable"},
|
||||
{name: "Groups", visible: true, viewRule: "Public", modifyRule: "Immutable"},
|
||||
{name: "3rd-party logins", visible: true, viewRule: "Self", modifyRule: "Self"},
|
||||
{Name: "Multi-factor authentication", Visible: true, ViewRule: "Self", ModifyRule: "Self"},
|
||||
{name: "Properties", visible: false, viewRule: "Admin", modifyRule: "Admin"},
|
||||
@ -221,11 +222,12 @@ class OrganizationListPage extends BaseListPage {
|
||||
title: i18next.t("general:Action"),
|
||||
dataIndex: "",
|
||||
key: "op",
|
||||
width: "240px",
|
||||
width: "320px",
|
||||
fixed: (Setting.isMobile()) ? "false" : "right",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/group-tree/${record.name}`)}>{i18next.t("general:Groups")}</Button>
|
||||
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/organizations/${record.name}/users`)}>{i18next.t("general:Users")}</Button>
|
||||
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} onClick={() => this.props.history.push(`/organizations/${record.name}`)}>{i18next.t("general:Edit")}</Button>
|
||||
<PopconfirmModal
|
||||
|
@ -13,7 +13,8 @@
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {Button, Card, Col, Input, InputNumber, List, Result, Row, Select, Spin, Switch, Tag} from "antd";
|
||||
import {Button, Card, Col, Input, InputNumber, List, Result, Row, Select, Space, Spin, Switch, Tag} from "antd";
|
||||
import * as GroupBackend from "./backend/GroupBackend";
|
||||
import * as UserBackend from "./backend/UserBackend";
|
||||
import * as OrganizationBackend from "./backend/OrganizationBackend";
|
||||
import * as Setting from "./Setting";
|
||||
@ -32,7 +33,7 @@ import PropertyTable from "./table/propertyTable";
|
||||
import {CountryCodeSelect} from "./common/select/CountryCodeSelect";
|
||||
import PopconfirmModal from "./common/modal/PopconfirmModal";
|
||||
import {DeleteMfa} from "./backend/MfaBackend";
|
||||
import {CheckCircleOutlined} from "@ant-design/icons";
|
||||
import {CheckCircleOutlined, HolderOutlined, UsergroupAddOutlined} from "@ant-design/icons";
|
||||
import {SmsMfaType} from "./auth/MfaSetupPage";
|
||||
import * as MfaBackend from "./backend/MfaBackend";
|
||||
|
||||
@ -47,6 +48,7 @@ class UserEditPage extends React.Component {
|
||||
userName: props.userName !== undefined ? props.userName : props.match.params.userName,
|
||||
user: null,
|
||||
application: null,
|
||||
groups: null,
|
||||
organizations: [],
|
||||
applications: [],
|
||||
mode: props.location.mode !== undefined ? props.location.mode : "edit",
|
||||
@ -63,6 +65,12 @@ class UserEditPage extends React.Component {
|
||||
this.setReturnUrl();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState, snapshot) {
|
||||
if (prevState.application !== this.state.application) {
|
||||
this.getGroups(this.state.organizationName);
|
||||
}
|
||||
}
|
||||
|
||||
getUser() {
|
||||
UserBackend.getUser(this.state.organizationName, this.state.userName)
|
||||
.then((data) => {
|
||||
@ -107,9 +115,26 @@ class UserEditPage extends React.Component {
|
||||
this.setState({
|
||||
application: application,
|
||||
});
|
||||
|
||||
this.setState({
|
||||
isGroupsVisible: application.organizationObj.accountItems?.some((item) => item.name === "Groups" && item.visible),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getGroups(organizationName) {
|
||||
if (this.state.isGroupsVisible) {
|
||||
GroupBackend.getGroups(organizationName)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.setState({
|
||||
groups: res.data,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setReturnUrl() {
|
||||
const searchParams = new URLSearchParams(this.props.location.search);
|
||||
const returnUrl = searchParams.get("returnUrl");
|
||||
@ -212,14 +237,6 @@ class UserEditPage extends React.Component {
|
||||
|
||||
const isAdmin = Setting.isAdminUser(this.props.account);
|
||||
|
||||
// return (
|
||||
// <div>
|
||||
// {
|
||||
// JSON.stringify({accountItem: accountItem, isSelf: isSelf, isAdmin: isAdmin})
|
||||
// }
|
||||
// </div>
|
||||
// )
|
||||
|
||||
if (accountItem.viewRule === "Self") {
|
||||
if (!this.isSelfOrAdmin()) {
|
||||
return null;
|
||||
@ -259,6 +276,7 @@ class UserEditPage extends React.Component {
|
||||
<Select virtual={false} style={{width: "100%"}} disabled={disabled} value={this.state.user.owner} onChange={(value => {
|
||||
this.getApplicationsByOrganization(value);
|
||||
this.updateUserField("owner", value);
|
||||
this.getGroups(value);
|
||||
})}>
|
||||
{
|
||||
this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>)
|
||||
@ -267,6 +285,35 @@ class UserEditPage extends React.Component {
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
} else if (accountItem.name === "Groups") {
|
||||
return (
|
||||
<Row style={{marginTop: "10px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("general:Groups"), i18next.t("general:Groups - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Select virtual={false} mode="multiple" style={{width: "100%"}} disabled={disabled} value={this.state.user.groups ?? []} onChange={(value => {
|
||||
if (this.state.groups?.filter(group => value.includes(group.id))
|
||||
.filter(group => group.type === "Physical").length > 1) {
|
||||
Setting.showMessage("error", i18next.t("general:You can only select one physical group"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateUserField("groups", value);
|
||||
})}
|
||||
>
|
||||
{
|
||||
this.state.groups?.map((group) => <Option key={group.id} value={group.id}>
|
||||
<Space>
|
||||
{group.type === "Physical" ? <UsergroupAddOutlined /> : <HolderOutlined />}
|
||||
{group.displayName}
|
||||
</Space>
|
||||
</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
} else if (accountItem.name === "ID") {
|
||||
return (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
@ -925,7 +972,12 @@ class UserEditPage extends React.Component {
|
||||
UserBackend.deleteUser(this.state.user)
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
this.props.history.push("/users");
|
||||
const userListUrl = sessionStorage.getItem("userListUrl");
|
||||
if (userListUrl !== null) {
|
||||
this.props.history.push(userListUrl);
|
||||
} else {
|
||||
this.props.history.push("/users");
|
||||
}
|
||||
} else {
|
||||
Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
|
||||
}
|
||||
|
@ -27,18 +27,41 @@ import PopconfirmModal from "./common/modal/PopconfirmModal";
|
||||
class UserListPage extends BaseListPage {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
...this.state,
|
||||
organizationName: this.props.organizationName ?? this.props.match?.params.organizationName ?? this.props.account.owner,
|
||||
organization: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setState({
|
||||
organizationName: this.props.match.params.organizationName,
|
||||
organization: null,
|
||||
});
|
||||
UNSAFE_componentWillMount() {
|
||||
super.UNSAFE_componentWillMount();
|
||||
this.getOrganization(this.state.organizationName);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (this.props.match.path !== prevProps.match.path || this.props.organizationName !== prevProps.organizationName) {
|
||||
this.setState({
|
||||
organizationName: this.props.organizationName ?? this.props.match?.params.organizationName,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.state.organizationName !== prevState.organizationName) {
|
||||
this.getOrganization(this.state.organizationName);
|
||||
}
|
||||
|
||||
if (prevProps.groupName !== this.props.groupName || this.state.organizationName !== prevState.organizationName) {
|
||||
this.fetch({
|
||||
pagination: this.state.pagination,
|
||||
searchText: this.state.searchText,
|
||||
searchedColumn: this.state.searchedColumn,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
newUser() {
|
||||
const randomName = Setting.getRandomName();
|
||||
const owner = (this.state.organizationName !== undefined) ? this.state.organizationName : this.props.account.owner;
|
||||
const owner = this.state.organizationName;
|
||||
return {
|
||||
owner: owner,
|
||||
name: `user_${randomName}`,
|
||||
@ -52,6 +75,7 @@ class UserListPage extends BaseListPage {
|
||||
phone: Setting.getRandomNumber(),
|
||||
countryCode: this.state.organization.countryCodes?.length > 0 ? this.state.organization.countryCodes[0] : "",
|
||||
address: [],
|
||||
groups: this.props.groupId !== undefined ? [this.props.groupId] : [],
|
||||
affiliation: "Example Inc.",
|
||||
tag: "staff",
|
||||
region: "",
|
||||
@ -116,6 +140,15 @@ class UserListPage extends BaseListPage {
|
||||
}
|
||||
}
|
||||
|
||||
getOrganization(organizationName) {
|
||||
OrganizationBackend.getOrganization("admin", organizationName)
|
||||
.then((organization) => {
|
||||
this.setState({
|
||||
organization: organization,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
renderUpload() {
|
||||
const props = {
|
||||
name: "file",
|
||||
@ -388,7 +421,7 @@ class UserListPage extends BaseListPage {
|
||||
const field = params.searchedColumn, value = params.searchText;
|
||||
const sortField = params.sortField, sortOrder = params.sortOrder;
|
||||
this.setState({loading: true});
|
||||
if (this.props.match.params.organizationName === undefined) {
|
||||
if (this.props.match?.path === "/users") {
|
||||
(Setting.isAdminUser(this.props.account) ? UserBackend.getGlobalUsers(params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder) : UserBackend.getUsers(this.props.account.owner, params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder))
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
@ -404,15 +437,7 @@ class UserListPage extends BaseListPage {
|
||||
searchText: params.searchText,
|
||||
searchedColumn: params.searchedColumn,
|
||||
});
|
||||
|
||||
const users = res.data;
|
||||
if (users.length > 0) {
|
||||
this.getOrganization(users[0].owner);
|
||||
} else {
|
||||
this.getOrganization(this.state.organizationName);
|
||||
}
|
||||
} else {
|
||||
|
||||
if (Setting.isResponseDenied(res)) {
|
||||
this.setState({
|
||||
isAuthorized: false,
|
||||
@ -423,7 +448,9 @@ class UserListPage extends BaseListPage {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
UserBackend.getUsers(this.props.match.params.organizationName, params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
|
||||
(this.props.groupName ?
|
||||
UserBackend.getUsers(this.state.organizationName, params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder, `${this.state.organizationName}/${this.props.groupName}`) :
|
||||
UserBackend.getUsers(this.state.organizationName, params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder))
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
@ -438,13 +465,6 @@ class UserListPage extends BaseListPage {
|
||||
searchText: params.searchText,
|
||||
searchedColumn: params.searchedColumn,
|
||||
});
|
||||
|
||||
const users = res.data;
|
||||
if (users.length > 0) {
|
||||
this.getOrganization(users[0].owner);
|
||||
} else {
|
||||
this.getOrganization(this.state.organizationName);
|
||||
}
|
||||
} else {
|
||||
if (Setting.isResponseDenied(res)) {
|
||||
this.setState({
|
||||
@ -457,15 +477,6 @@ class UserListPage extends BaseListPage {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
getOrganization(organizationName) {
|
||||
OrganizationBackend.getOrganization("admin", organizationName)
|
||||
.then((organization) => {
|
||||
this.setState({
|
||||
organization: organization,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default UserListPage;
|
||||
|
71
web/src/backend/GroupBackend.js
Normal file
71
web/src/backend/GroupBackend.js
Normal file
@ -0,0 +1,71 @@
|
||||
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed 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 * as Setting from "../Setting";
|
||||
|
||||
export function getGroups(owner = "", withTree = false, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-groups?owner=${owner}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}&withTree=${withTree}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function getGroup(owner, name) {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-group?id=${owner}/${encodeURIComponent(name)}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function updateGroup(owner, name, group) {
|
||||
const newGroup = Setting.deepCopy(group);
|
||||
return fetch(`${Setting.ServerUrl}/api/update-group?id=${owner}/${encodeURIComponent(name)}`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(newGroup),
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function addGroup(group) {
|
||||
const newGroup = Setting.deepCopy(group);
|
||||
return fetch(`${Setting.ServerUrl}/api/add-group`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(newGroup),
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function deleteGroup(group) {
|
||||
const newGroup = Setting.deepCopy(group);
|
||||
return fetch(`${Setting.ServerUrl}/api/delete-group`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify(newGroup),
|
||||
headers: {
|
||||
"Accept-Language": Setting.getAcceptLanguage(),
|
||||
},
|
||||
}).then(res => res.json());
|
||||
}
|
@ -25,8 +25,8 @@ export function getGlobalUsers(page, pageSize, field = "", value = "", sortField
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export function getUsers(owner, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-users?owner=${owner}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
|
||||
export function getUsers(owner, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "", groupId = "") {
|
||||
return fetch(`${Setting.ServerUrl}/api/get-users?owner=${owner}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}&groupId=${groupId}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
|
@ -19,16 +19,14 @@ import * as OrganizationBackend from "../../backend/OrganizationBackend";
|
||||
import * as Setting from "../../Setting";
|
||||
|
||||
function OrganizationSelect(props) {
|
||||
const {onChange} = props;
|
||||
const {onChange, initValue, style, onSelect} = props;
|
||||
const [organizations, setOrganizations] = React.useState([]);
|
||||
const [value, setValue] = React.useState("");
|
||||
const [value, setValue] = React.useState(initValue);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (props.organizations === undefined) {
|
||||
getOrganizations();
|
||||
}
|
||||
const initValue = organizations.length > 0 ? organizations[0] : "";
|
||||
handleOnChange(initValue);
|
||||
}, []);
|
||||
|
||||
const getOrganizations = () => {
|
||||
@ -36,6 +34,9 @@ function OrganizationSelect(props) {
|
||||
.then((res) => {
|
||||
if (res.status === "ok") {
|
||||
setOrganizations(res.data);
|
||||
if (initValue === undefined) {
|
||||
setValue(organizations.length > 0 ? organizations[0] : "");
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
@ -47,14 +48,14 @@ function OrganizationSelect(props) {
|
||||
|
||||
return (
|
||||
<Select
|
||||
options={organizations.map((organization) => Setting.getOption(organization.name, organization.name))}
|
||||
options={organizations.map((organization) => Setting.getOption(organization.displayName, organization.name))}
|
||||
virtual={false}
|
||||
showSearch
|
||||
placeholder={i18next.t("login:Please select an organization")}
|
||||
value={value}
|
||||
onChange={handleOnChange}
|
||||
filterOption={(input, option) => (option?.text ?? "").toLowerCase().includes(input.toLowerCase())}
|
||||
{...props}
|
||||
filterOption={(input, option) => (option?.label ?? "").toLowerCase().includes(input.toLowerCase())}
|
||||
style={style}
|
||||
onSelect={onSelect}
|
||||
>
|
||||
</Select>
|
||||
);
|
||||
|
@ -14,7 +14,7 @@
|
||||
|
||||
import React from "react";
|
||||
import * as Setting from "../../Setting";
|
||||
import {Dropdown} from "antd";
|
||||
import {Dropdown, Space} from "antd";
|
||||
import "../../App.less";
|
||||
import i18next from "i18next";
|
||||
import {CheckOutlined} from "@ant-design/icons";
|
||||
@ -43,10 +43,10 @@ class ThemeSelect extends React.Component {
|
||||
|
||||
getThemeItems() {
|
||||
return Themes.map((theme) => Setting.getItem(
|
||||
<div style={{display: "flex", justifyContent: "space-between"}}>
|
||||
<div>{i18next.t(`theme:${theme.label}`)}</div>
|
||||
<Space>
|
||||
{i18next.t(`theme:${theme.label}`)}
|
||||
{this.props.themeAlgorithm.includes(theme.key) ? <CheckOutlined style={{marginLeft: "5px"}} /> : null}
|
||||
</div>,
|
||||
</Space>,
|
||||
theme.key, theme.icon));
|
||||
}
|
||||
|
||||
|
@ -218,6 +218,8 @@
|
||||
"Forget URL - Tooltip": "Benutzerdefinierte URL für die \"Passwort vergessen\" Seite. Wenn nicht festgelegt, wird die standardmäßige Casdoor \"Passwort vergessen\" Seite verwendet. Wenn sie festgelegt ist, wird der \"Passwort vergessen\" Link auf der Login-Seite zu dieser URL umgeleitet",
|
||||
"Found some texts still not translated? Please help us translate at": "Haben Sie noch Texte gefunden, die nicht übersetzt wurden? Bitte helfen Sie uns beim Übersetzen",
|
||||
"Go to writable demo site?": "Gehe zur beschreibbaren Demo-Website?",
|
||||
"Groups": "Groups",
|
||||
"Groups - Tooltip": "Groups - Tooltip",
|
||||
"Home": "Zuhause",
|
||||
"Home - Tooltip": "Homepage der Anwendung",
|
||||
"ID": "ID",
|
||||
@ -311,6 +313,8 @@
|
||||
"This is a read-only demo site!": "Dies ist eine schreibgeschützte Demo-Seite!",
|
||||
"Timestamp": "Zeitstempel",
|
||||
"Tokens": "Token",
|
||||
"Type": "Type",
|
||||
"Type - Tooltip": "Type - Tooltip",
|
||||
"URL": "URL",
|
||||
"URL - Tooltip": "URL-Link",
|
||||
"Up": "Oben",
|
||||
@ -323,9 +327,18 @@
|
||||
"Users": "Benutzer",
|
||||
"Users under all organizations": "Benutzer unter allen Organisationen",
|
||||
"Webhooks": "Webhooks",
|
||||
"You can only select one physical group": "You can only select one physical group",
|
||||
"empty": "leere",
|
||||
"{total} in total": "Insgesamt {total}"
|
||||
},
|
||||
"group": {
|
||||
"Edit Group": "Edit Group",
|
||||
"New Group": "New Group",
|
||||
"Parent group": "Parent group",
|
||||
"Parent group - Tooltip": "Parent group - Tooltip",
|
||||
"Physical": "Physical",
|
||||
"Virtual": "Virtual"
|
||||
},
|
||||
"ldap": {
|
||||
"Admin": "Admin",
|
||||
"Admin - Tooltip": "CN oder ID des LDAP-Serveradministrators",
|
||||
@ -539,8 +552,6 @@
|
||||
"Edit Pricing": "Edit Pricing",
|
||||
"Free": "Kostenlos",
|
||||
"Getting started": "Loslegen",
|
||||
"Has trial": "Testphase verfügbar",
|
||||
"Has trial - Tooltip": "Verfügbarkeit der Testphase nach Auswahl eines Plans",
|
||||
"New Pricing": "New Pricing",
|
||||
"Trial duration": "Testphase Dauer",
|
||||
"Trial duration - Tooltip": "Dauer der Testphase",
|
||||
|
@ -218,6 +218,8 @@
|
||||
"Forget URL - Tooltip": "Custom URL for the \"Forget password\" page. If not set, the default Casdoor \"Forget password\" page will be used. When set, the \"Forget password\" link on the login page will redirect to this URL",
|
||||
"Found some texts still not translated? Please help us translate at": "Found some texts still not translated? Please help us translate at",
|
||||
"Go to writable demo site?": "Go to writable demo site?",
|
||||
"Groups": "Groups",
|
||||
"Groups - Tooltip": "Groups - Tooltip",
|
||||
"Home": "Home",
|
||||
"Home - Tooltip": "Home page of the application",
|
||||
"ID": "ID",
|
||||
@ -311,6 +313,8 @@
|
||||
"This is a read-only demo site!": "This is a read-only demo site!",
|
||||
"Timestamp": "Timestamp",
|
||||
"Tokens": "Tokens",
|
||||
"Type": "Type",
|
||||
"Type - Tooltip": "Type - Tooltip",
|
||||
"URL": "URL",
|
||||
"URL - Tooltip": "URL link",
|
||||
"Up": "Up",
|
||||
@ -323,9 +327,18 @@
|
||||
"Users": "Users",
|
||||
"Users under all organizations": "Users under all organizations",
|
||||
"Webhooks": "Webhooks",
|
||||
"You can only select one physical group": "You can only select one physical group",
|
||||
"empty": "empty",
|
||||
"{total} in total": "{total} in total"
|
||||
},
|
||||
"group": {
|
||||
"Edit Group": "Edit Group",
|
||||
"New Group": "New Group",
|
||||
"Parent group": "Parent group",
|
||||
"Parent group - Tooltip": "Parent group - Tooltip",
|
||||
"Physical": "Physical",
|
||||
"Virtual": "Virtual"
|
||||
},
|
||||
"ldap": {
|
||||
"Admin": "Admin",
|
||||
"Admin - Tooltip": "CN or ID of the LDAP server administrator",
|
||||
@ -539,8 +552,6 @@
|
||||
"Edit Pricing": "Edit Pricing",
|
||||
"Free": "Free",
|
||||
"Getting started": "Getting started",
|
||||
"Has trial": "Has trial",
|
||||
"Has trial - Tooltip": "Availability of the trial period after choosing a plan",
|
||||
"New Pricing": "New Pricing",
|
||||
"Trial duration": "Trial duration",
|
||||
"Trial duration - Tooltip": "Trial duration period",
|
||||
|
@ -218,6 +218,8 @@
|
||||
"Forget URL - Tooltip": "URL personalizada para la página \"Olvidé mi contraseña\". Si no se establece, se utilizará la página \"Olvidé mi contraseña\" predeterminada de Casdoor. Cuando se establezca, el enlace \"Olvidé mi contraseña\" en la página de inicio de sesión redireccionará a esta URL",
|
||||
"Found some texts still not translated? Please help us translate at": "¿Encontraste algunos textos que aún no están traducidos? Por favor, ayúdanos a traducirlos en",
|
||||
"Go to writable demo site?": "¿Ir al sitio demo editable?",
|
||||
"Groups": "Groups",
|
||||
"Groups - Tooltip": "Groups - Tooltip",
|
||||
"Home": "Hogar",
|
||||
"Home - Tooltip": "Página de inicio de la aplicación",
|
||||
"ID": "identificación",
|
||||
@ -311,6 +313,8 @@
|
||||
"This is a read-only demo site!": "¡Este es un sitio de demostración solo de lectura!",
|
||||
"Timestamp": "Marca de tiempo",
|
||||
"Tokens": "Tokens",
|
||||
"Type": "Type",
|
||||
"Type - Tooltip": "Type - Tooltip",
|
||||
"URL": "Dirección URL",
|
||||
"URL - Tooltip": "Enlace de URL",
|
||||
"Up": "Arriba",
|
||||
@ -323,9 +327,18 @@
|
||||
"Users": "Usuarios",
|
||||
"Users under all organizations": "Usuarios bajo todas las organizaciones",
|
||||
"Webhooks": "Webhooks",
|
||||
"You can only select one physical group": "You can only select one physical group",
|
||||
"empty": "vacío",
|
||||
"{total} in total": "{total} en total"
|
||||
},
|
||||
"group": {
|
||||
"Edit Group": "Edit Group",
|
||||
"New Group": "New Group",
|
||||
"Parent group": "Parent group",
|
||||
"Parent group - Tooltip": "Parent group - Tooltip",
|
||||
"Physical": "Physical",
|
||||
"Virtual": "Virtual"
|
||||
},
|
||||
"ldap": {
|
||||
"Admin": "Administrador",
|
||||
"Admin - Tooltip": "CN o ID del administrador del servidor LDAP",
|
||||
@ -539,8 +552,6 @@
|
||||
"Edit Pricing": "Edit Pricing",
|
||||
"Free": "Gratis",
|
||||
"Getting started": "Empezar",
|
||||
"Has trial": "Tiene período de prueba",
|
||||
"Has trial - Tooltip": "Disponibilidad del período de prueba después de elegir un plan",
|
||||
"New Pricing": "New Pricing",
|
||||
"Trial duration": "Duración del período de prueba",
|
||||
"Trial duration - Tooltip": "Duración del período de prueba",
|
||||
|
@ -218,6 +218,8 @@
|
||||
"Forget URL - Tooltip": "URL personnalisée pour la page \"Mot de passe oublié\". Si elle n'est pas définie, la page par défaut \"Mot de passe oublié\" de Casdoor sera utilisée. Lorsqu'elle est définie, le lien \"Mot de passe oublié\" sur la page de connexion sera redirigé vers cette URL",
|
||||
"Found some texts still not translated? Please help us translate at": "Trouvé des textes encore non traduits ? Veuillez nous aider à les traduire",
|
||||
"Go to writable demo site?": "Allez sur le site de démonstration modifiable ?",
|
||||
"Groups": "Groups",
|
||||
"Groups - Tooltip": "Groups - Tooltip",
|
||||
"Home": "Maison",
|
||||
"Home - Tooltip": "Page d'accueil de l'application",
|
||||
"ID": "Identité",
|
||||
@ -311,6 +313,8 @@
|
||||
"This is a read-only demo site!": "Ceci est un site de démonstration en lecture seule !",
|
||||
"Timestamp": "Horodatage",
|
||||
"Tokens": "Les jetons",
|
||||
"Type": "Type",
|
||||
"Type - Tooltip": "Type - Tooltip",
|
||||
"URL": "URL",
|
||||
"URL - Tooltip": "Lien d'URL",
|
||||
"Up": "Haut",
|
||||
@ -323,9 +327,18 @@
|
||||
"Users": "Utilisateurs",
|
||||
"Users under all organizations": "Utilisateurs sous toutes les organisations",
|
||||
"Webhooks": "Webhooks",
|
||||
"You can only select one physical group": "You can only select one physical group",
|
||||
"empty": "vide",
|
||||
"{total} in total": "{total} au total"
|
||||
},
|
||||
"group": {
|
||||
"Edit Group": "Edit Group",
|
||||
"New Group": "New Group",
|
||||
"Parent group": "Parent group",
|
||||
"Parent group - Tooltip": "Parent group - Tooltip",
|
||||
"Physical": "Physical",
|
||||
"Virtual": "Virtual"
|
||||
},
|
||||
"ldap": {
|
||||
"Admin": "Admin",
|
||||
"Admin - Tooltip": "CN ou ID de l'administrateur du serveur LDAP",
|
||||
@ -539,8 +552,6 @@
|
||||
"Edit Pricing": "Edit Pricing",
|
||||
"Free": "Gratuit",
|
||||
"Getting started": "Commencer",
|
||||
"Has trial": "Essai gratuit disponible",
|
||||
"Has trial - Tooltip": "Disponibilité de la période d'essai après avoir choisi un forfait",
|
||||
"New Pricing": "New Pricing",
|
||||
"Trial duration": "Durée de l'essai",
|
||||
"Trial duration - Tooltip": "Durée de la période d'essai",
|
||||
|
@ -218,6 +218,8 @@
|
||||
"Forget URL - Tooltip": "URL kustom untuk halaman \"Lupa kata sandi\". Jika tidak diatur, halaman \"Lupa kata sandi\" default Casdoor akan digunakan. Ketika diatur, tautan \"Lupa kata sandi\" pada halaman masuk akan diarahkan ke URL ini",
|
||||
"Found some texts still not translated? Please help us translate at": "Menemukan beberapa teks yang masih belum diterjemahkan? Tolong bantu kami menerjemahkan di",
|
||||
"Go to writable demo site?": "Pergi ke situs demo yang dapat ditulis?",
|
||||
"Groups": "Groups",
|
||||
"Groups - Tooltip": "Groups - Tooltip",
|
||||
"Home": "Rumah",
|
||||
"Home - Tooltip": "Halaman utama aplikasi",
|
||||
"ID": "ID",
|
||||
@ -311,6 +313,8 @@
|
||||
"This is a read-only demo site!": "Ini adalah situs demo hanya untuk dibaca saja!",
|
||||
"Timestamp": "Waktu penanda waktu",
|
||||
"Tokens": "Token-token",
|
||||
"Type": "Type",
|
||||
"Type - Tooltip": "Type - Tooltip",
|
||||
"URL": "URL",
|
||||
"URL - Tooltip": "Tautan URL",
|
||||
"Up": "Ke atas",
|
||||
@ -323,9 +327,18 @@
|
||||
"Users": "Pengguna-pengguna",
|
||||
"Users under all organizations": "Pengguna di bawah semua organisasi",
|
||||
"Webhooks": "Webhooks",
|
||||
"You can only select one physical group": "You can only select one physical group",
|
||||
"empty": "kosong",
|
||||
"{total} in total": "{total} secara keseluruhan"
|
||||
},
|
||||
"group": {
|
||||
"Edit Group": "Edit Group",
|
||||
"New Group": "New Group",
|
||||
"Parent group": "Parent group",
|
||||
"Parent group - Tooltip": "Parent group - Tooltip",
|
||||
"Physical": "Physical",
|
||||
"Virtual": "Virtual"
|
||||
},
|
||||
"ldap": {
|
||||
"Admin": "Admin",
|
||||
"Admin - Tooltip": "CN atau ID dari administrator server LDAP",
|
||||
@ -539,8 +552,6 @@
|
||||
"Edit Pricing": "Edit Pricing",
|
||||
"Free": "Gratis",
|
||||
"Getting started": "Mulai",
|
||||
"Has trial": "Mempunyai periode percobaan",
|
||||
"Has trial - Tooltip": "Ketersediaan periode percobaan setelah memilih rencana",
|
||||
"New Pricing": "New Pricing",
|
||||
"Trial duration": "Durasi percobaan",
|
||||
"Trial duration - Tooltip": "Durasi periode percobaan",
|
||||
|
@ -218,6 +218,8 @@
|
||||
"Forget URL - Tooltip": "「パスワードをお忘れの場合」ページのカスタムURL。未設定の場合、デフォルトのCasdoor「パスワードをお忘れの場合」ページが使用されます。設定された場合、ログインページの「パスワードをお忘れの場合」リンクはこのURLにリダイレクトされます",
|
||||
"Found some texts still not translated? Please help us translate at": "まだ翻訳されていない文章が見つかりましたか?是非とも翻訳のお手伝いをお願いします",
|
||||
"Go to writable demo site?": "書き込み可能なデモサイトに移動しますか?",
|
||||
"Groups": "Groups",
|
||||
"Groups - Tooltip": "Groups - Tooltip",
|
||||
"Home": "ホーム",
|
||||
"Home - Tooltip": "アプリケーションのホームページ",
|
||||
"ID": "ID",
|
||||
@ -311,6 +313,8 @@
|
||||
"This is a read-only demo site!": "これは読み取り専用のデモサイトです!",
|
||||
"Timestamp": "タイムスタンプ",
|
||||
"Tokens": "トークン",
|
||||
"Type": "Type",
|
||||
"Type - Tooltip": "Type - Tooltip",
|
||||
"URL": "URL",
|
||||
"URL - Tooltip": "URLリンク",
|
||||
"Up": "アップ",
|
||||
@ -323,9 +327,18 @@
|
||||
"Users": "ユーザー",
|
||||
"Users under all organizations": "すべての組織のユーザー",
|
||||
"Webhooks": "Webhooks",
|
||||
"You can only select one physical group": "You can only select one physical group",
|
||||
"empty": "空",
|
||||
"{total} in total": "総計{total}"
|
||||
},
|
||||
"group": {
|
||||
"Edit Group": "Edit Group",
|
||||
"New Group": "New Group",
|
||||
"Parent group": "Parent group",
|
||||
"Parent group - Tooltip": "Parent group - Tooltip",
|
||||
"Physical": "Physical",
|
||||
"Virtual": "Virtual"
|
||||
},
|
||||
"ldap": {
|
||||
"Admin": "Admin",
|
||||
"Admin - Tooltip": "LDAPサーバー管理者のCNまたはID",
|
||||
@ -539,8 +552,6 @@
|
||||
"Edit Pricing": "Edit Pricing",
|
||||
"Free": "無料",
|
||||
"Getting started": "はじめる",
|
||||
"Has trial": "トライアル期間あり",
|
||||
"Has trial - Tooltip": "プラン選択後のトライアル期間の有無",
|
||||
"New Pricing": "New Pricing",
|
||||
"Trial duration": "トライアル期間の長さ",
|
||||
"Trial duration - Tooltip": "トライアル期間の長さ",
|
||||
|
@ -218,6 +218,8 @@
|
||||
"Forget URL - Tooltip": "\"비밀번호를 잊어버렸을 경우\" 페이지에 대한 사용자 정의 URL. 설정되지 않은 경우 기본 Casdoor \"비밀번호를 잊어버렸을 경우\" 페이지가 사용됩니다. 설정된 경우 로그인 페이지의 \"비밀번호를 잊으셨나요?\" 링크는 이 URL로 리디렉션됩니다",
|
||||
"Found some texts still not translated? Please help us translate at": "아직 번역되지 않은 텍스트가 있나요? 번역에 도움을 주실 수 있나요?",
|
||||
"Go to writable demo site?": "쓰기 가능한 데모 사이트로 이동하시겠습니까?",
|
||||
"Groups": "Groups",
|
||||
"Groups - Tooltip": "Groups - Tooltip",
|
||||
"Home": "집",
|
||||
"Home - Tooltip": "어플리케이션 홈 페이지",
|
||||
"ID": "ID",
|
||||
@ -311,6 +313,8 @@
|
||||
"This is a read-only demo site!": "이것은 읽기 전용 데모 사이트입니다!",
|
||||
"Timestamp": "타임스탬프",
|
||||
"Tokens": "토큰",
|
||||
"Type": "Type",
|
||||
"Type - Tooltip": "Type - Tooltip",
|
||||
"URL": "URL",
|
||||
"URL - Tooltip": "URL 링크",
|
||||
"Up": "위로",
|
||||
@ -323,9 +327,18 @@
|
||||
"Users": "사용자들",
|
||||
"Users under all organizations": "모든 조직의 사용자",
|
||||
"Webhooks": "Webhooks",
|
||||
"You can only select one physical group": "You can only select one physical group",
|
||||
"empty": "빈",
|
||||
"{total} in total": "총 {total}개"
|
||||
},
|
||||
"group": {
|
||||
"Edit Group": "Edit Group",
|
||||
"New Group": "New Group",
|
||||
"Parent group": "Parent group",
|
||||
"Parent group - Tooltip": "Parent group - Tooltip",
|
||||
"Physical": "Physical",
|
||||
"Virtual": "Virtual"
|
||||
},
|
||||
"ldap": {
|
||||
"Admin": "Admin",
|
||||
"Admin - Tooltip": "LDAP 서버 관리자의 CN 또는 ID",
|
||||
@ -539,8 +552,6 @@
|
||||
"Edit Pricing": "Edit Pricing",
|
||||
"Free": "무료",
|
||||
"Getting started": "시작하기",
|
||||
"Has trial": "무료 체험 가능",
|
||||
"Has trial - Tooltip": "플랜 선택 후 체험 기간의 가용 여부",
|
||||
"New Pricing": "New Pricing",
|
||||
"Trial duration": "체험 기간",
|
||||
"Trial duration - Tooltip": "체험 기간의 기간",
|
||||
|
@ -218,6 +218,8 @@
|
||||
"Forget URL - Tooltip": "URL personalizada para a página de \"Esqueci a senha\". Se não definido, será usada a página padrão de \"Esqueci a senha\" do Casdoor. Quando definido, o link de \"Esqueci a senha\" na página de login será redirecionado para esta URL",
|
||||
"Found some texts still not translated? Please help us translate at": "Encontrou algum texto ainda não traduzido? Ajude-nos a traduzir em",
|
||||
"Go to writable demo site?": "Acessar o site de demonstração gravável?",
|
||||
"Groups": "Groups",
|
||||
"Groups - Tooltip": "Groups - Tooltip",
|
||||
"Home": "Página Inicial",
|
||||
"Home - Tooltip": "Página inicial do aplicativo",
|
||||
"ID": "ID",
|
||||
@ -311,6 +313,8 @@
|
||||
"This is a read-only demo site!": "Este é um site de demonstração apenas para leitura!",
|
||||
"Timestamp": "Carimbo de Data/Hora",
|
||||
"Tokens": "Tokens",
|
||||
"Type": "Type",
|
||||
"Type - Tooltip": "Type - Tooltip",
|
||||
"URL": "URL",
|
||||
"URL - Tooltip": "Link da URL",
|
||||
"Up": "Acima",
|
||||
@ -323,9 +327,18 @@
|
||||
"Users": "Usuários",
|
||||
"Users under all organizations": "Usuários em todas as organizações",
|
||||
"Webhooks": "Webhooks",
|
||||
"You can only select one physical group": "You can only select one physical group",
|
||||
"empty": "vazio",
|
||||
"{total} in total": "{total} no total"
|
||||
},
|
||||
"group": {
|
||||
"Edit Group": "Edit Group",
|
||||
"New Group": "New Group",
|
||||
"Parent group": "Parent group",
|
||||
"Parent group - Tooltip": "Parent group - Tooltip",
|
||||
"Physical": "Physical",
|
||||
"Virtual": "Virtual"
|
||||
},
|
||||
"ldap": {
|
||||
"Admin": "Administrador",
|
||||
"Admin - Tooltip": "CN ou ID do administrador do servidor LDAP",
|
||||
@ -539,8 +552,6 @@
|
||||
"Edit Pricing": "Edit Pricing",
|
||||
"Free": "Miễn phí",
|
||||
"Getting started": "Bắt đầu",
|
||||
"Has trial": "Có thời gian thử nghiệm",
|
||||
"Has trial - Tooltip": "Khả dụng thời gian thử nghiệm sau khi chọn kế hoạch",
|
||||
"New Pricing": "New Pricing",
|
||||
"Trial duration": "Thời gian thử nghiệm",
|
||||
"Trial duration - Tooltip": "Thời gian thử nghiệm",
|
||||
|
@ -218,6 +218,8 @@
|
||||
"Forget URL - Tooltip": "Настроенный URL для страницы \"Забыли пароль\". Если не установлено, будет использоваться стандартная страница \"Забыли пароль\" Casdoor. При установке, ссылка \"Забыли пароль\" на странице входа будет перенаправляться на этот URL",
|
||||
"Found some texts still not translated? Please help us translate at": "Нашли некоторые тексты, которые еще не переведены? Пожалуйста, помогите нам перевести на",
|
||||
"Go to writable demo site?": "Перейти на демонстрационный сайт для записи данных?",
|
||||
"Groups": "Groups",
|
||||
"Groups - Tooltip": "Groups - Tooltip",
|
||||
"Home": "Дом",
|
||||
"Home - Tooltip": "Главная страница приложения",
|
||||
"ID": "ID",
|
||||
@ -311,6 +313,8 @@
|
||||
"This is a read-only demo site!": "Это демонстрационный сайт только для чтения!",
|
||||
"Timestamp": "Отметка времени",
|
||||
"Tokens": "Токены",
|
||||
"Type": "Type",
|
||||
"Type - Tooltip": "Type - Tooltip",
|
||||
"URL": "URL",
|
||||
"URL - Tooltip": "Ссылка URL",
|
||||
"Up": "Вверх",
|
||||
@ -323,9 +327,18 @@
|
||||
"Users": "Пользователи",
|
||||
"Users under all organizations": "Пользователи всех организаций",
|
||||
"Webhooks": "Webhooks",
|
||||
"You can only select one physical group": "You can only select one physical group",
|
||||
"empty": "пустые",
|
||||
"{total} in total": "{total} в общей сложности"
|
||||
},
|
||||
"group": {
|
||||
"Edit Group": "Edit Group",
|
||||
"New Group": "New Group",
|
||||
"Parent group": "Parent group",
|
||||
"Parent group - Tooltip": "Parent group - Tooltip",
|
||||
"Physical": "Physical",
|
||||
"Virtual": "Virtual"
|
||||
},
|
||||
"ldap": {
|
||||
"Admin": "Admin",
|
||||
"Admin - Tooltip": "CN или ID администратора сервера LDAP",
|
||||
@ -539,8 +552,6 @@
|
||||
"Edit Pricing": "Edit Pricing",
|
||||
"Free": "Бесплатно",
|
||||
"Getting started": "Выьрать план",
|
||||
"Has trial": "Есть пробный период",
|
||||
"Has trial - Tooltip": "Наличие пробного периода после выбора плана",
|
||||
"New Pricing": "New Pricing",
|
||||
"Trial duration": "Продолжительность пробного периода",
|
||||
"Trial duration - Tooltip": "Продолжительность пробного периода",
|
||||
|
@ -218,6 +218,8 @@
|
||||
"Forget URL - Tooltip": "Đường dẫn tùy chỉnh cho trang \"Quên mật khẩu\". Nếu không được thiết lập, trang \"Quên mật khẩu\" mặc định của Casdoor sẽ được sử dụng. Khi cài đặt, liên kết \"Quên mật khẩu\" trên trang đăng nhập sẽ chuyển hướng đến URL này",
|
||||
"Found some texts still not translated? Please help us translate at": "Tìm thấy một số văn bản vẫn chưa được dịch? Vui lòng giúp chúng tôi dịch tại",
|
||||
"Go to writable demo site?": "Bạn có muốn đi đến trang demo có thể viết được không?",
|
||||
"Groups": "Groups",
|
||||
"Groups - Tooltip": "Groups - Tooltip",
|
||||
"Home": "Nhà",
|
||||
"Home - Tooltip": "Trang chủ của ứng dụng",
|
||||
"ID": "ID",
|
||||
@ -311,6 +313,8 @@
|
||||
"This is a read-only demo site!": "Đây là trang web giới thiệu chỉ có chức năng đọc!",
|
||||
"Timestamp": "Đánh dấu thời gian",
|
||||
"Tokens": "Mã thông báo",
|
||||
"Type": "Type",
|
||||
"Type - Tooltip": "Type - Tooltip",
|
||||
"URL": "URL",
|
||||
"URL - Tooltip": "Đường dẫn URL",
|
||||
"Up": "Lên",
|
||||
@ -323,9 +327,18 @@
|
||||
"Users": "Người dùng",
|
||||
"Users under all organizations": "Người dùng trong tất cả các tổ chức",
|
||||
"Webhooks": "Webhooks",
|
||||
"You can only select one physical group": "You can only select one physical group",
|
||||
"empty": "trống",
|
||||
"{total} in total": "Trong tổng số {total}"
|
||||
},
|
||||
"group": {
|
||||
"Edit Group": "Edit Group",
|
||||
"New Group": "New Group",
|
||||
"Parent group": "Parent group",
|
||||
"Parent group - Tooltip": "Parent group - Tooltip",
|
||||
"Physical": "Physical",
|
||||
"Virtual": "Virtual"
|
||||
},
|
||||
"ldap": {
|
||||
"Admin": "Admin",
|
||||
"Admin - Tooltip": "CN hoặc ID của quản trị viên máy chủ LDAP",
|
||||
@ -539,8 +552,6 @@
|
||||
"Edit Pricing": "Edit Pricing",
|
||||
"Free": "Miễn phí",
|
||||
"Getting started": "Bắt đầu",
|
||||
"Has trial": "Có thời gian thử nghiệm",
|
||||
"Has trial - Tooltip": "Khả dụng thời gian thử nghiệm sau khi chọn kế hoạch",
|
||||
"New Pricing": "New Pricing",
|
||||
"Trial duration": "Thời gian thử nghiệm",
|
||||
"Trial duration - Tooltip": "Thời gian thử nghiệm",
|
||||
|
@ -218,6 +218,8 @@
|
||||
"Forget URL - Tooltip": "自定义忘记密码页面的URL,不设置时采用Casdoor默认的忘记密码页面,设置后Casdoor各类页面的忘记密码链接会跳转到该URL",
|
||||
"Found some texts still not translated? Please help us translate at": "发现有些文字尚未翻译?请移步这里帮我们翻译:",
|
||||
"Go to writable demo site?": "跳转至可写演示站点?",
|
||||
"Groups": "用户组",
|
||||
"Groups - Tooltip": "Groups - Tooltip",
|
||||
"Home": "首页",
|
||||
"Home - Tooltip": "应用的首页",
|
||||
"ID": "ID",
|
||||
@ -311,6 +313,8 @@
|
||||
"This is a read-only demo site!": "这是一个只读演示站点!",
|
||||
"Timestamp": "时间戳",
|
||||
"Tokens": "令牌",
|
||||
"Type": "类型",
|
||||
"Type - Tooltip": "类型",
|
||||
"URL": "链接",
|
||||
"URL - Tooltip": "URL链接",
|
||||
"Up": "上移",
|
||||
@ -323,9 +327,18 @@
|
||||
"Users": "用户",
|
||||
"Users under all organizations": "所有组织里的用户",
|
||||
"Webhooks": "Webhooks",
|
||||
"You can only select one physical group": "只能选择一个实体组",
|
||||
"empty": "无",
|
||||
"{total} in total": "{total} 总计"
|
||||
},
|
||||
"group": {
|
||||
"Edit Group": "编辑用户组",
|
||||
"New Group": "新建用户组",
|
||||
"Parent group": "上级组",
|
||||
"Parent group - Tooltip": "上级组",
|
||||
"Physical": "物理组",
|
||||
"Virtual": "虚拟组"
|
||||
},
|
||||
"ldap": {
|
||||
"Admin": "管理员",
|
||||
"Admin - Tooltip": "LDAP服务器管理员的CN或ID",
|
||||
@ -539,8 +552,6 @@
|
||||
"Edit Pricing": "编辑定价",
|
||||
"Free": "免费",
|
||||
"Getting started": "开始使用",
|
||||
"Has trial": "有试用期",
|
||||
"Has trial - Tooltip": "选择计划后是否有试用期",
|
||||
"New Pricing": "添加定价",
|
||||
"Trial duration": "试用期时长",
|
||||
"Trial duration - Tooltip": "试用期时长",
|
||||
|
@ -93,6 +93,7 @@ class AccountTable extends React.Component {
|
||||
{name: "Signup application", label: i18next.t("general:Signup application")},
|
||||
{name: "Roles", label: i18next.t("general:Roles")},
|
||||
{name: "Permissions", label: i18next.t("general:Permissions")},
|
||||
{name: "Groups", label: i18next.t("general:Groups")},
|
||||
{name: "3rd-party logins", label: i18next.t("user:3rd-party logins")},
|
||||
{name: "Properties", label: i18next.t("user:Properties")},
|
||||
{name: "Is online", label: i18next.t("user:Is online")},
|
||||
|
Loading…
x
Reference in New Issue
Block a user