Merge pull request #180 from getrebuild/online-users-480

RB-480 Online users
This commit is contained in:
devezhao 2020-06-16 18:03:18 +08:00 committed by GitHub
commit fd2294ce5e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 382 additions and 215 deletions

2
@rbv

@ -1 +1 @@
Subproject commit 743aa9c6fb21bbf3f88a9eec93b34c0ee8a4c4a6
Subproject commit e75127ce05c1ad0b93ffff42d545bd21bc7971b7

View file

@ -31,7 +31,7 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<argLine>-Dfile.encoding=UTF-8</argLine>
<skipTests>true</skipTests>
<spring.version>4.3.26.RELEASE</spring.version>
<spring.version>4.3.27.RELEASE</spring.version>
</properties>
<build>
@ -139,7 +139,7 @@
<dependency>
<groupId>com.github.devezhao</groupId>
<artifactId>momentjava</artifactId>
<version>91f4df2081</version>
<version>0.2.0</version>
</dependency>
<dependency>
<groupId>com.github.devezhao</groupId>
@ -149,7 +149,7 @@
<dependency>
<groupId>com.github.devezhao</groupId>
<artifactId>commons</artifactId>
<version>1.1.7</version>
<version>1.2.0</version>
<exclusions>
<exclusion>
<artifactId>httpclient</artifactId>
@ -160,7 +160,7 @@
<dependency>
<groupId>com.github.devezhao</groupId>
<artifactId>persist4j</artifactId>
<version>1beaf8f565</version>
<version>1.4.2</version>
</dependency>
<dependency>
<groupId>junit</groupId>

View file

