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> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<argLine>-Dfile.encoding=UTF-8</argLine> <argLine>-Dfile.encoding=UTF-8</argLine>
<skipTests>true</skipTests> <skipTests>true</skipTests>
<spring.version>4.3.26.RELEASE</spring.version> <spring.version>4.3.27.RELEASE</spring.version>
</properties> </properties>
<build> <build>
@ -139,7 +139,7 @@
<dependency> <dependency>
<groupId>com.github.devezhao</groupId> <groupId>com.github.devezhao</groupId>
<artifactId>momentjava</artifactId> <artifactId>momentjava</artifactId>
<version>91f4df2081</version> <version>0.2.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.github.devezhao</groupId> <groupId>com.github.devezhao</groupId>
@ -149,7 +149,7 @@
<dependency> <dependency>
<groupId>com.github.devezhao</groupId> <groupId>com.github.devezhao</groupId>
<artifactId>commons</artifactId> <artifactId>commons</artifactId>
<version>1.1.7</version> <version>1.2.0</version>
<exclusions> <exclusions>
<exclusion> <exclusion>
<artifactId>httpclient</artifactId> <artifactId>httpclient</artifactId>
@ -160,7 +160,7 @@
<dependency> <dependency>
<groupId>com.github.devezhao</groupId> <groupId>com.github.devezhao</groupId>
<artifactId>persist4j</artifactId> <artifactId>persist4j</artifactId>
<version>1beaf8f565</version> <version>1.4.2</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>junit</groupId> <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; package com.rebuild.server.configuration.portals;
import cn.devezhao.commons.CodecUtils;
import cn.devezhao.persist4j.Entity;
import cn.devezhao.persist4j.engine.ID; import cn.devezhao.persist4j.engine.ID;
import com.alibaba.fastjson.JSON; 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.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.ArrayList;
import java.util.Iterator;
import java.util.List; 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_FEEDS = "$FEEDS$";
// 文件 // 文件
public static final String NAV_FILEMRG = "$FILEMRG$"; public static final String NAV_FILEMRG = "$FILEMRG$";
// 项目看板
public static final String NAV_KANBANMRG = "$KANBANMRG$";
public static final NavManager instance = new NavManager(); public static final NavManager instance = new NavManager();
private NavManager() { } protected NavManager() { }
/** /**
* @param user * @param user
@ -79,160 +66,4 @@ public class NavManager extends BaseLayoutManager {
} }
return array.toArray(new ID[0]); 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), AdminDangers(true),
// 允许同一用户多个会话
MultipleSessions(true),
; ;
private Object defaultVal; private Object defaultVal;

View file

@ -129,6 +129,17 @@ public class OnlineSessionStore extends CurrentCaller implements HttpSessionList
Object loginUser = s.getAttribute(WebUtils.CURRENT_USER); Object loginUser = s.getAttribute(WebUtils.CURRENT_USER);
Assert.notNull(loginUser, "No login user found in session!"); 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_SESSIONS.remove(s);
ONLINE_USERS.put((ID) loginUser, s); ONLINE_USERS.put((ID) loginUser, s);
} }

View file

@ -82,7 +82,9 @@ public class RequestWatchHandler extends HandlerInterceptorAdapter implements In
// for Language // for Language
Application.getSessionStore().setLocale(AppUtils.getLocale(request)); Application.getSessionStore().setLocale(AppUtils.getLocale(request));
// Last active // Last active
Application.getSessionStore().storeLastActive(request); if (!(isIgnoreActive(requestUrl) || ServletUtils.isAjaxRequest(request))) {
Application.getSessionStore().storeLastActive(request);
}
} }
boolean chain = super.preHandle(request, response, handler); boolean chain = super.preHandle(request, response, handler);
@ -217,6 +219,14 @@ public class RequestWatchHandler extends HandlerInterceptorAdapter implements In
|| reqUrl.startsWith("/commons/barcode/render"); || 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("/language/")
|| reqUrl.startsWith("/commons/barcode/"); || reqUrl.startsWith("/commons/barcode/");
} }
} }

View file

@ -1,35 +1,34 @@
/* /*
rebuild - Building your business-systems freely. Copyright (c) REBUILD <https://getrebuild.com/> and its owners. All rights reserved.
Copyright (C) 2019 devezhao <zhaofang123@gmail.com>
This program is free software: you can redistribute it and/or modify rebuild is dual-licensed under commercial and open source licenses (GPLv3).
it under the terms of the GNU General Public License as published by See LICENSE and COMMERCIAL in the project root for license information.
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/>.
*/ */
package com.rebuild.web.admin.bizz; package com.rebuild.web.admin.bizz;
import cn.devezhao.commons.web.WebUtils;
import cn.devezhao.momentjava.Moment;
import cn.devezhao.persist4j.engine.ID; import cn.devezhao.persist4j.engine.ID;
import com.alibaba.fastjson.JSON; 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.configuration.portals.DataListManager;
import com.rebuild.server.service.bizz.UserHelper;
import com.rebuild.utils.JSONUtils;
import com.rebuild.utils.LocationUtils; import com.rebuild.utils.LocationUtils;
import com.rebuild.web.BaseEntityControll; import com.rebuild.web.BaseEntityControll;
import com.rebuild.web.OnlineSessionStore;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException; import java.io.IOException;
import java.util.Date;
/** /**
* @author devezhao-mac zhaofang123@gmail.com * @author devezhao-mac zhaofang123@gmail.com
@ -57,4 +56,41 @@ public class LoginLogControll extends BaseEntityControll {
writeFailure(response); 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 contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page import="com.alibaba.fastjson.JSONObject"%> <%@ page import="com.alibaba.fastjson.JSONObject"%>
<%@ page import="com.alibaba.fastjson.JSONArray"%> <%@ page import="com.alibaba.fastjson.JSONArray"%>
<%@ page import="com.rebuild.server.configuration.portals.NavManager"%>
<%@ page import="com.rebuild.utils.AppUtils" %> <%@ page import="com.rebuild.utils.AppUtils" %>
<%@ page import="com.rebuild.server.service.bizz.privileges.ZeroEntry" %> <%@ page import="com.rebuild.server.service.bizz.privileges.ZeroEntry" %>
<%@ page import="com.rebuild.server.configuration.portals.NavBuilder" %>
<% <%
final String activeNav = request.getParameter("activeNav"); 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="rb-left-sidebar">
<div class="left-sidebar-wrapper"> <div class="left-sidebar-wrapper">
@ -16,14 +16,14 @@ final JSONArray navArray = NavManager.instance.getNavForPortal(request);
<div class="left-sidebar-content no-divider"> <div class="left-sidebar-content no-divider">
<ul class="sidebar-elements"> <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> <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> </ul>
</div> </div>
</div> </div>
</div> </div>
<% if (AppUtils.allow(request, ZeroEntry.AllowCustomNav)) { %> <% if (AppUtils.allow(request, ZeroEntry.AllowCustomNav)) { %>
<div class="bottom-widget"> <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>
<% } %> <% } %>
</div> </div>

View file

@ -48,7 +48,7 @@
</div> </div>
<div class="col-12 col-lg-6"> <div class="col-12 col-lg-6">
<div class="dataTables_oper"> <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> <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"> <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> <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>
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<div class="dataTables_oper"> <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"> <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> <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"> <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-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.jsx" type="text/babel"></script>
<script src="${baseUrl}/assets/js/rb-forms.exts.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"> <script type="text/babel">
RbList.renderAfter = function() { RbList.renderAfter = function() {
let ipAddrIndex = -1 let ipAddrIndex = -1
@ -82,6 +84,9 @@ RbList.renderAfter = function() {
}) })
}) })
} }
$(document).ready(() => {
$('.J_view-online').click(() => renderRbcomp(<OnlineUserViewer />))
})
</script> </script>
</body> </body>
</html> </html>

View file

@ -29,7 +29,7 @@
</div> </div>
<div class="col-12 col-lg-6"> <div class="col-12 col-lg-6">
<div class="dataTables_oper"> <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> <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"> <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> <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>
<div class="col-12 col-lg-7"> <div class="col-12 col-lg-7">
<div class="dataTables_oper"> <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"> <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 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> <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> </label>
</div> </div>
<div class="mb-1"> <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="alert alert-warning alert-icon hide col-sm-6 mb-0">
<div class="icon"><span class="zmdi zmdi-alert-triangle"></span></div> <div class="icon"><span class="zmdi zmdi-alert-triangle"></span></div>
<div class="message">系统内建实体,不允许删除</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') value = $val('.J_menuUrl')
if (!value) { if (!value) {
RbHighbar.create('请输入 URL'); return RbHighbar.create('请输入 URL'); return
} else if (!!value && !$regex.isUrl(value)) { } else if (!($regex.isUrl(value) || $regex.isUrl(`https://getrebuild.com${value}`))) {
RbHighbar.create('请输入有效的 URL') RbHighbar.create('请输入有效的 URL')
return return
} }

View file

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

View file

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

View file

@ -69,8 +69,8 @@
</div> </div>
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<div class="dataTables_oper"> <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-space btn-secondary J_edit" disabled="disabled"><i class="icon zmdi zmdi-border-color"></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> <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"> <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> <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>
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<div class="dataTables_oper"> <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-space btn-secondary J_edit" disabled="disabled"><i class="icon zmdi zmdi-border-color"></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"> <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> <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"> <div class="dropdown-menu dropdown-menu-right">

View file

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