Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add user status filter to admin user management page #16770

Merged
merged 40 commits into from
Oct 12, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
d5adeee
Admin can filter user list by status
wxiaoguang Aug 22, 2021
0b2604b
introduce window.config.PageData to pass template data to javascript …
wxiaoguang Aug 25, 2021
921e3d8
Merge branch 'main' into admin-user-list-filter
wxiaoguang Aug 25, 2021
dbf21d2
revert ActivityTopAuthors related changes, maybe a new PR is needed
wxiaoguang Aug 25, 2021
3f2a934
Merge branch 'admin-user-list-filter' of github.com:wxiaoguang/gitea …
wxiaoguang Aug 25, 2021
30f7430
use LEFT JOIN instead of SubQuery when admin filters users by 2fa. re…
wxiaoguang Aug 26, 2021
79a2be0
Merge remote-tracking branch 'go-gitea/main' into admin-user-list-filter
wxiaoguang Aug 26, 2021
6ec918e
use OptionalBool instead of status map
wxiaoguang Aug 26, 2021
e133612
refactor SearchUserOptions.toConds to SearchUserOptions.toSearchQuery…
wxiaoguang Aug 26, 2021
86a245a
add unit test for user search
wxiaoguang Aug 26, 2021
eba00ca
Merge branch 'main' into admin-user-list-filter
wxiaoguang Aug 31, 2021
5cf8f29
Merge branch 'main' into admin-user-list-filter
wxiaoguang Sep 1, 2021
3296672
only allow admin to use filters to search users
wxiaoguang Sep 3, 2021
21869fa
Merge branch 'main' into admin-user-list-filter
wxiaoguang Sep 3, 2021
f6f06f2
reformat
wxiaoguang Sep 3, 2021
df32fb4
fix search query: Where and Join
wxiaoguang Sep 4, 2021
3e9a588
Merge branch 'main' into admin-user-list-filter
wxiaoguang Sep 4, 2021
9ddc61c
Merge branch 'main' into admin-user-list-filter
wxiaoguang Sep 18, 2021
db75013
Merge branch 'main' into admin-user-list-filter
wxiaoguang Sep 22, 2021
89b3564
fix merge conflict
wxiaoguang Sep 22, 2021
a660168
Merge branch 'main' into admin-user-list-filter
wxiaoguang Sep 26, 2021
6bf9c07
fix merge
wxiaoguang Sep 26, 2021
a0a963a
fix unit test
wxiaoguang Sep 26, 2021
2148ce5
refactor
wxiaoguang Sep 27, 2021
1fe17b3
Merge branch 'main' into admin-user-list-filter
wxiaoguang Sep 27, 2021
892613d
fix lint
wxiaoguang Sep 27, 2021
d76cd6d
fix sort order
wxiaoguang Sep 27, 2021
96cc571
fix sql table name quote, clean up
wxiaoguang Sep 28, 2021
1806f1a
only query fields of `user` table
wxiaoguang Sep 28, 2021
508fded
Merge branch 'main' into admin-user-list-filter
wxiaoguang Sep 28, 2021
7f35e9c
clean
wxiaoguang Sep 28, 2021
a2bbf4c
fix comment and lint
wxiaoguang Sep 28, 2021
8b4c540
clean and try unit test for mssql
wxiaoguang Sep 28, 2021
c8c5263
Merge branch 'main' into admin-user-list-filter
wxiaoguang Oct 6, 2021
65c2378
Merge branch 'main' into admin-user-list-filter
wxiaoguang Oct 9, 2021
fb92420
Merge branch 'main' into admin-user-list-filter
wxiaoguang Oct 11, 2021
821685e
Merge branch 'main' into admin-user-list-filter
6543 Oct 11, 2021
c0a5a39
refactor
wxiaoguang Oct 12, 2021
4b90372
Merge branch 'main' into admin-user-list-filter
wxiaoguang Oct 12, 2021
31135e5
Merge branch 'main' into admin-user-list-filter
lunny Oct 12, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 37 additions & 9 deletions models/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -1588,14 +1588,15 @@ func GetUser(user *User) (bool, error) {
// SearchUserOptions contains the options for searching
type SearchUserOptions struct {
ListOptions
Keyword string
Type UserType
UID int64
OrderBy SearchOrderBy
Visible []structs.VisibleType
Actor *User // The user doing the search
IsActive util.OptionalBool
SearchByEmail bool // Search by email as well as username/full name
Keyword string
StatusFilterMap map[string]string // Admin can apply advanced search filters
Type UserType
UID int64
OrderBy SearchOrderBy
Visible []structs.VisibleType
Actor *User // The user doing the search
IsActive util.OptionalBool
SearchByEmail bool // Search by email as well as username/full name
}

func (opts *SearchUserOptions) toConds() builder.Cond {
Expand Down Expand Up @@ -1643,8 +1644,35 @@ func (opts *SearchUserOptions) toConds() builder.Cond {
// Don't forget about self
accessCond = accessCond.Or(builder.Eq{"id": opts.Actor.ID})
cond = cond.And(accessCond)
} else {
// Admin can apply advanced filters
for filterKey, filterValue := range opts.StatusFilterMap {
wxiaoguang marked this conversation as resolved.
Show resolved Hide resolved
if filterValue == "" {
continue
}
if filterKey == "is_active" {
cond = cond.And(builder.Eq{"is_active": filterValue})
} else if filterKey == "is_admin" {
cond = cond.And(builder.Eq{"is_admin": filterValue})
} else if filterKey == "is_restricted" {
cond = cond.And(builder.Eq{"is_restricted": filterValue})
} else if filterKey == "is_prohibit_login" {
cond = cond.And(builder.Eq{"prohibit_login": filterValue})
} else if filterKey == "is_2fa_enabled" {
// TODO: bad performance here, maybe there will be a column "is_2fa_enabled" in the future
var twoFactorCond builder.Cond
twoFactorBuilder := builder.Select("uid").From("two_factor").Where(builder.And(builder.Expr("two_factor.uid = user.id")))
if filterValue == "1" {
twoFactorCond = builder.In("id", twoFactorBuilder)
} else {
twoFactorCond = builder.NotIn("id", twoFactorBuilder)
}
cond = cond.And(twoFactorCond)
wxiaoguang marked this conversation as resolved.
Show resolved Hide resolved
} else {
log.Critical("Unknown admin user search filter: %v=%v", filterKey, filterValue)
wxiaoguang marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

} else {
// Force visibility for privacy
// Not logged in - only public users
Expand Down
12 changes: 12 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -2347,6 +2347,18 @@ users.still_own_repo = This user still owns one or more repositories. Delete or
users.still_has_org = This user is a member of an organization. Remove the user from any organizations first.
users.deletion_success = The user account has been deleted.
users.reset_2fa = Reset 2FA
users.list_status_filter.menu_text = Filter
users.list_status_filter.reset = Reset
users.list_status_filter.is_active = Active
users.list_status_filter.not_active = Inactive
users.list_status_filter.is_admin = Admin
users.list_status_filter.not_admin = Not Admin
users.list_status_filter.is_restricted = Restricted
users.list_status_filter.not_restricted = Not Restricted
users.list_status_filter.is_prohibit_login = Prohibit Login
users.list_status_filter.not_prohibit_login = Allow Login
users.list_status_filter.is_2fa_enabled = 2FA Enabled
users.list_status_filter.not_2fa_enabled = 2FA Disabled

emails.email_manage_panel = User Email Management
emails.primary = Primary
Expand Down
18 changes: 15 additions & 3 deletions options/locale/locale_zh-CN.ini
Original file line number Diff line number Diff line change
Expand Up @@ -634,8 +634,8 @@ last_used=上次使用在
no_activity=没有最近活动
can_read_info=读取
can_write_info=写入
key_state_desc=7 天内使用过该密钥
token_state_desc=7 天内使用过该密钥
key_state_desc=7 天内使用过该密钥
wxiaoguang marked this conversation as resolved.
Show resolved Hide resolved
token_state_desc=7 天内使用过该密钥
principal_state_desc=7 天内使用过该规则
show_openid=在个人信息上显示
hide_openid=在个人信息上隐藏
Expand Down Expand Up @@ -812,7 +812,7 @@ watchers=关注者
stargazers=称赞者
forks=派生仓库
pick_reaction=选择你的表情
reactions_more=再加载 %d
reactions_more=再加载 %d
unit_disabled=站点管理员已禁用此仓库单元。
language_other=其它
adopt_search=输入用户名以搜索未被收录的仓库... (留空以查找全部)
Expand Down Expand Up @@ -2337,6 +2337,18 @@ users.still_own_repo=此用户仍然拥有一个或多个仓库。必须首先
users.still_has_org=此用户是组织的成员。必须先从组织中删除用户。
users.deletion_success=用户帐户已被删除。
users.reset_2fa=重置两步验证
users.list_status_filter.menu_text = 筛选
users.list_status_filter.reset = 重置
users.list_status_filter.is_active = 已激活
users.list_status_filter.not_active = 未激活
users.list_status_filter.is_admin = 管理员
users.list_status_filter.not_admin = 非管理员
users.list_status_filter.is_restricted = 受限
users.list_status_filter.not_restricted = 未受限
users.list_status_filter.is_prohibit_login = 禁止登录
users.list_status_filter.not_prohibit_login = 允许登录
users.list_status_filter.is_2fa_enabled = 两步认证已开启
users.list_status_filter.not_2fa_enabled = 两步认证已禁用

emails.email_manage_panel=邮件管理
emails.primary=主要的
Expand Down
9 changes: 9 additions & 0 deletions routers/web/explore/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,25 @@ func RenderUserSearch(ctx *context.Context, opts *models.SearchUserOptions, tplN
orderBy = models.SearchOrderByAlphabetically
}

statusFilterKeys := []string{"is_active", "is_admin", "is_restricted", "is_2fa_enabled", "is_prohibit_login"}
wxiaoguang marked this conversation as resolved.
Show resolved Hide resolved
statusFilterMap := map[string]string{}
for _, filterKey := range statusFilterKeys {
statusFilterMap[filterKey] = ctx.FormString("status_filter[" + filterKey + "]")
}

opts.Keyword = ctx.FormTrim("q")
opts.OrderBy = orderBy
opts.StatusFilterMap = statusFilterMap
if len(opts.Keyword) == 0 || isKeywordValid(opts.Keyword) {
users, count, err = models.SearchUsers(opts)
if err != nil {
ctx.ServerError("SearchUsers", err)
return
}
}

ctx.Data["Keyword"] = opts.Keyword
ctx.Data["StatusFilterMap"] = statusFilterMap
ctx.Data["Total"] = count
ctx.Data["Users"] = users
ctx.Data["UsersTwoFaStatus"] = models.UserList(users).GetTwoFaStatus()
Expand Down
2 changes: 1 addition & 1 deletion templates/admin/base/search.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
</div>
</div>
</div>
<form class="ui form ignore-dirty" style="max-width: 90%">
<form class="ui form ignore-dirty" style="max-width: 90%;">
<div class="ui fluid action input">
<input name="q" value="{{.Keyword}}" placeholder="{{.i18n.Tr "explore.search"}}..." autofocus>
<button class="ui blue button">{{.i18n.Tr "explore.search"}}</button>
Expand Down
62 changes: 61 additions & 1 deletion templates/admin/user/list.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,67 @@
</div>
</h4>
<div class="ui attached segment">
{{template "admin/base/search" .}}
<form class="ui form ignore-dirty" id="user-list-search-form">

<!-- Right Menu -->
<div class="ui right floated secondary filter menu">
<!-- Status Filter Menu Item -->
<div class="ui dropdown type jump item">
<span class="text">{{.i18n.Tr "admin.users.list_status_filter.menu_text"}} {{svg "octicon-triangle-down" 14 "dropdown icon"}}</span>
<div class="menu">
<a class="item j-reset-status-filter">{{.i18n.Tr "admin.users.list_status_filter.reset"}}</a>
<div class="ui divider"></div>
<label class="item"><input type="radio" name="status_filter[is_admin]" value="1"> {{.i18n.Tr "admin.users.list_status_filter.is_admin"}}</label>
<label class="item"><input type="radio" name="status_filter[is_admin]" value="0"> {{.i18n.Tr "admin.users.list_status_filter.not_admin"}}</label>
<div class="ui divider"></div>
<label class="item"><input type="radio" name="status_filter[is_active]" value="1"> {{.i18n.Tr "admin.users.list_status_filter.is_active"}}</label>
<label class="item"><input type="radio" name="status_filter[is_active]" value="0"> {{.i18n.Tr "admin.users.list_status_filter.not_active"}}</label>
<div class="ui divider"></div>
<label class="item"><input type="radio" name="status_filter[is_restricted]" value="0"> {{.i18n.Tr "admin.users.list_status_filter.not_restricted"}}</label>
<label class="item"><input type="radio" name="status_filter[is_restricted]" value="1"> {{.i18n.Tr "admin.users.list_status_filter.is_restricted"}}</label>
<div class="ui divider"></div>
<label class="item"><input type="radio" name="status_filter[is_prohibit_login]" value="0"> {{.i18n.Tr "admin.users.list_status_filter.not_prohibit_login"}}</label>
<label class="item"><input type="radio" name="status_filter[is_prohibit_login]" value="1"> {{.i18n.Tr "admin.users.list_status_filter.is_prohibit_login"}}</label>
<div class="ui divider"></div>
<label class="item"><input type="radio" name="status_filter[is_2fa_enabled]" value="1"> {{.i18n.Tr "admin.users.list_status_filter.is_2fa_enabled"}}</label>
<label class="item"><input type="radio" name="status_filter[is_2fa_enabled]" value="0"> {{.i18n.Tr "admin.users.list_status_filter.not_2fa_enabled"}}</label>
</div>
</div>

<!-- Sort Menu Item -->
<div class="ui dropdown type jump item">
<span class="text">
{{.i18n.Tr "repo.issues.filter_sort"}} {{svg "octicon-triangle-down" 14 "dropdown icon"}}
</span>
<div class="menu">
<button class="item" name="sort" value="oldest">{{.i18n.Tr "repo.issues.filter_sort.oldest"}}</button>
<button class="item" name="sort" value="newest">{{.i18n.Tr "repo.issues.filter_sort.latest"}}</button>
<button class="item" name="sort" value="alphabetically">{{.i18n.Tr "repo.issues.label.filter_sort.alphabetically"}}</button>
<button class="item" name="sort" value="reversealphabetically">{{.i18n.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</button>
<button class="item" name="sort" value="recentupdate">{{.i18n.Tr "repo.issues.filter_sort.recentupdate"}}</button>
<button class="item" name="sort" value="leastupdate">{{.i18n.Tr "repo.issues.filter_sort.leastupdate"}}</button>
</div>
</div>
</div>

<!-- Search Text -->
<div class="ui fluid action input" style="max-width: 70%;">
<input name="q" value="{{.Keyword}}" placeholder="{{.i18n.Tr "explore.search"}}..." autofocus>
<button class="ui blue button">{{.i18n.Tr "explore.search"}}</button>
</div>

{{/* here we have valid go template syntax, but eslint doesn't like it and reports "error Parsing error: Unexpected token {" */}}
<script>
<!-- /* eslint-disable */ -->
(function() {
wxiaoguang marked this conversation as resolved.
Show resolved Hide resolved
window.giteaContext = window.giteaContext || {};
window.giteaContext.adminUserListSearchForm = {
wxiaoguang marked this conversation as resolved.
Show resolved Hide resolved
statusFilterMap: {{.StatusFilterMap}},
sortType: {{.SortType}} || 'oldest'
}
})();
</script>
</form>
</div>
<div class="ui attached table segment">
<table class="ui very basic striped table">
Expand Down
34 changes: 34 additions & 0 deletions web_src/js/features/admin-users.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export function initAdminUserListSearchForm() {
if (!$('.admin').length) return;
if (!window.giteaContext || !window.giteaContext.adminUserListSearchForm) return;

const $form = $('#user-list-search-form');
if (!$form.length) return;

const searchForm = window.giteaContext.adminUserListSearchForm;

$form.find(`button[name=sort][value=${searchForm.sortType}]`).addClass('active');

if (searchForm.statusFilterMap) {
for (const [k, v] of Object.entries(searchForm.statusFilterMap)) {
if (!v) continue;
$form.find(`input[name="status_filter[${k}]"][value=${v}]`).prop('checked', true);
}
}

$form.find(`input[type=radio]`).click(() => {
$form.submit();
return false;
});

$form.find('.j-reset-status-filter').click(() => {
$form.find(`input[type=radio]`).each((_, e) => {
const $e = $(e);
if ($e.attr('name').startsWith('status_filter[')) {
$e.prop('checked', false);
}
});
$form.submit();
return false;
});
}
2 changes: 2 additions & 0 deletions web_src/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import initMigration from './features/migration.js';
import initProject from './features/projects.js';
import initServiceWorker from './features/serviceworker.js';
import initTableSort from './features/tablesort.js';
import {initAdminUserListSearchForm} from './features/admin-users.js';
import {createCodeEditor, createMonaco} from './features/codeeditor.js';
import {initMarkupAnchors} from './markup/anchors.js';
import {initNotificationsTable, initNotificationCount} from './features/notification.js';
Expand Down Expand Up @@ -2839,6 +2840,7 @@ $(document).ready(async () => {
initFileViewToggle();
initReleaseEditor();
initRelease();
initAdminUserListSearchForm();

const routes = {
'div.user.settings': initUserSettings,
Expand Down