@ -0,0 +1,198 @@
/*
Copyright (c) REBUILD <https://getrebuild.com/> and its owners. All rights reserved.
rebuild is dual-licensed under commercial and open source licenses (GPLv3).
See LICENSE and COMMERCIAL in the project root for license information.
*/
package com.rebuild.server.configuration.portals;
import cn.devezhao.commons.CodecUtils;
import cn.devezhao.persist4j.Entity;
import cn.devezhao.persist4j.engine.ID;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.rebuild.server.Application;
import com.rebuild.server.ServerListener;
import com.rebuild.server.configuration.ConfigEntry;
import com.rebuild.server.metadata.MetadataHelper;
import com.rebuild.utils.AppUtils;
import com.rebuild.utils.JSONUtils;
import org.apache.commons.lang.StringUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import javax.servlet.http.HttpServletRequest;
import java.util.Iterator;
/**
* 导航渲染
*
* @author devezhao
* @since 2020/6/16
*/
public class NavBuilder extends NavManager {
public static final NavBuilder instance = new NavBuilder();
private NavBuilder() { }
/**
* 默认导航
*/
private static final JSONArray NAVS_DEFAULT = JSONUtils.toJSONObjectArray(
new String[] { "icon", "text", "type", "value" },
new Object[][] {
new Object[] { "chart-donut", "动态", "ENTITY", NAV_FEEDS },
new Object[] { "folder", "文件", "ENTITY", NAV_FILEMRG }
});
/**
* @param request
* @return
*/
public JSONArray getNavPortal(HttpServletRequest request) {
return getNavPortal(AppUtils.getRequestUser(request));
}
/**
* @param user
* @return
*/
public JSONArray getNavPortal(ID user) {
ConfigEntry config = getLayoutOfNav(user);
if (config == null) {
return NAVS_DEFAULT;
}
// 过滤
JSONArray navs = (JSONArray) config.getJSON("config");
for (Iterator<Object> iter = navs.iterator(); iter.hasNext(); ) {
JSONObject nav = (JSONObject) iter.next();
JSONArray subNavs = nav.getJSONArray("sub");
if (subNavs != null && !subNavs.isEmpty()) {
for (Iterator<Object> subIter = subNavs.iterator(); subIter.hasNext(); ) {
JSONObject subNav = (JSONObject) subIter.next();
if (isFilterNav(subNav, user)) {
subIter.remove();
}
}
// 无子级移除主菜单
if (subNavs.isEmpty()) {
iter.remove();
}
} else if (isFilterNav(nav, user)) {
iter.remove();
}
}
return navs;
}
/**
* 是否需要过滤掉
*
* @param nav
* @param user
* @return
*/
private boolean isFilterNav(JSONObject nav, ID user) {
String type = nav.getString("type");
if ("ENTITY".equalsIgnoreCase(type)) {
String entity = nav.getString("value");
if (NAV_PARENT.equals(entity)) {
return true;
} else if (NAV_FEEDS.equals(entity) || NAV_FILEMRG.equals(entity)) {
return false;
} else if (!MetadataHelper.containsEntity(entity)) {
LOG.warn("Unknow entity in nav : " + entity);
return true;
}
Entity entityMeta = MetadataHelper.getEntity(entity);
return !Application.getSecurityManager().allowRead(user, entityMeta.getEntityCode());
}
return false;
}
/**
* 渲染导航菜單
*
* @param item
* @param activeNav
* @return
*/
public String renderNavItem(JSONObject item, String activeNav) {
final boolean isUrlType = "URL".equals(item.getString("type"));
String navName = item.getString("value");
String navUrl = item.getString("value");
boolean isOutUrl = isUrlType && navUrl.startsWith("http");
if (isUrlType) {
navName = "nav_url-" + navName.hashCode();
if (isOutUrl) {
navUrl = ServerListener.getContextPath() + "/commons/url-safe?url=" + CodecUtils.urlEncode(navUrl);
} else {
navUrl = ServerListener.getContextPath() + navUrl;
}
} else if (NAV_FEEDS.equals(navName)) {
navName = "nav_entity-Feeds";
navUrl = ServerListener.getContextPath() + "/feeds/home";
} else if (NAV_FILEMRG.equals(navName)) {
navName = "nav_entity-Attachment";
navUrl = ServerListener.getContextPath() + "/files/home";
} else {
navName = "nav_entity-" + navName;
navUrl = ServerListener.getContextPath() + "/app/" + navUrl + "/list";
}
String navIcon = StringUtils.defaultIfBlank(item.getString("icon"), "texture");
String navText = item.getString("text");
JSONArray subNavs = null;
if (activeNav != null) {
subNavs = item.getJSONArray("sub");
if (subNavs == null || subNavs.isEmpty()) {
subNavs = null;
}
}
StringBuilder navHtml = new StringBuilder()
.append(String.format("<li class=\"%s\"><a href=\"%s\" target=\"%s\"><i class=\"icon zmdi zmdi-%s\"></i><span>%s</span></a>",
navName + (subNavs == null ? StringUtils.EMPTY : " parent"),
subNavs == null ? navUrl : "###",
isOutUrl ? "_blank" : "_self",
navIcon,
navText));
if (subNavs != null) {
StringBuilder subHtml = new StringBuilder()
.append("<ul class=\"sub-menu\"><li class=\"title\">")
.append(navText)
.append("</li><li class=\"nav-items\"><div class=\"content\"><ul class=\"sub-menu-ul\">");
for (Object o : subNavs) {
JSONObject subNav = (JSONObject) o;
subHtml.append(renderNavItem(subNav, null));
}
subHtml.append("</ul></div></li></ul>");
navHtml.append(subHtml);
}
navHtml.append("</li>");
if (activeNav != null) {
Document navBody = Jsoup.parseBodyFragment(navHtml.toString());
for (Element nav : navBody.select("." + activeNav)) {
nav.addClass("active");
if (activeNav.startsWith("nav_entity-")) {
Element navParent = nav.parent();
if (navParent != null && navParent.hasClass("sub-menu-ul")) {
navParent.parent().parent().parent().parent().addClass("open active");
}
}
}
return navBody.selectFirst("li").outerHtml();
}
return navHtml.toString();
}
}

View file

@ -7,26 +7,11 @@ See LICENSE and COMMERCIAL in the project root for license information.
package com.rebuild.server.configuration.portals;
import cn.devezhao.commons.CodecUtils;
import cn.devezhao.persist4j.Entity;
import cn.devezhao.persist4j.engine.ID;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.rebuild.server.Application;
import com.rebuild.server.ServerListener;
import com.rebuild.server.configuration.ConfigEntry;
import com.rebuild.server.metadata.MetadataHelper;
import com.rebuild.utils.AppUtils;
import com.rebuild.utils.JSONUtils;
import org.apache.commons.lang.StringUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
@ -43,9 +28,11 @@ public class NavManager extends BaseLayoutManager {
public static final String NAV_FEEDS = "$FEEDS$";
// 文件
public static final String NAV_FILEMRG = "$FILEMRG$";
// 项目看板
public static final String NAV_KANBANMRG = "$KANBANMRG$";
public static final NavManager instance = new NavManager();
private NavManager() { }
protected NavManager() { }
/**
* @param user
@ -79,160 +66,4 @@ public class NavManager extends BaseLayoutManager {
}
return array.toArray(new ID[0]);
}
// ----
/**
* 默认导航
*/
private static final JSONArray NAVS_DEFAULT = JSONUtils.toJSONObjectArray(
new String[] { "icon", "text", "type", "value" },
new Object[][] {
new Object[] { "chart-donut", "动态", "ENTITY", NAV_FEEDS },
new Object[] { "folder", "文件", "ENTITY", NAV_FILEMRG }
});
/**
* @param request
* @return
*/
public JSONArray getNavForPortal(HttpServletRequest request) {
return getNavForPortal(AppUtils.getRequestUser(request));
}
/**
* @param user
* @return
*/
public JSONArray getNavForPortal(ID user) {
ConfigEntry config = getLayoutOfNav(user);
if (config == null) {
return NAVS_DEFAULT;
}
// 过滤
JSONArray navs = (JSONArray) config.getJSON("config");
for (Iterator<Object> iter = navs.iterator(); iter.hasNext(); ) {
JSONObject nav = (JSONObject) iter.next();
JSONArray subNavs = nav.getJSONArray("sub");
if (subNavs != null && !subNavs.isEmpty()) {
for (Iterator<Object> subIter = subNavs.iterator(); subIter.hasNext(); ) {
JSONObject subNav = (JSONObject) subIter.next();
if (isFilterNav(subNav, user)) {
subIter.remove();
}
}
// 无子级移除主菜单
if (subNavs.isEmpty()) {
iter.remove();
}
} else if (isFilterNav(nav, user)) {
iter.remove();
}
}
return navs;
}
/**
* 是否需要过滤掉
*
* @param nav
* @param user
* @return
*/
private boolean isFilterNav(JSONObject nav, ID user) {
String type = nav.getString("type");
if ("ENTITY".equalsIgnoreCase(type)) {
String entity = nav.getString("value");
if (NAV_PARENT.equals(entity)) {
return true;
} else if (NAV_FEEDS.equals(entity) || NAV_FILEMRG.equals(entity)) {
return false;
} else if (!MetadataHelper.containsEntity(entity)) {
LOG.warn("Unknow entity in nav : " + entity);
return true;
}
Entity entityMeta = MetadataHelper.getEntity(entity);
return !Application.getSecurityManager().allowRead(user, entityMeta.getEntityCode());
}
return false;
}
/**
* 渲染导航菜單
*
* @param item
* @param activeNav
* @return
*/
public String renderNavItem(JSONObject item, String activeNav) {
final boolean isUrlType = "URL".equals(item.getString("type"));
String navName = item.getString("value");
String navUrl = item.getString("value");
if (isUrlType) {
navName = "nav_url-" + navName.hashCode();
navUrl = ServerListener.getContextPath() + "/commons/url-safe?url=" + CodecUtils.urlEncode(navUrl);
} else if (NAV_FEEDS.equals(navName)) {
navName = "nav_entity-Feeds";
navUrl = ServerListener.getContextPath() + "/feeds/home";
} else if (NAV_FILEMRG.equals(navName)) {
navName = "nav_entity-Attachment";
navUrl = ServerListener.getContextPath() + "/files/home";
} else {
navName = "nav_entity-" + navName;
navUrl = ServerListener.getContextPath() + "/app/" + navUrl + "/list";
}
String navIcon = StringUtils.defaultIfBlank(item.getString("icon"), "texture");
String navText = item.getString("text");
JSONArray subNavs = null;
if (activeNav != null) {
subNavs = item.getJSONArray("sub");
if (subNavs == null || subNavs.isEmpty()) {
subNavs = null;
}
}
StringBuilder navHtml = new StringBuilder()
.append(String.format("<li class=\"%s\"><a href=\"%s\" target=\"%s\"><i class=\"icon zmdi zmdi-%s\"></i><span>%s</span></a>",
navName + (subNavs == null ? StringUtils.EMPTY : " parent"),
subNavs == null ? navUrl : "###",
isUrlType ? "_blank" : "_self",
navIcon,
navText));
if (subNavs != null) {
StringBuilder subHtml = new StringBuilder()
.append("<ul class=\"sub-menu\"><li class=\"title\">")
.append(navText)
.append("</li><li class=\"nav-items\"><div class=\"content\"><ul class=\"sub-menu-ul\">");
for (Object o : subNavs) {
JSONObject subNav = (JSONObject) o;
subHtml.append(renderNavItem(subNav, null));
}
subHtml.append("</ul></div></li></ul>");
navHtml.append(subHtml);
}
navHtml.append("</li>");
if (activeNav != null) {
Document navBody = Jsoup.parseBodyFragment(navHtml.toString());
for (Element nav : navBody.select("." + activeNav)) {
nav.addClass("active");
if (activeNav.startsWith("nav_entity-")) {
Element navParent = nav.parent();
if (navParent != null && navParent.hasClass("sub-menu-ul")) {
navParent.parent().parent().parent().parent().addClass("open active");
}
}
}
return navBody.selectFirst("li").outerHtml();
}
return navHtml.toString();
}
}

View file

@ -68,6 +68,9 @@ public enum ConfigurableItem {
// 管理员警告
AdminDangers(true),
// 允许同一用户多个会话
MultipleSessions(true),
;
private Object defaultVal;

View file

@ -129,6 +129,17 @@ public class OnlineSessionStore extends CurrentCaller implements HttpSessionList
Object loginUser = s.getAttribute(WebUtils.CURRENT_USER);
Assert.notNull(loginUser, "No login user found in session!");
if (!SysConfiguration.getBool(ConfigurableItem.MultipleSessions)) {
HttpSession previous = getSession((ID) loginUser);
if (previous != null) {
LOG.warn("Kill previous session : " + loginUser + " < " + previous.getId());
try {
previous.invalidate();
} catch (Exception ignored) {
}
}
}
ONLINE_SESSIONS.remove(s);
ONLINE_USERS.put((ID) loginUser, s);
}

View file

@ -82,7 +82,9 @@ public class RequestWatchHandler extends HandlerInterceptorAdapter implements In
// for Language
Application.getSessionStore().setLocale(AppUtils.getLocale(request));
// Last active
Application.getSessionStore().storeLastActive(request);
if (!(isIgnoreActive(requestUrl) || ServletUtils.isAjaxRequest(request))) {
Application.getSessionStore().storeLastActive(request);
}
}
boolean chain = super.preHandle(request, response, handler);
@ -217,6 +219,14 @@ public class RequestWatchHandler extends HandlerInterceptorAdapter implements In
|| reqUrl.startsWith("/commons/barcode/render");
}
/**
* @param reqUrl
* @return
*/
private static boolean isIgnoreActive(String reqUrl) {
return reqUrl.contains("/language/") || reqUrl.contains("/user-avatar");
}
/**
* 是否特定缓存策略
*
@ -229,4 +239,6 @@ public class RequestWatchHandler extends HandlerInterceptorAdapter implements In
|| reqUrl.startsWith("/language/")
|| reqUrl.startsWith("/commons/barcode/");
}
}

View file

@ -1,35 +1,34 @@
/*
rebuild - Building your business-systems freely.
Copyright (C) 2019 devezhao <zhaofang123@gmail.com>
Copyright (c) REBUILD <https://getrebuild.com/> and its owners. All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
rebuild is dual-licensed under commercial and open source licenses (GPLv3).
See LICENSE and COMMERCIAL in the project root for license information.
*/
package com.rebuild.web.admin.bizz;
import cn.devezhao.commons.web.WebUtils;
import cn.devezhao.momentjava.Moment;
import cn.devezhao.persist4j.engine.ID;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.rebuild.server.Application;
import com.rebuild.server.configuration.portals.DataListManager;
import com.rebuild.server.service.bizz.UserHelper;
import com.rebuild.utils.JSONUtils;
import com.rebuild.utils.LocationUtils;
import com.rebuild.web.BaseEntityControll;
import com.rebuild.web.OnlineSessionStore;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.Date;
/**
* @author devezhao-mac zhaofang123@gmail.com
@ -57,4 +56,41 @@ public class LoginLogControll extends BaseEntityControll {
writeFailure(response);
}
}
@RequestMapping("/admin/bizuser/online-users")
public void getOnlineUsers(HttpServletResponse response) throws IOException {
JSONArray users = new JSONArray();
for (HttpSession s : Application.getSessionStore().getAllSession()) {
ID user = (ID) s.getAttribute(WebUtils.CURRENT_USER);
if (user == null) continue;
Object[] active = (Object[]) s.getAttribute(OnlineSessionStore.SK_LASTACTIVE);
if (active == null) {
active = new Object[] { "", "/dashboard/home" };
} else {
active = active.clone();
active[0] = Moment.moment(new Date((Long) active[0])).fromNow();
}
JSONObject item = JSONUtils.toJSONObject(
new String[] { "user", "fullName", "activeTime", "activeUrl" },
new Object[] { user, UserHelper.getName(user), active[0], active[1] } );
users.add(item);
}
writeSuccess(response, users);
}
@RequestMapping("/admin/bizuser/kill-session")
public void killSession(HttpServletRequest request, HttpServletResponse response) throws IOException {
ID user = getIdParameterNotNull(request, "user");
HttpSession s = Application.getSessionStore().getSession(user);
if (s != null) {
LOG.warn("Kill session via admin : " + user + " < " + s.getId());
try {
s.invalidate();
} catch (Exception ignored) {
}
}
writeSuccess(response);
}
}

View file

@ -1,12 +1,12 @@
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page import="com.alibaba.fastjson.JSONObject"%>
<%@ page import="com.alibaba.fastjson.JSONArray"%>
<%@ page import="com.rebuild.server.configuration.portals.NavManager"%>
<%@ page import="com.rebuild.utils.AppUtils" %>
<%@ page import="com.rebuild.server.service.bizz.privileges.ZeroEntry" %>
<%@ page import="com.rebuild.server.configuration.portals.NavBuilder" %>
<%
final String activeNav = request.getParameter("activeNav");
final JSONArray navArray = NavManager.instance.getNavForPortal(request);
final JSONArray navArray = NavBuilder.instance.getNavPortal(request);
%>
<div class="rb-left-sidebar">
<div class="left-sidebar-wrapper">
@ -16,14 +16,14 @@ final JSONArray navArray = NavManager.instance.getNavForPortal(request);
<div class="left-sidebar-content no-divider">
<ul class="sidebar-elements">
<li class="<%="dashboard-home".equals(activeNav) ? "active" : ""%>"><a href="${baseUrl}/dashboard/home"><i class="icon zmdi zmdi-home"></i><span>首页</span></a></li>
<% for (Object o : navArray) { out.print(NavManager.instance.renderNavItem((JSONObject) o, activeNav)); } %>
<% for (Object o : navArray) out.print(NavBuilder.instance.renderNavItem((JSONObject) o, activeNav)); %>
</ul>
</div>
</div>
</div>
<% if (AppUtils.allow(request, ZeroEntry.AllowCustomNav)) { %>
<div class="bottom-widget">
<a class="nav-settings" href="javascript:;" title="设置导航菜单"><i class="icon zmdi zmdi-apps"></i></a>
<a class="nav-settings" title="设置导航菜单"><i class="icon zmdi zmdi-apps"></i></a>
</div>
<% } %>
</div>

View file

@ -48,7 +48,7 @@
</div>
<div class="col-12 col-lg-6">
<div class="dataTables_oper">
<button class="btn btn-space btn-secondary J_view" disabled="disabled"><i class="icon zmdi zmdi-folder"></i> 打开</button>
<button class="btn btn-space btn-secondary J_view" type="button" disabled="disabled"><i class="icon zmdi zmdi-folder"></i> 打开</button>
<button class="btn btn-primary btn-space J_new" type="button"><i class="icon zmdi zmdi-accounts-add"></i> 新建${entityLabel}</button>
<div class="btn-group btn-space">
<button class="btn btn-secondary dropdown-toggle" type="button" data-toggle="dropdown">更多 <i class="icon zmdi zmdi-more-vert"></i></button>

View file

@ -29,6 +29,7 @@
</div>
<div class="col-12 col-md-6">
<div class="dataTables_oper">
<button class="btn btn-space btn-secondary J_view-online" type="button"><i class="icon zmdi zmdi-accounts"></i> 查看在线用户</button>
<div class="btn-group btn-space">
<button class="btn btn-secondary dropdown-toggle" type="button" data-toggle="dropdown">更多 <i class="icon zmdi zmdi-more-vert"></i></button>
<div class="dropdown-menu dropdown-menu-right">
@ -59,6 +60,7 @@ window.__PageConfig = {
<script src="${baseUrl}/assets/js/rb-datalist.jsx" type="text/babel"></script>
<script src="${baseUrl}/assets/js/rb-forms.jsx" type="text/babel"></script>
<script src="${baseUrl}/assets/js/rb-forms.exts.jsx" type="text/babel"></script>
<script src="${baseUrl}/assets/js/admin/online-users.jsx" type="text/babel"></script>
<script type="text/babel">
RbList.renderAfter = function() {
let ipAddrIndex = -1
@ -82,6 +84,9 @@ RbList.renderAfter = function() {
})
})
}
$(document).ready(() => {
$('.J_view-online').click(() => renderRbcomp(<OnlineUserViewer />))
})
</script>
</body>
</html>

View file

@ -29,7 +29,7 @@
</div>
<div class="col-12 col-lg-6">
<div class="dataTables_oper">
<button class="btn btn-space btn-secondary J_view" disabled="disabled"><i class="icon zmdi zmdi-folder"></i> 打开</button>
<button class="btn btn-space btn-secondary J_view" type="button" disabled="disabled"><i class="icon zmdi zmdi-folder"></i> 打开</button>
<button class="btn btn-primary btn-space J_new" type="button"><i class="icon zmdi zmdi-plus"></i> 新建${entityLabel}</button>
<div class="btn-group btn-space">
<button class="btn btn-secondary dropdown-toggle" type="button" data-toggle="dropdown">更多 <i class="icon zmdi zmdi-more-vert"></i></button>

View file

@ -48,7 +48,7 @@
</div>
<div class="col-12 col-lg-7">
<div class="dataTables_oper">
<button class="btn btn-space btn-secondary J_view" disabled="disabled"><i class="icon zmdi zmdi-folder"></i> 打开</button>
<button class="btn btn-space btn-secondary J_view" type="button" disabled="disabled"><i class="icon zmdi zmdi-folder"></i> 打开</button>
<div class="btn-group btn-space">
<button class="btn btn-primary J_new" type="button"><i class="icon zmdi zmdi-account-add"></i> 新建${entityLabel}</button>
<button class="btn btn-primary dropdown-toggle auto" type="button" data-toggle="dropdown"><span class="icon zmdi zmdi-chevron-down"></span></button>

View file

@ -57,7 +57,7 @@ a#entityIcon:hover{opacity:0.8}
</label>
</div>
<div class="mb-1">
<button type="button" class="btn btn-danger J_drop-confirm" disabled="disabled" data-loading-text="删除中"><i class="zmdi zmdi-delete icon"></i> 确认删除</button>
<button type="button" class="btn btn-danger J_drop-confirm" type="button" disabled="disabled" data-loading-text="删除中"><i class="zmdi zmdi-delete icon"></i> 确认删除</button>
<div class="alert alert-warning alert-icon hide col-sm-6 mb-0">
<div class="icon"><span class="zmdi zmdi-alert-triangle"></span></div>
<div class="message">系统内建实体,不允许删除</div>

View file

@ -0,0 +1,70 @@
/*
Copyright (c) REBUILD <https://getrebuild.com/> and its owners. All rights reserved.
rebuild is dual-licensed under commercial and open source licenses (GPLv3).
See LICENSE and COMMERCIAL in the project root for license information.
*/
// eslint-disable-next-line no-unused-vars
class OnlineUserViewer extends RbModalHandler {
constructor(props) {
super(props)
}
render() {
return <RbModal ref={(c) => this._dlg = c} title="在线用户" disposeOnHide={true}>
<table className="table table-hover table-sm mb-0">
<thead>
<tr>
<th width="30%">用户</th>
<th>最近活跃</th>
<th width="80"></th>
</tr>
</thead>
<tbody>
{(this.state.users || []).map((item) => {
return (
<tr key={`user-${item.user}`}>
<td className="user-avatar">
<img src={`${rb.baseUrl}/account/user-avatar/${item.user}`} />
<span>{item.fullName}</span>
</td>
<td>
<a href="###" className="text-break">{item.activeUrl || '无'}</a>
<p className="text-muted fs-12 m-0">{item.activeTime}</p>
</td>
<td className="text-right">
<button className="btn btn-danger bordered btn-sm" type="button" onClick={() => this._killSession(item.user)}>强退</button>
</td>
</tr>
)
})}
</tbody>
</table>
</RbModal >
}
componentDidMount() {
this._load()
}
_load() {
$.get('/admin/bizuser/online-users', (res) => {
if (res.error_code === 0) this.setState({ users: res.data })
else RbHighbar.error(res.error_msg)
})
}
_killSession(user) {
const that = this
RbAlert.create('确认强制退出该用户?', {
confirm: function () {
$.post(`/admin/bizuser/kill-session?user=${user}`, () => {
this.hide()
that._load()
})
}
})
}
}

View file

@ -45,7 +45,7 @@ $(document).ready(function () {
value = $val('.J_menuUrl')
if (!value) {
RbHighbar.create('请输入 URL'); return
} else if (!!value && !$regex.isUrl(value)) {
} else if (!($regex.isUrl(value) || $regex.isUrl(`https://getrebuild.com${value}`))) {
RbHighbar.create('请输入有效的 URL')
return
}

View file

@ -63,7 +63,7 @@
<select class="form-control form-control-sm J_menuEntity">
<option value="">请选择关联项</option>
<optgroup label="业务实体"></optgroup>
<optgroup label="其他">
<optgroup label="系统内建">
<option value="$FEEDS$" data-icon="chart-donut">动态</option>
<option value="$FILEMRG$" data-icon="folder">文件</option>
<option value="$PARENT$" data-icon="menu">父级菜单</option>
@ -72,6 +72,7 @@
</div>
<div class="tab-pane" id="URL">
<input type="text" class="form-control form-control-sm J_menuUrl" placeholder="输入 URL">
<div class="form-text">支持外部地址或相对地址</div>
</div>
</div>
</div>

View file

@ -19,14 +19,14 @@
<script src="${baseUrl}/assets/js/zmdi-icons.js"></script>
<script type="text/babel">
$(document).ready(function(){
let call = parent.clickIcon || function(icon){ console.log(icon) }
const call = parent && parent.clickIcon ? parent.clickIcon : function(icon){ console.log(icon) }
$(ZMDI_ICONS).each(function(){
if (ZMDI_ICONS_IGNORE.contains(this + '') == false) {
let a = $('<a data-icon="' + this + '" title="' + this.toUpperCase() + '"><i class="zmdi zmdi-' + this + '"></a>').appendTo('#icons')
a.click(function(){ call($(this).data('icon')) })
const $a = $('<a data-icon="' + this + '" title="' + this.toUpperCase() + '"><i class="zmdi zmdi-' + this + '"></a>').appendTo('#icons')
$a.click(function(){ call($(this).data('icon')) })
}
})
parent.RbModal.resize()
parent && parent.RbModal && parent.RbModal.resize()
})
</script>
</body>

View file

@ -69,8 +69,8 @@
</div>
<div class="col-12 col-md-6">
<div class="dataTables_oper">
<button class="btn btn-space btn-secondary J_view" disabled="disabled"><i class="icon zmdi zmdi-folder"></i> 打开</button>
<button class="btn btn-space btn-secondary J_edit" disabled="disabled"><i class="icon zmdi zmdi-border-color"></i> 编辑</button>
<button class="btn btn-space btn-secondary J_view" type="button" disabled="disabled"><i class="icon zmdi zmdi-folder"></i> 打开</button>
<button class="btn btn-space btn-secondary J_edit" type="button" disabled="disabled"><i class="icon zmdi zmdi-border-color"></i> 编辑</button>
<button class="btn btn-space btn-primary J_new"><i class="icon zmdi zmdi-plus"></i> 新建</button>
<div class="btn-group btn-space J_action">
<button class="btn btn-secondary dropdown-toggle" type="button" data-toggle="dropdown">更多 <i class="icon zmdi zmdi-more-vert"></i></button>

View file

@ -67,8 +67,8 @@
</div>
<div class="col-12 col-md-6">
<div class="dataTables_oper">
<button class="btn btn-space btn-secondary J_view" disabled="disabled"><i class="icon zmdi zmdi-folder"></i> 打开</button>
<button class="btn btn-space btn-secondary J_edit" disabled="disabled"><i class="icon zmdi zmdi-border-color"></i> 编辑</button>
<button class="btn btn-space btn-secondary J_view" type="button" disabled="disabled"><i class="icon zmdi zmdi-folder"></i> 打开</button>
<button class="btn btn-space btn-secondary J_edit" type="button" disabled="disabled"><i class="icon zmdi zmdi-border-color"></i> 编辑</button>
<div class="btn-group btn-space J_action">
<button class="btn btn-secondary dropdown-toggle" type="button" data-toggle="dropdown">更多 <i class="icon zmdi zmdi-more-vert"></i></button>
<div class="dropdown-menu dropdown-menu-right">

View file

@ -46,18 +46,18 @@ public class NavManagerTest extends TestSupport {
}
@Test
public void testPortalNav() throws Exception {
public void testNavBuilder() throws Exception {
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders
.get("/rebuild")
.sessionAttr(WebUtils.CURRENT_USER, UserService.ADMIN_USER);
HttpServletRequest request = builder.buildRequest(new MockServletContext());
JSON navForPortal = NavManager.instance.getNavForPortal(request);
JSONArray navForPortal = NavBuilder.instance.getNavPortal(request);
System.out.println("testPortalNav .......... \n" + navForPortal.toJSONString());
if (!((JSONArray) navForPortal).isEmpty()) {
JSONObject firstNav = (JSONObject) ((JSONArray) navForPortal).get(0);
String navHtml = NavManager.instance.renderNavItem(firstNav, "home");
if (!navForPortal.isEmpty()) {
JSONObject firstNav = (JSONObject) navForPortal.get(0);
String navHtml = NavBuilder.instance.renderNavItem(firstNav, "home");
System.out.println(navHtml);
}
}