💄 Updating the UI and style files.
This commit is contained in:
1 parent
e1a340d2a5
commit
8d058d57fa
146 files changed
+29716
-576
No files matched your search
@@ -56,3 +56,6 @@ Servers
|
||||
.metadata
|
||||
upload
|
||||
gen_code
|
||||
|
||||
### node
|
||||
node_modules
|
||||
+12
-11
@@ -11,7 +11,7 @@
|
||||
Target Server Version : 50723
|
||||
File Encoding : utf-8
|
||||
|
||||
Date: 11/18/2018 10:19:32 AM
|
||||
Date: 11/19/2018 00:57:56 AM
|
||||
*/
|
||||
|
||||
SET NAMES utf8;
|
||||
@@ -70,13 +70,13 @@ CREATE TABLE `sys_dept` (
|
||||
`parent_id` int(11) DEFAULT NULL,
|
||||
`tenant_id` int(11) DEFAULT NULL,
|
||||
PRIMARY KEY (`dept_id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='部门管理';
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='部门管理';
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of `sys_dept`
|
||||
-- ----------------------------
|
||||
BEGIN;
|
||||
INSERT INTO `sys_dept` VALUES ('1', '山东农信', null, '2018-01-22 19:00:23', '2018-09-13 01:46:29', '0', '0', '1'), ('2', '沙县国际', null, '2018-01-22 19:00:38', '2018-09-13 01:46:30', '0', '0', '1'), ('3', '潍坊农信', null, '2018-01-22 19:00:44', '2018-09-13 01:46:31', '0', '1', '1'), ('4', '高新农信', null, '2018-01-22 19:00:52', '2018-10-06 10:41:52', '0', '3', '1'), ('5', '院校农信', null, '2018-01-22 19:00:57', '2018-10-06 10:42:51', '0', '4', '1'), ('6', '潍坊学院农信', null, '2018-01-22 19:01:06', '2018-09-13 03:29:10', '1', '5', '1'), ('7', '山东沙县', null, '2018-01-22 19:01:57', '2018-09-13 01:46:42', '0', '2', '1'), ('8', '潍坊沙县', null, '2018-01-22 19:02:03', '2018-09-13 01:46:43', '0', '7', '1'), ('9', '高新沙县', null, '2018-01-22 19:02:14', '2018-09-13 01:46:44', '1', '8', '1');
|
||||
INSERT INTO `sys_dept` VALUES ('1', '山东农信', null, '2018-01-22 19:00:23', '2018-09-13 01:46:29', '0', '0', '1'), ('2', '沙县国际', null, '2018-01-22 19:00:38', '2018-09-13 01:46:30', '0', '0', '1'), ('3', '潍坊农信', null, '2018-01-22 19:00:44', '2018-09-13 01:46:31', '0', '1', '1'), ('4', '高新农信', null, '2018-01-22 19:00:52', '2018-10-06 10:41:52', '0', '3', '1'), ('5', '院校农信', null, '2018-01-22 19:00:57', '2018-10-06 10:42:51', '0', '4', '1'), ('6', '潍坊学院农信', null, '2018-01-22 19:01:06', '2018-09-13 03:29:10', '1', '5', '1'), ('7', '山东沙县', null, '2018-01-22 19:01:57', '2018-09-13 01:46:42', '0', '2', '1'), ('8', '潍坊沙县', null, '2018-01-22 19:02:03', '2018-09-13 01:46:43', '0', '7', '1'), ('9', '高新沙县', null, '2018-01-22 19:02:14', '2018-09-13 01:46:44', '1', '8', '1'), ('10', '租户2', null, '2018-11-18 13:27:11', '2018-11-18 13:42:19', '0', '0', '2');
|
||||
COMMIT;
|
||||
|
||||
-- ----------------------------
|
||||
@@ -95,7 +95,7 @@ CREATE TABLE `sys_dept_relation` (
|
||||
-- Records of `sys_dept_relation`
|
||||
-- ----------------------------
|
||||
BEGIN;
|
||||
INSERT INTO `sys_dept_relation` VALUES ('1', '1'), ('1', '3'), ('1', '4'), ('1', '5'), ('2', '2'), ('2', '7'), ('2', '8'), ('3', '3'), ('3', '4'), ('3', '5'), ('4', '4'), ('4', '5'), ('5', '5'), ('7', '7'), ('7', '8'), ('8', '8');
|
||||
INSERT INTO `sys_dept_relation` VALUES ('1', '1'), ('1', '3'), ('1', '4'), ('1', '5'), ('2', '2'), ('2', '7'), ('2', '8'), ('3', '3'), ('3', '4'), ('3', '5'), ('4', '4'), ('4', '5'), ('5', '5'), ('7', '7'), ('7', '8'), ('8', '8'), ('10', '10');
|
||||
COMMIT;
|
||||
|
||||
-- ----------------------------
|
||||
@@ -108,7 +108,7 @@ CREATE TABLE `sys_dict` (
|
||||
`label` varchar(100) NOT NULL COMMENT '标签名',
|
||||
`type` varchar(100) NOT NULL COMMENT '类型',
|
||||
`description` varchar(100) NOT NULL COMMENT '描述',
|
||||
`sort` int(11) NOT NULL COMMENT '排序(升序)',
|
||||
`sort` int(10) NOT NULL COMMENT '排序(升序)',
|
||||
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`remarks` varchar(255) DEFAULT NULL COMMENT '备注信息',
|
||||
@@ -153,13 +153,13 @@ CREATE TABLE `sys_log` (
|
||||
KEY `sys_log_request_uri` (`request_uri`),
|
||||
KEY `sys_log_type` (`type`),
|
||||
KEY `sys_log_create_date` (`create_time`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='日志表';
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=52 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='日志表';
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of `sys_log`
|
||||
-- ----------------------------
|
||||
BEGIN;
|
||||
INSERT INTO `sys_log` VALUES ('1', '0', '添加用户', 'pig', 'admin', '2018-11-14 12:43:47', null, '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36', '/user', 'POST', '', '122', '0', null, '1'), ('2', '0', '添加角色', 'pig', 'admin', '2018-11-17 23:58:45', null, '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36', '/role', 'POST', '', '98', '0', null, '1'), ('3', '0', '删除角色', 'pig', 'admin', '2018-11-17 23:58:48', null, '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36', '/role/5', 'DELETE', '', '46', '0', null, '1'), ('4', '0', '更新角色菜单', 'pig', 'admin', '2018-11-18 00:29:51', null, '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36', '/role/upd-menu', 'PUT', 'roleId=%5B1%5D&menuIds=%5B1000%2C1100%2C1101%2C1102%2C1103%2C1200%2C1201%2C1202%2C1203%2C1300%2C1301%2C1302%2C1303%2C1304%2C1400%2C1401%2C1402%2C1403%2C2000%2C2100%2C2101%2C2200%2C2201%2C2202%2C2203%2C2300%2C2400%2C2401%2C2402%2C2403%2C2500%2C2501%2C2502%2C2503%2C2600%2C2601%2C2700%2C3000%2C3100%2C3200%2C3300%2C3400%2C3500%2C3600%2C3601%2C3700%2C3800%2C4000%2C4100%2C4101%2C4200%2C4201%2C4300%2C4301%2C4302%2C4303%2C4400%2C4401%2C5000%2C5001%2C5002%2C%5D', '226', '0', null, '1'), ('5', '0', '修改字典', 'pig', 'admin', '2018-11-18 01:06:22', null, '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36', '/dict', 'PUT', '', '42', '0', null, '1');
|
||||
INSERT INTO `sys_log` VALUES ('1', '0', '添加用户', 'pig', 'admin', '2018-11-14 12:43:47', null, '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36', '/user', 'POST', '', '122', '0', null, '1'), ('2', '0', '添加角色', 'pig', 'admin', '2018-11-17 23:58:45', null, '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36', '/role', 'POST', '', '98', '0', null, '1'), ('3', '0', '删除角色', 'pig', 'admin', '2018-11-17 23:58:48', null, '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36', '/role/5', 'DELETE', '', '46', '0', null, '1'), ('4', '0', '更新角色菜单', 'pig', 'admin', '2018-11-18 00:29:51', null, '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36', '/role/upd-menu', 'PUT', 'roleId=%5B1%5D&menuIds=%5B1000%2C1100%2C1101%2C1102%2C1103%2C1200%2C1201%2C1202%2C1203%2C1300%2C1301%2C1302%2C1303%2C1304%2C1400%2C1401%2C1402%2C1403%2C2000%2C2100%2C2101%2C2200%2C2201%2C2202%2C2203%2C2300%2C2400%2C2401%2C2402%2C2403%2C2500%2C2501%2C2502%2C2503%2C2600%2C2601%2C2700%2C3000%2C3100%2C3200%2C3300%2C3400%2C3500%2C3600%2C3601%2C3700%2C3800%2C4000%2C4100%2C4101%2C4200%2C4201%2C4300%2C4301%2C4302%2C4303%2C4400%2C4401%2C5000%2C5001%2C5002%2C%5D', '226', '0', null, '1'), ('5', '0', '修改字典', 'pig', 'admin', '2018-11-18 01:06:22', null, '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36', '/dict', 'PUT', '', '42', '0', null, '1'), ('6', '0', '添加部门', 'pig', 'admin', '2018-11-18 13:27:16', null, '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36', '/dept', 'POST', '', '4455', '0', null, '2'), ('7', '0', '添加部门', 'pig', 'admin', '2018-11-18 13:28:12', null, '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36', '/dept', 'POST', '', '13124', '0', null, '2'), ('8', '0', '删除部门', 'pig', 'admin', '2018-11-18 13:28:44', null, '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36', '/dept/14', 'DELETE', '', '20', '0', null, '2'), ('9', '0', '添加部门', 'pig', 'admin', '2018-11-18 13:32:18', null, '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36', '/dept', 'POST', '', '17', '0', null, '2'), ('10', '0', '添加部门', 'pig', 'admin', '2018-11-18 13:32:31', null, '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36', '/dept', 'POST', '', '13', '0', null, '2'), ('11', '0', '删除部门', 'pig', 'admin', '2018-11-18 13:33:25', null, '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36', '/dept/15', 'DELETE', '', '10', '0', null, '2'), ('12', '0', '添加部门', 'pig', 'admin', '2018-11-18 13:43:46', null, '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36', '/dept', 'POST', '', '58', '0', null, '2'), ('13', '0', '添加部门', 'pig', 'admin', '2018-11-18 13:43:55', null, '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36', '/dept', 'POST', '', '12', '0', null, '2'), ('14', '0', '删除部门', 'pig', 'admin', '2018-11-18 13:44:32', null, '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36', '/dept/11', 'DELETE', '', '20', '0', null, '2'), ('15', '0', '发送验证码', null, 'anonymousUser', '2018-11-18 15:05:14', null, '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36', '/mobile/17034642119', 'GET', '', '23', '0', null, '2'), ('16', '0', '发送验证码', null, 'anonymousUser', '2018-11-18 15:05:39', null, '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36', '/mobile/17034642888', 'GET', '', '19', '0', null, '2'), ('17', '0', '发送验证码', null, 'anonymousUser', '2018-11-18 15:06:55', null, '127.0.0.1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36', '/mobile/17034642888', 'GET', '', '21', '0', null, '2'), ('18', '0', '发送验证码', null, 'anonymousUser', '2018-11-18 15:09:03', nullLine truncated
|
||||
COMMIT;
|
||||
|
||||
-- ----------------------------
|
||||
@@ -175,6 +175,7 @@ CREATE TABLE `sys_menu` (
|
||||
`icon` varchar(32) DEFAULT NULL COMMENT '图标',
|
||||
`component` varchar(64) DEFAULT NULL COMMENT 'VUE页面',
|
||||
`sort` int(11) DEFAULT '1' COMMENT '排序值',
|
||||
`keepalive` char(1) DEFAULT '0' COMMENT '0-开启,1- 关闭',
|
||||
`type` char(1) DEFAULT NULL COMMENT '菜单类型 (0菜单 1按钮)',
|
||||
`create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
@@ -187,7 +188,7 @@ CREATE TABLE `sys_menu` (
|
||||
-- Records of `sys_menu`
|
||||
-- ----------------------------
|
||||
BEGIN;
|
||||
INSERT INTO `sys_menu` VALUES ('1000', '权限管理', null, '/upms', '-1', 'icon-quanxianguanli', 'Layout', '0', '0', '2018-09-28 08:29:53', '2018-09-28 08:53:01', '0', '1'), ('1100', '用户管理', null, 'user', '1000', 'icon-yonghuguanli', 'views/admin/user/index', '1', '0', '2017-11-02 22:24:37', '2018-09-28 09:00:41', '0', '1'), ('1101', '用户新增', 'sys_user_add', null, '1100', null, null, null, '1', '2017-11-08 09:52:09', '2018-09-28 09:06:34', '0', '1'), ('1102', '用户修改', 'sys_user_edit', null, '1100', null, null, null, '1', '2017-11-08 09:52:48', '2018-09-28 09:06:37', '0', '1'), ('1103', '用户删除', 'sys_user_del', null, '1100', null, null, null, '1', '2017-11-08 09:54:01', '2018-09-28 09:06:42', '0', '1'), ('1200', '菜单管理', null, 'menu', '1000', 'icon-caidanguanli', 'views/admin/menu/index', '2', '0', '2017-11-08 09:57:27', '2018-09-28 09:00:45', '0', '1'), ('1201', '菜单新增', 'sys_menu_add', null, '1200', null, null, null, '1', '2017-11-08 10:15:53', '2018-09-28 09:07:16', '0', '1'), ('1202', '菜单修改', 'sys_menu_edit', null, '1200', null, null, null, '1', '2017-11-08 10:16:23', '2018-09-28 09:07:18', '0', '1'), ('1203', '菜单删除', 'sys_menu_del', null, '1200', null, null, null, '1', '2017-11-08 10:16:43', '2018-09-28 09:07:22', '0', '1'), ('1300', '角色管理', null, 'role', '1000', 'icon-jiaoseguanli', 'views/admin/role/index', '3', '0', '2017-11-08 10:13:37', '2018-09-28 09:00:48', '0', '1'), ('1301', '角色新增', 'sys_role_add', null, '1300', null, null, null, '1', '2017-11-08 10:14:18', '2018-09-28 09:07:46', '0', '1'), ('1302', '角色修改', 'sys_role_edit', null, '1300', null, null, null, '1', '2017-11-08 10:14:41', '2018-09-28 09:07:49', '0', '1'), ('1303', '角色删除', 'sys_role_del', null, '1300', null, null, null, '1', '2017-11-08 10:14:59', '2018-09-28 09:07:53', '0', '1'), ('1304', '分配权限', 'sys_role_perm', null, '1300', null, null, null, '1', '2018-04-20 07:22:55', '2018-09-28 09:13:23', '0', '1'), ('1400', '部门管理', null, 'dept', '1000', 'icon-web-icon-', 'views/admin/dept/index', '4', '0', '2018-01-20 13:17:19', '2018-09-28 09:00:52', '0', '1'), ('1401', '部门新增', 'sys_dept_add', null, '1400', null, null, null, '1', '2018-01-20 14:56:16', '2018-09-28 09:08:13', '0', '1'), ('1402', '部门修改', 'sys_dept_edit', null, '1400', null, null, null, '1', '2018-01-20 14:56:59', '2018-09-28 09:08:16', '0', '1'), ('1403', '部门删除', 'sys_dept_del', null, '1400', null, null, null, '1', '2018-01-20 14:57:28', '2018-09-28 09:08:18', '0', '1'), ('2000', '系统管理', null, '/admin', '-1', 'icon-xitongguanli', 'Layout', '1', '0', '2017-11-07 20:56:00', '2018-09-28 08:53:18', '0', '1'), ('2100', '日志管理', null, 'log', '2000', 'icon-rizhiguanli', 'views/admin/log/index', '5', '0', '2017-11-20 14:06:22', '2018-09-28 09:01:52', '0', '1'), ('2101', '日志删除', 'sys_log_del', null, '2100', null, null, null, '1', '2017-11-20 20:37:37', '2018-09-28 09:08:44', '0', '1'), ('2200', '字典管理', null, 'dict', '2000', 'icon-navicon-zdgl', 'views/admin/dict/index', '6', '0', '2017-11-29 11:30:52', '2018-09-28 09:01:47', '0', '1'), ('2201', '字典删除', 'sys_dict_del', null, '2200', null, null, null, '1', '2017-11-29 11:30:11', '2018-09-28 09:09:10', '0', '1'), ('2202', '字典新增', 'sys_dict_add', null, '2200', null, null, null, '1', '2018-05-11 22:34:55', '2018-09-28 09:09:12', '0', '1'), ('2203', '字典修改', 'sys_dict_edit', null, '2200', null, null, null, '1', '2018-05-11 22:36:03', '2018-09-28 09:09:16', '0', '1'), ('2300', '代码生成', '', 'gen', '2000', 'icon-weibiaoti46', 'views/admin/gen/index', '8', '0', '2018-01-20 13:17:19', '2018-09-28 09:02:54', '0', '1'), ('2400', '终端管理', '', 'client', '2000', 'icon-shouji', 'views/admin/client/index', '9', '0', '2018-01-20 13:17:19', '2018-09-28 09:01:43', '0', '1'), ('2401', '客户端新增', 'sys_client_add', null, '2400', '1', null, null, '1', '2018-05-15 21:35:18', '2018-09-28 09:10:25', '0', '1'), ('2402', '客户端修改', 'sys_client_edit', null, '2400', null, null, null, '1', '2018-05-15 21:37:06', '2018-09-28 09:10:27', '0', '1'), ('2403', '客户端删除', 'sys_client_del', null, '2400', null, null, null, '1', '2018-05-15 21:39:16', '2018-09-28 09:10:30', '0', '1'), ('2500', '密钥管理', '', 'social', '2000', 'icon-miyue', 'views/admin/social/index', '10', '0', '2018-01-20 13:17:19', '2018-09-28 09:01:41', '0', '1'), ('2501', '密钥新增', 'generator_syssocialdetails_add', null, '2500', '1', null, '0', '1', '2018-05-15 21:35:18', '2018-09-28 09:11:02', '0', '1'), ('2502', '密钥修改', 'generator_syssocialdetails_edit', null, '2500', '1', null, '1', '1', '2018-05-15 21:35:18', '2018-09-28 09:11:04', '0', '1'), ('2503', '密钥删除', 'generator_syssocialdetails_del', null, '2500', '1', null, '2', '1', '2018-05-15 21:35:18', '2018-09-28 09:11:06', '0', '1'), ('2600', '令牌管理', null, 'token', '2000', 'icon-denglvlingpai', 'vieLine truncated
|
||||
INSERT INTO `sys_menu` VALUES ('1000', '权限管理', null, '/upms', '-1', 'icon-quanxianguanli', 'Layout', '0', '0', '0', '2018-09-28 08:29:53', '2018-09-28 08:53:01', '0', '1'), ('1100', '用户管理', null, 'user', '1000', 'icon-yonghuguanli', 'views/admin/user/index', '1', '0', '0', '2017-11-02 22:24:37', '2018-09-28 09:00:41', '0', '1'), ('1101', '用户新增', 'sys_user_add', null, '1100', null, null, null, '0', '1', '2017-11-08 09:52:09', '2018-09-28 09:06:34', '0', '1'), ('1102', '用户修改', 'sys_user_edit', null, '1100', null, null, null, '0', '1', '2017-11-08 09:52:48', '2018-09-28 09:06:37', '0', '1'), ('1103', '用户删除', 'sys_user_del', null, '1100', null, null, null, '0', '1', '2017-11-08 09:54:01', '2018-09-28 09:06:42', '0', '1'), ('1200', '菜单管理', null, 'menu', '1000', 'icon-caidanguanli', 'views/admin/menu/index', '2', '0', '0', '2017-11-08 09:57:27', '2018-09-28 09:00:45', '0', '1'), ('1201', '菜单新增', 'sys_menu_add', null, '1200', null, null, null, '0', '1', '2017-11-08 10:15:53', '2018-09-28 09:07:16', '0', '1'), ('1202', '菜单修改', 'sys_menu_edit', null, '1200', null, null, null, '0', '1', '2017-11-08 10:16:23', '2018-09-28 09:07:18', '0', '1'), ('1203', '菜单删除', 'sys_menu_del', null, '1200', null, null, null, '0', '1', '2017-11-08 10:16:43', '2018-09-28 09:07:22', '0', '1'), ('1300', '角色管理', null, 'role', '1000', 'icon-jiaoseguanli', 'views/admin/role/index', '3', '0', '0', '2017-11-08 10:13:37', '2018-09-28 09:00:48', '0', '1'), ('1301', '角色新增', 'sys_role_add', null, '1300', null, null, null, '0', '1', '2017-11-08 10:14:18', '2018-09-28 09:07:46', '0', '1'), ('1302', '角色修改', 'sys_role_edit', null, '1300', null, null, null, '0', '1', '2017-11-08 10:14:41', '2018-09-28 09:07:49', '0', '1'), ('1303', '角色删除', 'sys_role_del', null, '1300', null, null, null, '0', '1', '2017-11-08 10:14:59', '2018-09-28 09:07:53', '0', '1'), ('1304', '分配权限', 'sys_role_perm', null, '1300', null, null, null, '0', '1', '2018-04-20 07:22:55', '2018-09-28 09:13:23', '0', '1'), ('1400', '部门管理', null, 'dept', '1000', 'icon-web-icon-', 'views/admin/dept/index', '4', '0', '0', '2018-01-20 13:17:19', '2018-09-28 09:00:52', '0', '1'), ('1401', '部门新增', 'sys_dept_add', null, '1400', null, null, null, '0', '1', '2018-01-20 14:56:16', '2018-09-28 09:08:13', '0', '1'), ('1402', '部门修改', 'sys_dept_edit', null, '1400', null, null, null, '0', '1', '2018-01-20 14:56:59', '2018-09-28 09:08:16', '0', '1'), ('1403', '部门删除', 'sys_dept_del', null, '1400', null, null, null, '0', '1', '2018-01-20 14:57:28', '2018-09-28 09:08:18', '0', '1'), ('2000', '系统管理', null, '/admin', '-1', 'icon-xitongguanli', 'Layout', '1', '0', '0', '2017-11-07 20:56:00', '2018-09-28 08:53:18', '0', '1'), ('2100', '日志管理', null, 'log', '2000', 'icon-rizhiguanli', 'views/admin/log/index', '5', '0', '0', '2017-11-20 14:06:22', '2018-09-28 09:01:52', '0', '1'), ('2101', '日志删除', 'sys_log_del', null, '2100', null, null, null, '0', '1', '2017-11-20 20:37:37', '2018-09-28 09:08:44', '0', '1'), ('2200', '字典管理', null, 'dict', '2000', 'icon-navicon-zdgl', 'views/admin/dict/index', '6', '0', '0', '2017-11-29 11:30:52', '2018-09-28 09:01:47', '0', '1'), ('2201', '字典删除', 'sys_dict_del', null, '2200', null, null, null, '0', '1', '2017-11-29 11:30:11', '2018-09-28 09:09:10', '0', '1'), ('2202', '字典新增', 'sys_dict_add', null, '2200', null, null, null, '0', '1', '2018-05-11 22:34:55', '2018-09-28 09:09:12', '0', '1'), ('2203', '字典修改', 'sys_dict_edit', null, '2200', null, null, null, '0', '1', '2018-05-11 22:36:03', '2018-09-28 09:09:16', '0', '1'), ('2300', '代码生成', '', 'gen', '2000', 'icon-weibiaoti46', 'views/admin/gen/index', '8', '0', '0', '2018-01-20 13:17:19', '2018-09-28 09:02:54', '0', '1'), ('2400', '终端管理', '', 'client', '2000', 'icon-shouji', 'views/admin/client/index', '9', '0', '0', '2018-01-20 13:17:19', '2018-09-28 09:01:43', '0', '1'), ('2401', '客户端新增', 'sys_client_add', null, '2400', '1', null, null, '0', '1', '2018-05-15 21:35:18', '2018-09-28 09:10:25', '0', '1'), ('2402', '客户端修改', 'sys_client_edit', null, '2400', null, null, null, '0', '1', '2018-05-15 21:37:06', '2018-09-28 09:10:27', '0', '1'), ('2403', '客户端删除', 'sys_client_del', null, '2400', null, null, null, '0', '1', '2018-05-15 21:39:16', '2018-09-28 09:10:30', '0', '1'), ('2500', '密钥管理', '', 'social', '2000', 'icon-miyue', 'views/admin/social/index', '10', '0', '0', '2018-01-20 13:17:19', '2018-09-28 09:01:41', '0', '1'), ('2501', '密钥新增', 'generator_syssocialdetails_add', null, '2500', '1', null, '0', '0', '1', '2018-05-15 21:35:18', '2018-09-28 09:11:02', '0', '1'), ('2502', '密钥修改', 'generator_syssocialdetails_edit', null, '2500', '1', null, '1', '0', '1', '2018-05-15 21:35:18', '2018-09-28 09:11:04', '0', '1'), ('2503', '密钥删除', 'generator_syssocialdetails_del', nLine truncated
|
||||
COMMIT;
|
||||
|
||||
-- ----------------------------
|
||||
@@ -232,13 +233,13 @@ CREATE TABLE `sys_role` (
|
||||
`tenant_id` int(11) DEFAULT NULL,
|
||||
PRIMARY KEY (`role_id`),
|
||||
UNIQUE KEY `role_idx1_role_code` (`role_code`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=DYNAMIC;
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of `sys_role`
|
||||
-- ----------------------------
|
||||
BEGIN;
|
||||
INSERT INTO `sys_role` VALUES ('1', 'admin', 'ROLE_ADMIN', '超级管理员', '2017-10-29 15:45:51', '2018-09-13 01:46:01', '0', '1'), ('2', 'ROLE_CQQ', 'ROLE_CQQ', 'ROLE_CQQ', '2018-11-11 19:42:26', '2018-11-11 20:26:01', '0', '2'), ('3', '测试角色', 'ROLE_TEST', '测试专用角色', '2018-11-14 12:43:18', null, '0', '1'), ('4', 'TOLE_TEST1', 'ROLE_TEST1', 'ROLE_TEST1', '2018-11-17 15:49:13', '2018-11-17 16:06:08', '1', '1'), ('5', 'sadsa', 'dsada', 'asdas', '2018-11-17 23:58:45', '2018-11-17 23:58:48', '1', '1');
|
||||
INSERT INTO `sys_role` VALUES ('1', 'admin', 'ROLE_ADMIN', '超级管理员', '2017-10-29 15:45:51', '2018-09-13 01:46:01', '0', '1'), ('2', 'ROLE_CQQ', 'ROLE_CQQ', 'ROLE_CQQ', '2018-11-11 19:42:26', '2018-11-11 20:26:01', '0', '2'), ('3', '测试角色', 'ROLE_TEST', '测试专用角色', '2018-11-14 12:43:18', null, '0', '1'), ('4', 'TOLE_TEST1', 'ROLE_TEST1', 'ROLE_TEST1', '2018-11-17 15:49:13', '2018-11-17 16:06:08', '1', '1');
|
||||
COMMIT;
|
||||
|
||||
-- ----------------------------
|
||||
@@ -336,7 +337,7 @@ CREATE TABLE `sys_user` (
|
||||
-- Records of `sys_user`
|
||||
-- ----------------------------
|
||||
BEGIN;
|
||||
INSERT INTO `sys_user` VALUES ('1', 'admin', '$2a$10$RpFJjxYiXdEsAGnWp/8fsOetMuOON96Ntk/Ym2M/RKRyU0GZseaDC', null, '17034642888', null, '1', '2018-04-20 07:15:18', '2018-11-16 23:31:30', '0', '0', 'o_0FT0uyg_H1vVy2H0JpSwlVGhWQ', null, '1'), ('2', 'admin', '$2a$10$RpFJjxYiXdEsAGnWp/8fsOetMuOON96Ntk/Ym2M/RKRyU0GZseaDC', null, '17034642888', null, '1', '2018-04-20 07:15:18', '2018-11-17 17:23:54', '0', '0', 'o_0FT0uyg_H1vVy2H0JpSwlVGhWQ', null, '2'), ('3', 'test', '$2a$10$vb.AEz6fpVVdiwr3m8H29.Qc8.GsP6fGW9vZFTsUhgvzg2zpsl5p2', null, null, null, '1', '2018-11-14 12:43:47', '2018-11-17 17:23:27', '1', '0', null, null, '1');
|
||||
INSERT INTO `sys_user` VALUES ('1', 'admin', '$2a$10$RpFJjxYiXdEsAGnWp/8fsOetMuOON96Ntk/Ym2M/RKRyU0GZseaDC', null, '17034642888', null, '1', '2018-04-20 07:15:18', '2018-11-16 23:31:30', '0', '0', 'o_0FT0uyg_H1vVy2H0JpSwlVGhWQ', null, '1'), ('2', 'admin', '$2a$10$RpFJjxYiXdEsAGnWp/8fsOetMuOON96Ntk/Ym2M/RKRyU0GZseaDC', null, '17034642888', null, '10', '2018-04-20 07:15:18', '2018-11-18 13:43:03', '0', '0', 'o_0FT0uyg_H1vVy2H0JpSwlVGhWQ', null, '2'), ('3', 'test', '$2a$10$vb.AEz6fpVVdiwr3m8H29.Qc8.GsP6fGW9vZFTsUhgvzg2zpsl5p2', null, null, null, '1', '2018-11-14 12:43:47', '2018-11-17 17:23:27', '1', '0', null, null, '1');
|
||||
COMMIT;
|
||||
|
||||
-- ----------------------------
|
||||
|
||||
@@ -4,3 +4,10 @@ spring:
|
||||
user:
|
||||
name: ENC(rZHA4LW5hHmhLAAzJoFNag==) # pigx
|
||||
password: ENC(bjeyh+Aeii3kHXkoo00ZUw==) # pigx
|
||||
boot:
|
||||
admin:
|
||||
ui:
|
||||
resource-locations: 'classpath:/ui/,classpath:/static/'
|
||||
template-location: 'classpath:/ui/'
|
||||
title: 'pigx 服务状态监控'
|
||||
brand: 'pigx 服务状态监控'
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/app'
|
||||
]
|
||||
};
|
||||
+16315
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,132 @@
|
||||
{
|
||||
"name": "spring-boot-admin-server-ui",
|
||||
"description": "Spring Boot Admin UI",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"watch": "vue-cli-service build --watch",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint",
|
||||
"test:unit": "vue-cli-service test:unit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.8",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.5.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^5.5.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.5.0",
|
||||
"@fortawesome/vue-fontawesome": "^0.1.2",
|
||||
"ansi_up": "^3.0.0",
|
||||
"autolinker": "^1.7.1",
|
||||
"axios": "^0.18.0",
|
||||
"bulma": "^0.7.2",
|
||||
"bulma-badge": "^2.0.0",
|
||||
"d3-array": "^1.2.4",
|
||||
"d3-axis": "^1.0.12",
|
||||
"d3-brush": "^1.0.6",
|
||||
"d3-scale": "^2.1.2",
|
||||
"d3-selection": "^1.3.2",
|
||||
"d3-shape": "^1.2.2",
|
||||
"d3-time": "^1.0.10",
|
||||
"event-source-polyfill": "^1.0.3",
|
||||
"lodash": "^4.17.11",
|
||||
"moment": "^2.22.2",
|
||||
"moment-shortformat": "^2.1.0",
|
||||
"popper.js": "^1.14.4",
|
||||
"pretty-bytes": "^5.1.0",
|
||||
"resize-observer-polyfill": "^1.5.0",
|
||||
"rxjs": "^6.3.3",
|
||||
"vue": "^2.5.17",
|
||||
"vue-clickaway": "2.2.1",
|
||||
"vue-router": "^3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "^3.1.1",
|
||||
"@vue/cli-plugin-eslint": "^3.1.4",
|
||||
"@vue/cli-plugin-unit-jest": "^3.1.1",
|
||||
"@vue/cli-service": "^3.1.2",
|
||||
"@vue/eslint-config-standard": "^4.0.0",
|
||||
"@vue/test-utils": "^1.0.0-beta.20",
|
||||
"babel-core": "7.0.0-bridge.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babel-jest": "^23.6.0",
|
||||
"eslint": "^5.8.0",
|
||||
"eslint-plugin-vue": "^5.0.0-0",
|
||||
"html-loader": "^0.5.5",
|
||||
"node-sass": "^4.9.4",
|
||||
"sass-loader": "^7.0.1",
|
||||
"vue-template-compiler": "^2.5.17",
|
||||
"webpack-bundle-analyzer": "^3.0.3"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:vue/strongly-recommended"
|
||||
],
|
||||
"rules": {
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
],
|
||||
"no-console": [
|
||||
"error",
|
||||
{
|
||||
"allow": [
|
||||
"warn"
|
||||
]
|
||||
}
|
||||
],
|
||||
"vue/max-attributes-per-line": "off"
|
||||
},
|
||||
"parserOptions": {
|
||||
"parser": "babel-eslint"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"*.spec.js"
|
||||
],
|
||||
"env": {
|
||||
"jest": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"postcss": {
|
||||
"plugins": {
|
||||
"autoprefixer": {}
|
||||
}
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not ie <= 8"
|
||||
],
|
||||
"jest": {
|
||||
"setupFiles": [
|
||||
"<rootDir>/src/test/frontend/globals.js"
|
||||
],
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"vue"
|
||||
],
|
||||
"transform": {
|
||||
"^.+\\.vue$": "vue-jest",
|
||||
".+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$": "jest-transform-stub",
|
||||
"^.+\\.js$": "babel-jest"
|
||||
},
|
||||
"moduleNameMapper": {
|
||||
"^@/(.*)$": "<rootDir>/src/main/frontend/$1"
|
||||
},
|
||||
"snapshotSerializers": [
|
||||
"jest-serializer-vue"
|
||||
],
|
||||
"testMatch": [
|
||||
"**/*.spec.js"
|
||||
],
|
||||
"testURL": "http://example.com"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*!
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
*
|
||||
* 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 the initial variables
|
||||
@import "~bulma/sass/utilities/initial-variables";
|
||||
@import "~bulma/sass/utilities/functions";
|
||||
|
||||
$turquoise: rgb(66, 211, 165);
|
||||
|
||||
// overrides
|
||||
$primary: $turquoise;
|
||||
$primary-invert: $white;
|
||||
|
||||
// Add new color variables to the color map.
|
||||
@import "~bulma/sass/utilities/derived-variables.sass";
|
||||
|
||||
$addColors: ();
|
||||
$colors: map-merge($colors, $addColors);
|
||||
|
||||
$link: $primary;
|
||||
|
||||
$modal-card-head-background-color: $white;
|
||||
$modal-content-width: calc(100vh - 40px);
|
||||
|
||||
$navbar-background-color: $black-ter;
|
||||
$navbar-item-color: $grey;
|
||||
$navbar-item-hover-color: $white;
|
||||
$navbar-item-hover-background-color: $navbar-background-color;
|
||||
$navbar-item-active-color: $white;
|
||||
$navbar-item-active-background-color: $navbar-background-color;
|
||||
|
||||
@import "~bulma/sass/utilities/mixins.sass";
|
||||
|
||||
// for calculations / originally rem
|
||||
$navbar-height-px: 52px;
|
||||
@@ -0,0 +1,140 @@
|
||||
/*!
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
*
|
||||
* 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 "utilities";
|
||||
// Import the rest of Bulma
|
||||
@import "~bulma/bulma";
|
||||
@import "~bulma-badge/dist/css/bulma-badge";
|
||||
|
||||
html {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
body {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
& > div {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.is-breakable {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
pre.is-breakable {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.monospaced {
|
||||
font-family: $family-monospace;
|
||||
}
|
||||
|
||||
.is-muted {
|
||||
color: $grey-light;
|
||||
}
|
||||
|
||||
.is-selectable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button.is-icon {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.navbar-link::after {
|
||||
border-color: currentColor;
|
||||
}
|
||||
|
||||
.navbar .badge {
|
||||
&[data-badge]::after {
|
||||
box-shadow: 0 0 0 $badge-border-size $navbar-background-color;
|
||||
}
|
||||
}
|
||||
|
||||
//Loading spinner
|
||||
.section.is-loading,
|
||||
.content.is-loading {
|
||||
&::after {
|
||||
@include loader;
|
||||
height: 5rem;
|
||||
width: 5rem;
|
||||
left: calc(50% - 2.5em);
|
||||
top: calc(50% - 2.5em);
|
||||
border-width: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
p.is-loading {
|
||||
&::before {
|
||||
display: inline-block;
|
||||
right: 0.25em;
|
||||
@include loader;
|
||||
}
|
||||
}
|
||||
|
||||
//hero as card-header
|
||||
.card .hero {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
//Bullets
|
||||
.has-bullet::before {
|
||||
background: currentColor;
|
||||
content: '';
|
||||
width: 0.75em;
|
||||
height: 0.75em;
|
||||
display: inline-block;
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
@each $name, $pair in $colors {
|
||||
.has-bullet.has-bullet-#{$name}::before {
|
||||
$color: nth($pair, 1);
|
||||
background-color: $color;
|
||||
}
|
||||
}
|
||||
|
||||
//D3 Charts
|
||||
.tick {
|
||||
& line {
|
||||
stroke: currentColor;
|
||||
}
|
||||
& text {
|
||||
fill: currentColor;
|
||||
font-size: $size-7;
|
||||
}
|
||||
}
|
||||
|
||||
path.domain {
|
||||
stroke: currentColor;
|
||||
}
|
||||
|
||||
//for login.html
|
||||
.login {
|
||||
background-color: $black-ter;
|
||||
|
||||
&--logo {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 7.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.5 KiB |
@@ -0,0 +1,9 @@
|
||||
<svg viewBox="0 0 135.297 120.36666" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<style>.hex{fill:#42d3a5;} .line{fill:#fff;}</style>
|
||||
</defs>
|
||||
<path d="M133.605291 66.312792l-27.6913077 47.9628009c-2.1826121 3.7799055-6.2171779 6.1091806-10.5824021 6.1091806H39.9489658c-4.3652242 0-8.3984567-2.3292751-10.5824021-6.1091806L1.6765893 66.312792c-2.1826121-3.7799055-2.1826121-8.4384557 0-12.2196945L29.3665637 6.1316299C31.5505091 2.3503911 35.5837416.021116 39.9489658.021116h55.3826154c4.3652242 0 8.39979 2.3292751 10.5824021 6.1105139l27.6913077 47.9614676c2.1826121 3.7812388 2.1826121 8.439789 0 12.2196945"
|
||||
class="hex"/>
|
||||
<path d="M61.53275952 19.96761733c-1.41466463.03066923-2.66193345.92969675-3.14052148 2.26164345l-17.8355541 49.69575758-5.85535361-9.759556c-.6159846-1.02927427-1.72965676-1.6601585-2.92962676-1.6601585H.05149659c.04970976 2.00834979.5878153 4.00699982 1.62695933 5.80645483l.59373516 1.02727432h27.56531085l8.57008574 14.28264293c.62131447 1.0359741 1.73705657 1.66015849 2.9276268 1.66015849.1119972 0 .22197446-.00559986.33397166-.01757956 1.31466713-.1279968 2.43673908-1.00337492 2.88472788-2.24604385l17.25756855-48.0947976 18.95252617 46.4108397c.48531787 1.18667033 1.5956601 2.00194995 2.87292818 2.11124722 1.26926826.11332717 2.5074373-.50440739 3.1874203-1.59376016l15.14962126-24.28039297 8.8435789 11.5637109c.6479838.84530887 1.65345867 1.34176645 2.71473213 1.34176645h18.8235294l1.24996875-2.16594585c.84245894-1.45896352 1.35666609-3.0480238 1.54876128-4.6678833h-19.93350165l-10.8157296-14.14464637c-.68131296-.88930777-1.7675558-1.39116522-2.87692807-1.33786655-1.119972.05332866-2.14294643.6515037-2.7362316 1.60345991L84.56351375 70.56637232 64.76700867 22.0885843c-.5359866-1.3079673-1.82695433-2.15894603-3.2362191-2.12294693z"
|
||||
class="line"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
+93
@@ -0,0 +1,93 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`application-status should match the snapshot with custom status 1`] = `
|
||||
<div
|
||||
class="application-status"
|
||||
>
|
||||
<font-awesome-icon-stub
|
||||
class="application-status__icon application-status__icon--?"
|
||||
/>
|
||||
|
||||
<!---->
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`application-status should match the snapshot with status DOWN 1`] = `
|
||||
<div
|
||||
class="application-status"
|
||||
>
|
||||
<font-awesome-icon-stub
|
||||
class="application-status__icon application-status__icon--DOWN"
|
||||
icon="times-circle"
|
||||
/>
|
||||
|
||||
<!---->
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`application-status should match the snapshot with status OFFLINE 1`] = `
|
||||
<div
|
||||
class="application-status"
|
||||
>
|
||||
<font-awesome-icon-stub
|
||||
class="application-status__icon application-status__icon--OFFLINE"
|
||||
icon="minus-circle"
|
||||
/>
|
||||
|
||||
<!---->
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`application-status should match the snapshot with status OUT_OF_SERVICE 1`] = `
|
||||
<div
|
||||
class="application-status"
|
||||
>
|
||||
<font-awesome-icon-stub
|
||||
class="application-status__icon application-status__icon--OUT_OF_SERVICE"
|
||||
icon="ban"
|
||||
/>
|
||||
|
||||
<!---->
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`application-status should match the snapshot with status RESTRICTED 1`] = `
|
||||
<div
|
||||
class="application-status"
|
||||
>
|
||||
<font-awesome-icon-stub
|
||||
class="application-status__icon application-status__icon--RESTRICTED"
|
||||
icon="exclamation"
|
||||
/>
|
||||
|
||||
<!---->
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`application-status should match the snapshot with status UNKNOWN 1`] = `
|
||||
<div
|
||||
class="application-status"
|
||||
>
|
||||
<font-awesome-icon-stub
|
||||
class="application-status__icon application-status__icon--UNKNOWN"
|
||||
icon="question-circle"
|
||||
/>
|
||||
|
||||
<!---->
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`application-status should match the snapshot with status UP with Timestamp 1`] = `
|
||||
<div
|
||||
class="application-status"
|
||||
>
|
||||
<font-awesome-icon-stub
|
||||
class="application-status__icon application-status__icon--UP"
|
||||
icon="check"
|
||||
/>
|
||||
|
||||
<small>
|
||||
14m
|
||||
</small>
|
||||
</div>
|
||||
`;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`time-ago given a short time passed time should match the snapshot 1`] = `14m`;
|
||||
|
||||
exports[`time-ago given a long time passed time should match the snapshot 1`] = `Jul 7`;
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
*
|
||||
* 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 {library} from '@fortawesome/fontawesome-svg-core';
|
||||
import {faGithub} from '@fortawesome/free-brands-svg-icons/faGithub';
|
||||
import {faGitter} from '@fortawesome/free-brands-svg-icons/faGitter';
|
||||
import {faStackOverflow} from '@fortawesome/free-brands-svg-icons/faStackOverflow';
|
||||
import {faTimesCircle as farTimesCircle} from '@fortawesome/free-regular-svg-icons/faTimesCircle';
|
||||
import {faBan} from '@fortawesome/free-solid-svg-icons/faBan';
|
||||
import {faBell} from '@fortawesome/free-solid-svg-icons/faBell';
|
||||
import {faBellSlash} from '@fortawesome/free-solid-svg-icons/faBellSlash';
|
||||
import {faBook} from '@fortawesome/free-solid-svg-icons/faBook';
|
||||
import {faCheck} from '@fortawesome/free-solid-svg-icons/faCheck';
|
||||
import {faDownload} from '@fortawesome/free-solid-svg-icons/faDownload';
|
||||
import {faExclamation} from '@fortawesome/free-solid-svg-icons/faExclamation';
|
||||
import {faExclamationTriangle} from '@fortawesome/free-solid-svg-icons/faExclamationTriangle';
|
||||
import {faHeartbeat} from '@fortawesome/free-solid-svg-icons/faHeartbeat';
|
||||
import {faHome} from '@fortawesome/free-solid-svg-icons/faHome';
|
||||
import {faMinusCircle} from '@fortawesome/free-solid-svg-icons/faMinusCircle';
|
||||
import {faPencilAlt} from '@fortawesome/free-solid-svg-icons/faPencilAlt';
|
||||
import {faQuestionCircle} from '@fortawesome/free-solid-svg-icons/faQuestionCircle';
|
||||
import {faSignOutAlt} from '@fortawesome/free-solid-svg-icons/faSignOutAlt';
|
||||
import {faStepBackward} from '@fortawesome/free-solid-svg-icons/faStepBackward';
|
||||
import {faStepForward} from '@fortawesome/free-solid-svg-icons/faStepForward';
|
||||
import {faTimesCircle} from '@fortawesome/free-solid-svg-icons/faTimesCircle';
|
||||
import {faTrash} from '@fortawesome/free-solid-svg-icons/faTrash';
|
||||
import {faUserCircle} from '@fortawesome/free-solid-svg-icons/faUserCircle';
|
||||
import {faWrench} from '@fortawesome/free-solid-svg-icons/faWrench';
|
||||
import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome';
|
||||
|
||||
library.add(
|
||||
//solid
|
||||
faTrash,
|
||||
faDownload,
|
||||
faStepForward,
|
||||
faStepBackward,
|
||||
faCheck,
|
||||
faQuestionCircle,
|
||||
faBan,
|
||||
faTimesCircle,
|
||||
faMinusCircle,
|
||||
faExclamation,
|
||||
faBook,
|
||||
faSignOutAlt,
|
||||
faExclamationTriangle,
|
||||
faPencilAlt,
|
||||
faBell,
|
||||
faBellSlash,
|
||||
faUserCircle,
|
||||
faHeartbeat,
|
||||
faHome,
|
||||
faWrench,
|
||||
//regular,
|
||||
farTimesCircle,
|
||||
//brands
|
||||
faGithub,
|
||||
faStackOverflow,
|
||||
faGitter
|
||||
);
|
||||
|
||||
export default FontAwesomeIcon;
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
|
||||
const components = [];
|
||||
|
||||
/* global require */
|
||||
const context = require.context('.', false, /^(?:(?!.*\.spec\.(js|vue)$).)*\.(js|vue)$/);
|
||||
context.keys().forEach(function (key) {
|
||||
const name = /^(.\/)+(.*)\.(vue|js)$/.exec(key)[2];
|
||||
components.push({name, component: context(key).default})
|
||||
});
|
||||
|
||||
export default {
|
||||
install(Vue) {
|
||||
components.forEach(component => Vue.component(component.name, component.component));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<button @click="click" class="confirm-button" :class="{ 'is-warning' : confirm }" v-on-clickaway="abort">
|
||||
<slot name="confirm" v-if="confirm">Confirm</slot>
|
||||
<slot v-else />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {directive as onClickaway} from 'vue-clickaway';
|
||||
|
||||
export default {
|
||||
directives: {onClickaway},
|
||||
data: () => ({
|
||||
confirm: false
|
||||
}),
|
||||
methods: {
|
||||
abort() {
|
||||
this.confirm = false;
|
||||
},
|
||||
click(event) {
|
||||
if (this.confirm) {
|
||||
this.$el.style.width = null;
|
||||
this.$emit('click', event);
|
||||
} else {
|
||||
const width = this.$el.getBoundingClientRect().width;
|
||||
this.$el.style.width = `${width}px`;
|
||||
}
|
||||
this.confirm = !this.confirm;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
@import "~@/assets/css/utilities";
|
||||
|
||||
.confirm-button {
|
||||
transition: all $easing 150ms;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,50 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div v-html="formatted" class="formatted" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {Autolink} from '@/utils/autolink';
|
||||
import objToString from '@/utils/objToString';
|
||||
|
||||
const autolink = new Autolink({truncate: {
|
||||
length: 50,
|
||||
location: 'smart'
|
||||
}});
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
// eslint-disable-next-line vue/require-prop-type-constructor
|
||||
type: null,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
formatted() {
|
||||
return autolink(objToString(this.value));
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.formatted {
|
||||
white-space: pre;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,77 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<button type="button" class="icon-button" :disabled="disabled" v-on="$listeners">
|
||||
<font-awesome-icon :icon="icon" :size="size" :class="iconClass" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
icon: {
|
||||
type: [String, Array],
|
||||
required: true,
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
iconClass: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "~@/assets/css/utilities";
|
||||
|
||||
.icon-button {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
color: inherit;
|
||||
|
||||
&:not([disabled]) {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: $black;
|
||||
}
|
||||
|
||||
& svg {
|
||||
fill: currentcolor;
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,87 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="card panel">
|
||||
<header
|
||||
v-if="title || $slots['header']"
|
||||
class="card-header"
|
||||
:class="{'panel__header--sticky': headerSticksBelow}"
|
||||
v-sticks-below="headerSticksBelow"
|
||||
>
|
||||
<p class="card-header-title">
|
||||
<span v-text="title" />
|
||||
<slot name="header" />
|
||||
</p>
|
||||
<div class="panel__close">
|
||||
<sba-icon-button v-if="closeable" :icon="['far', 'times-circle']" @click.stop="close" />
|
||||
</div>
|
||||
</header>
|
||||
<div v-if="$slots['default']" class="card-content">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import sticksBelow from '@/directives/sticks-below';
|
||||
import SbaIconButton from './sba-icon-button';
|
||||
|
||||
export default {
|
||||
components: {SbaIconButton},
|
||||
directives: {sticksBelow},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
closeable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
headerSticksBelow: {
|
||||
type: Array,
|
||||
default: undefined
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
close(event) {
|
||||
this.$emit('close', event);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "~@/assets/css/utilities";
|
||||
|
||||
.panel {
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
&__close {
|
||||
margin-right: 0.75em;
|
||||
color: $grey-light;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-self: flex-end;
|
||||
}
|
||||
|
||||
&__header--sticky {
|
||||
position: sticky;
|
||||
background-color: $white;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
*
|
||||
* 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 {mount} from '@vue/test-utils';
|
||||
import moment from 'moment';
|
||||
import sbaStatus from './sba-status';
|
||||
|
||||
moment.now = () => +new Date(1318781879406);
|
||||
|
||||
describe('application-status', () => {
|
||||
|
||||
const testSnapshotForStatus = (status, date) => {
|
||||
const wrapper = mount(sbaStatus, {
|
||||
propsData: {
|
||||
status,
|
||||
date
|
||||
},
|
||||
stubs: {
|
||||
'font-awesome-icon': true
|
||||
}
|
||||
});
|
||||
expect(wrapper.vm.$el).toMatchSnapshot();
|
||||
};
|
||||
|
||||
it('should match the snapshot with status UP with Timestamp', () => {
|
||||
testSnapshotForStatus('UP', 1318781000000);
|
||||
});
|
||||
|
||||
it('should match the snapshot with status RESTRICTED', () => {
|
||||
testSnapshotForStatus('RESTRICTED');
|
||||
});
|
||||
|
||||
it('should match the snapshot with status OUT_OF_SERVICE', () => {
|
||||
testSnapshotForStatus('OUT_OF_SERVICE');
|
||||
});
|
||||
|
||||
it('should match the snapshot with status DOWN', () => {
|
||||
testSnapshotForStatus('DOWN');
|
||||
});
|
||||
|
||||
it('should match the snapshot with status UNKNOWN', () => {
|
||||
testSnapshotForStatus('UNKNOWN');
|
||||
});
|
||||
|
||||
it('should match the snapshot with status OFFLINE', () => {
|
||||
testSnapshotForStatus('OFFLINE');
|
||||
});
|
||||
|
||||
it('should match the snapshot with custom status', () => {
|
||||
testSnapshotForStatus('?');
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="application-status">
|
||||
<font-awesome-icon :icon="icon" class="application-status__icon"
|
||||
:class="`application-status__icon--${status}`"
|
||||
/>
|
||||
<small v-if="date">
|
||||
<sba-time-ago :date="date" />
|
||||
</small>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
import sbaTimeAgo from './sba-time-ago';
|
||||
|
||||
const icons = {
|
||||
'UP': 'check',
|
||||
'RESTRICTED': 'exclamation',
|
||||
'OUT_OF_SERVICE': 'ban',
|
||||
'DOWN': 'times-circle',
|
||||
'OFFLINE': 'minus-circle',
|
||||
'UNKNOWN': 'question-circle'
|
||||
};
|
||||
|
||||
export default {
|
||||
// eslint-disable-next-line vue/no-unused-components
|
||||
components: {sbaTimeAgo},
|
||||
props: {
|
||||
status: {
|
||||
type: String,
|
||||
default: 'UNKNOWN'
|
||||
},
|
||||
date: {
|
||||
type: [String, Date, Number, moment],
|
||||
default: null
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
icon() {
|
||||
return icons[this.status];
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "~@/assets/css/utilities";
|
||||
|
||||
.application-status {
|
||||
text-align: center;
|
||||
line-height: 1rem;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
|
||||
&__icon {
|
||||
color: gray;
|
||||
margin: 0 auto;
|
||||
|
||||
&--UP {
|
||||
color: $success;
|
||||
}
|
||||
|
||||
&--RESTRICTED {
|
||||
color: $warning;
|
||||
}
|
||||
|
||||
&--OUT_OF_SERVICE,
|
||||
&--DOWN {
|
||||
color: $danger;
|
||||
}
|
||||
|
||||
&--UNKNOWN,
|
||||
&--OFFLINE {
|
||||
color: $grey;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,42 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="field is-grouped is-grouped-multiline" v-if="hasTags">
|
||||
<div class="control" v-for="(value, key) in tags" :key="key">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-dark" v-text="key" />
|
||||
<span class="tag is-light" v-text="value" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
tags: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasTags() {
|
||||
return Object.keys(this.tags).length > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
*
|
||||
* 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 moment from 'moment-shortformat';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
date: {
|
||||
type: [String, Date, Number, moment],
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data: () => ({
|
||||
now: moment(),
|
||||
timer: null,
|
||||
}),
|
||||
computed: {
|
||||
timeAgo() {
|
||||
return moment(this.date).short(true, this.now);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.timer = window.setInterval(() => {
|
||||
this.now = moment();
|
||||
}, 1000);
|
||||
},
|
||||
render() {
|
||||
return this._v(this.timeAgo);
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.timer) {
|
||||
window.clearInterval(this.timer);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
*
|
||||
* 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 {shallowMount} from '@vue/test-utils';
|
||||
import moment from 'moment';
|
||||
import sbaTimeAgo from './sba-time-ago.js';
|
||||
|
||||
moment.now = () => +new Date(1318781879406);
|
||||
|
||||
describe('time-ago', () => {
|
||||
|
||||
describe('given a short time passed time', () => {
|
||||
it('should match the snapshot', () => {
|
||||
const wrapper = shallowMount(sbaTimeAgo, {
|
||||
propsData: {
|
||||
date: moment(1318781000000)
|
||||
}
|
||||
});
|
||||
expect(wrapper.vm.$el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('given a long time passed time', () => {
|
||||
it('should match the snapshot', () => {
|
||||
const wrapper = shallowMount(sbaTimeAgo, {
|
||||
propsData: {
|
||||
date: moment(1310000000000)
|
||||
}
|
||||
});
|
||||
expect(wrapper.vm.$el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
*
|
||||
* 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 ResizeObserver from 'resize-observer-polyfill';
|
||||
|
||||
const observers = new WeakMap();
|
||||
|
||||
const bind = (el, binding) => {
|
||||
unbind(el);
|
||||
const observer = new ResizeObserver(binding.value);
|
||||
observer.observe(el);
|
||||
observers.set(el, observer);
|
||||
|
||||
};
|
||||
|
||||
const unbind = (el) => {
|
||||
const observer = observers.get(el);
|
||||
if (observer) {
|
||||
observer.disconnect();
|
||||
observers.delete(el);
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
bind,
|
||||
update(el, binding) {
|
||||
if (binding.value === binding.oldValue) {
|
||||
return
|
||||
}
|
||||
bind(el, binding)
|
||||
},
|
||||
unbind
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
*
|
||||
* 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 Popper from 'popper.js';
|
||||
|
||||
const poppers = new WeakMap();
|
||||
|
||||
const bind = (el, binding) => {
|
||||
const reference = typeof binding.value === 'string' ? document.getElementById(binding.value) : binding.value;
|
||||
if (reference) {
|
||||
const popper = new Popper(reference, el);
|
||||
poppers.set(el, popper);
|
||||
}
|
||||
};
|
||||
|
||||
const unbind = (el) => {
|
||||
const popper = poppers.get(el);
|
||||
if (popper) {
|
||||
popper.destroy(el);
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
bind,
|
||||
update(el, binding) {
|
||||
if (binding.value === binding.oldValue) {
|
||||
return
|
||||
}
|
||||
bind(el, binding)
|
||||
},
|
||||
unbind
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const bind = (el, binding) => {
|
||||
if (!binding.value) {
|
||||
return;
|
||||
}
|
||||
const top = binding.value.map(v => document.querySelector(v))
|
||||
.filter(v => Boolean(v))
|
||||
.map(v => v.getBoundingClientRect().height)
|
||||
.reduce((a, b) => a + b, 0);
|
||||
el.style.top = `${top}px`;
|
||||
el.style.position = 'sticky';
|
||||
};
|
||||
|
||||
export default {
|
||||
bind,
|
||||
update(el, binding) {
|
||||
if (binding.value === binding.oldValue) {
|
||||
return
|
||||
}
|
||||
bind(el, binding)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<!--
|
||||
~ Copyright 2014-2018 the original author or authors.
|
||||
~
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<base th:href="@{${adminContextPath} + '/'}" href="/">
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta name="format-detection" content="telephone=no,email=no">
|
||||
<meta name="theme-color" content="#42d3a5">
|
||||
|
||||
<!-- include extensions -->
|
||||
<th:block th:each="cssFile : ${cssExtensions}">
|
||||
<link rel="preload" th:href="${cssFile.resourcePath}" as="style">
|
||||
</th:block>
|
||||
<th:block th:each="jsFile : ${jsExtensions}">
|
||||
<link rel="preload" th:href="${jsFile.resourcePath}" as="script">
|
||||
</th:block>
|
||||
<th:block th:each="cssFile : ${cssExtensions}">
|
||||
<link th:href="${cssFile.resourcePath}" rel="stylesheet">
|
||||
</th:block>
|
||||
|
||||
<link rel="shortcut icon" href="assets/img/favicon.png" type="image/png">
|
||||
<title th:text="${uiSettings.title}">服务监控</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="app"></div>
|
||||
|
||||
<script th:inline="javascript">
|
||||
var SBA = {
|
||||
uiSettings: /*[[${uiSettings}]]*/ {},
|
||||
user: /*[[${user}]]*/ null,
|
||||
extensions: [],
|
||||
/*[#th:block th:if="${_csrf}"]*/
|
||||
csrf: {
|
||||
parameterName: /*[[${_csrf.parameterName}]]*/ null,
|
||||
headerName: /*[[${_csrf.headerName}]]*/ null
|
||||
},
|
||||
/*[/th:block]*/
|
||||
use: function (ext) {
|
||||
this.extensions.push(ext);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<!-- include extensions -->
|
||||
<th:block th:each="jsFile : ${jsExtensions}">
|
||||
<script lang="javascript" th:src="${jsFile.resourcePath}"></script>
|
||||
</th:block>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
*
|
||||
* 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 '@/assets/css/base.scss';
|
||||
import moment from 'moment';
|
||||
import Vue from 'vue';
|
||||
import VueRouter from 'vue-router';
|
||||
import components from './components';
|
||||
import Notifications from './notifications';
|
||||
import sbaShell from './shell';
|
||||
import Store from './store';
|
||||
import ViewRegistry from './viewRegistry';
|
||||
import views from './views';
|
||||
|
||||
moment.locale(window.navigator.language);
|
||||
Vue.use(VueRouter);
|
||||
Vue.use(components);
|
||||
|
||||
const applicationStore = new Store();
|
||||
const viewRegistry = new ViewRegistry();
|
||||
|
||||
const installables = [
|
||||
Notifications,
|
||||
...views,
|
||||
...global.SBA.extensions
|
||||
];
|
||||
|
||||
installables.forEach(view => view.install({
|
||||
viewRegistry,
|
||||
applicationStore,
|
||||
vue: Vue
|
||||
}));
|
||||
|
||||
new Vue({
|
||||
router: new VueRouter({
|
||||
linkActiveClass: 'is-active',
|
||||
routes: viewRegistry.routes
|
||||
}),
|
||||
el: '#app',
|
||||
render(h) {
|
||||
return h(sbaShell, {
|
||||
props: {
|
||||
views: this.views,
|
||||
applications: this.applications,
|
||||
error: this.error
|
||||
}
|
||||
});
|
||||
},
|
||||
data: {
|
||||
views: viewRegistry.views,
|
||||
applications: applicationStore.applications,
|
||||
error: null
|
||||
},
|
||||
methods: {
|
||||
onError(error) {
|
||||
console.warn('Connection to server failed:', error);
|
||||
this.error = error;
|
||||
},
|
||||
onConnected() {
|
||||
this.error = null;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
applicationStore.addEventListener('connected', this.onConnected);
|
||||
applicationStore.addEventListener('error', this.onError);
|
||||
applicationStore.start();
|
||||
},
|
||||
beforeDestroy() {
|
||||
applicationStore.stop();
|
||||
applicationStore.removeEventListener('connected', this.onConnected);
|
||||
applicationStore.removeEventListener('error', this.onError)
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
<!--
|
||||
~ Copyright 2014-2018 the original author or authors.
|
||||
~
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<base th:href="@{${adminContextPath} + '/'}" href="/">
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta name="format-detection" content="telephone=no,email=no">
|
||||
<meta name="theme-color" content="#42d3a5">
|
||||
<link rel="shortcut icon" href="assets/img/favicon.png" type="image/png">
|
||||
<title>Spring Boot Admin - Login</title>
|
||||
</head>
|
||||
<body class="login">
|
||||
|
||||
<section class="hero is-fullheight">
|
||||
<div class="hero-body">
|
||||
<div class="container has-text-centered">
|
||||
<div class="column is-4 is-offset-4">
|
||||
<div class="box">
|
||||
<figure class="image is-128x128 login--logo">
|
||||
<img src="assets/img/icon-spring-boot-admin.svg">
|
||||
</figure>
|
||||
<h1 class="title has-text-primary">PigX 服务状态监控</h1>
|
||||
<form method="post">
|
||||
<input type="hidden"
|
||||
th:if="${_csrf}"
|
||||
th:name="${_csrf.parameterName}"
|
||||
th:value="${_csrf.token}"/>
|
||||
<div class="field">
|
||||
<p class="is-medium has-text-danger" th:unless="${param.error == null}">
|
||||
Invalid username or password
|
||||
</p>
|
||||
<p class="is-medium" th:unless="${param.logout == null}">
|
||||
Logout successful
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input class="input is-medium" type="input" name="username" placeholder="Username"
|
||||
autofocus th:classappend="${param.error != null} ? is-danger">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input class="input is-medium" type="password" name="password"
|
||||
placeholder="Password" th:classappend="${param.error != null} ? is-danger">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input type="submit" class="button is-block is-primary is-medium is-fullwidth"
|
||||
value="Login">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
*
|
||||
* 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 '@/assets/css/base.scss';
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export default {
|
||||
created() {
|
||||
this.subscribe();
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.unsubscribe();
|
||||
},
|
||||
methods: {
|
||||
async subscribe() {
|
||||
if (!this.subscription) {
|
||||
this.subscription = await this.createSubscription();
|
||||
}
|
||||
},
|
||||
unsubscribe() {
|
||||
if (this.subscription) {
|
||||
try {
|
||||
!this.subscription.closed && this.subscription.unsubscribe();
|
||||
} finally {
|
||||
this.subscription = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
let granted = false;
|
||||
|
||||
const requestPermissions = () => {
|
||||
if ('Notification' in window) {
|
||||
granted = (window.Notification.permission === 'granted');
|
||||
if (!granted && window.Notification.permission !== 'denied') {
|
||||
window.Notification.requestPermission(permission => granted = (permission === 'granted'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const notify = (title, options) => {
|
||||
if (granted) {
|
||||
const note = new window.Notification(title, options);
|
||||
if (options.url !== null) {
|
||||
note.onclick = () => {
|
||||
window.focus();
|
||||
window.open(options.url, '_self');
|
||||
};
|
||||
}
|
||||
if (options.timeout > 0) {
|
||||
note.onshow = () => setTimeout(() => note.close(), options.timeout);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
install: ({applicationStore}) => {
|
||||
requestPermissions();
|
||||
|
||||
applicationStore.addEventListener('updated', (newVal, oldVal) => {
|
||||
if (newVal.status !== oldVal.status) {
|
||||
notify(`${newVal.name} is now ${newVal.status}`, {
|
||||
tag: `${newVal.name}-${newVal.status}`,
|
||||
lang: 'en',
|
||||
body: `was ${oldVal.status}.`,
|
||||
icon: newVal.status === 'UP' ? 'assets/img/favicon.png' : 'assets/img/favicon-danger.png',
|
||||
renotify: true,
|
||||
timeout: 10000
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
*
|
||||
* 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 axios, {redirectOn401} from '@/utils/axios';
|
||||
import waitForPolyfill from '@/utils/eventsource-polyfill';
|
||||
import {concat, from, ignoreElements, Observable} from '@/utils/rxjs';
|
||||
import uri from '@/utils/uri';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import Instance from './instance';
|
||||
|
||||
class Application {
|
||||
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
this.axios = axios.create({
|
||||
baseURL: uri`applications/${this.name}/`
|
||||
});
|
||||
this.axios.interceptors.response.use(
|
||||
response => response,
|
||||
redirectOn401()
|
||||
);
|
||||
}
|
||||
|
||||
findInstance(instanceId) {
|
||||
return this.instances.find(instance => instance.id === instanceId);
|
||||
}
|
||||
|
||||
get isUnregisterable() {
|
||||
return this.instances.findIndex(i => i.isUnregisterable) >= 0;
|
||||
}
|
||||
|
||||
async unregister() {
|
||||
return this.axios.delete('')
|
||||
}
|
||||
|
||||
static async list() {
|
||||
return await axios.get('applications', {
|
||||
transformResponse: Application._transformResponse
|
||||
});
|
||||
}
|
||||
|
||||
static getStream() {
|
||||
return concat(
|
||||
from(waitForPolyfill()).pipe(ignoreElements()),
|
||||
Observable.create(observer => {
|
||||
const eventSource = new EventSource('applications');
|
||||
eventSource.onmessage = message => observer.next({
|
||||
...message,
|
||||
data: Application._transformResponse(message.data)
|
||||
});
|
||||
|
||||
eventSource.onerror = err => observer.error(err);
|
||||
return () => {
|
||||
eventSource.close();
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
static _transformResponse(data) {
|
||||
if (!data) {
|
||||
return data;
|
||||
}
|
||||
const json = JSON.parse(data);
|
||||
if (json instanceof Array) {
|
||||
const applications = json.map(Application._toApplication);
|
||||
return sortBy(applications, [item => item.name]);
|
||||
}
|
||||
return Application._toApplication(json);
|
||||
}
|
||||
|
||||
static _toApplication(application) {
|
||||
const instances = application.instances.map(instance => Object.assign(new Instance(instance.id), instance));
|
||||
return Object.assign(new Application(application.name), {
|
||||
...application,
|
||||
instances: sortBy(instances, [instance => instance.registration.healthUrl])
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default Application;
|
||||
@@ -0,0 +1,332 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
*
|
||||
* 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 axios, {redirectOn401} from '@/utils/axios';
|
||||
import waitForPolyfill from '@/utils/eventsource-polyfill';
|
||||
import logtail from '@/utils/logtail';
|
||||
import {concat, from, ignoreElements, Observable} from '@/utils/rxjs';
|
||||
import uri from '@/utils/uri';
|
||||
import transform from 'lodash/transform';
|
||||
|
||||
const actuatorMimeTypes = [
|
||||
'application/vnd.spring-boot.actuator.v2+json',
|
||||
'application/vnd.spring-boot.actuator.v1+json',
|
||||
'application/json'
|
||||
];
|
||||
|
||||
const isInstanceActuatorRequest = url => url.match(/^instances[/][^/]+[/]actuator([/].*)?$/);
|
||||
|
||||
class Instance {
|
||||
constructor(id) {
|
||||
this.id = id;
|
||||
this.axios = axios.create({
|
||||
baseURL: uri`instances/${this.id}/`,
|
||||
});
|
||||
this.axios.interceptors.response.use(
|
||||
response => response,
|
||||
redirectOn401(error => !isInstanceActuatorRequest(error.config.url))
|
||||
);
|
||||
}
|
||||
|
||||
hasEndpoint(endpointId) {
|
||||
return this.endpoints.findIndex(endpoint => endpoint.id === endpointId) >= 0;
|
||||
}
|
||||
|
||||
get isUnregisterable() {
|
||||
return this.registration.source === 'http-api';
|
||||
}
|
||||
|
||||
async unregister() {
|
||||
return this.axios.delete('');
|
||||
}
|
||||
|
||||
async fetchInfo() {
|
||||
return this.axios.get(uri`actuator/info`, {
|
||||
headers: {'Accept': actuatorMimeTypes}
|
||||
});
|
||||
}
|
||||
|
||||
async fetchMetrics() {
|
||||
return this.axios.get(uri`actuator/metrics`, {
|
||||
headers: {'Accept': actuatorMimeTypes}
|
||||
});
|
||||
}
|
||||
|
||||
async fetchMetric(metric, tags) {
|
||||
const params = tags ? {
|
||||
tag: Object.entries(tags)
|
||||
.filter(([, value]) => typeof value !== 'undefined' && value !== null)
|
||||
.map(([name, value]) => `${name}:${value}`)
|
||||
.join(',')
|
||||
} : {};
|
||||
return this.axios.get(uri`actuator/metrics/${metric}`, {
|
||||
headers: {'Accept': actuatorMimeTypes},
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
async fetchHealth() {
|
||||
return await this.axios.get(uri`actuator/health`, {
|
||||
headers: {'Accept': actuatorMimeTypes}
|
||||
});
|
||||
}
|
||||
|
||||
async fetchEnv(name) {
|
||||
return this.axios.get(uri`actuator/env/${name || '' }`, {
|
||||
headers: {'Accept': actuatorMimeTypes}
|
||||
});
|
||||
}
|
||||
|
||||
async fetchConfigprops() {
|
||||
return this.axios.get(uri`actuator/configprops`, {
|
||||
headers: {'Accept': actuatorMimeTypes}
|
||||
});
|
||||
}
|
||||
|
||||
async hasEnvManagerSupport() {
|
||||
const response = await this.axios.options(uri`actuator/env`);
|
||||
return response.headers['allow'] && response.headers['allow'].includes('POST');
|
||||
}
|
||||
|
||||
async resetEnv() {
|
||||
return this.axios.delete(uri`actuator/env`);
|
||||
}
|
||||
|
||||
async setEnv(name, value) {
|
||||
return this.axios.post(uri`actuator/env`, {name, value}, {
|
||||
headers: {'Content-Type': 'application/json'}
|
||||
});
|
||||
}
|
||||
|
||||
async refreshContext() {
|
||||
return this.axios.post(uri`actuator/refresh`);
|
||||
}
|
||||
|
||||
async fetchLiquibase() {
|
||||
return this.axios.get(uri`actuator/liquibase`, {
|
||||
headers: {'Accept': actuatorMimeTypes}
|
||||
});
|
||||
}
|
||||
|
||||
async fetchScheduledTasks() {
|
||||
return this.axios.get(uri`actuator/scheduledtasks`, {
|
||||
headers: {'Accept': actuatorMimeTypes}
|
||||
});
|
||||
}
|
||||
|
||||
async fetchCaches() {
|
||||
return this.axios.get(uri`actuator/caches`, {
|
||||
headers: {'Accept': actuatorMimeTypes}
|
||||
});
|
||||
}
|
||||
|
||||
async clearCaches() {
|
||||
return this.axios.delete(uri`actuator/caches`, {
|
||||
headers: {'Accept': actuatorMimeTypes}
|
||||
});
|
||||
}
|
||||
|
||||
async clearCache(name, cacheManager) {
|
||||
return this.axios.delete(uri`actuator/caches/${name}` , {
|
||||
params: {'cacheManager': cacheManager},
|
||||
headers: {'Accept': actuatorMimeTypes}
|
||||
});
|
||||
}
|
||||
|
||||
async fetchFlyway() {
|
||||
return this.axios.get(uri`actuator/flyway`, {
|
||||
headers: {'Accept': actuatorMimeTypes}
|
||||
});
|
||||
}
|
||||
|
||||
async fetchLoggers() {
|
||||
return this.axios.get(uri`actuator/loggers`, {
|
||||
headers: {'Accept': actuatorMimeTypes},
|
||||
transformResponse: Instance._toLoggers
|
||||
});
|
||||
}
|
||||
|
||||
async configureLogger(name, level) {
|
||||
return this.axios.post(uri`actuator/loggers/${name}`, {configuredLevel: level}, {
|
||||
headers: {'Content-Type': 'application/json'}
|
||||
});
|
||||
}
|
||||
|
||||
async fetchHttptrace() {
|
||||
return this.axios.get(uri`actuator/httptrace`, {
|
||||
headers: {'Accept': actuatorMimeTypes}
|
||||
});
|
||||
}
|
||||
|
||||
async fetchBeans() {
|
||||
return this.axios.get(uri`actuator/beans`, {
|
||||
headers: {'Accept': actuatorMimeTypes}
|
||||
});
|
||||
}
|
||||
|
||||
async fetchThreaddump() {
|
||||
return this.axios.get(uri`actuator/threaddump`, {
|
||||
headers: {'Accept': actuatorMimeTypes}
|
||||
});
|
||||
}
|
||||
|
||||
async fetchAuditevents({after, type, principal}) {
|
||||
return this.axios.get(uri`actuator/auditevents`, {
|
||||
headers: {'Accept': actuatorMimeTypes},
|
||||
params: {
|
||||
after: after.toISOString(),
|
||||
type: type || undefined,
|
||||
principal: principal || undefined
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fetchSessionsByUsername(username) {
|
||||
return this.axios.get(uri`actuator/sessions`, {
|
||||
headers: {'Accept': actuatorMimeTypes},
|
||||
params: {
|
||||
username: username || undefined
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fetchSession(sessionId) {
|
||||
return this.axios.get(uri`actuator/sessions/${sessionId}`, {
|
||||
headers: {'Accept': actuatorMimeTypes}
|
||||
});
|
||||
}
|
||||
|
||||
async deleteSession(sessionId) {
|
||||
return this.axios.delete(uri`actuator/sessions/${sessionId}`, {
|
||||
headers: {'Accept': actuatorMimeTypes}
|
||||
});
|
||||
}
|
||||
|
||||
streamLogfile(interval) {
|
||||
return logtail(opt => this.axios.get(uri`actuator/logfile`, opt), interval);
|
||||
}
|
||||
|
||||
async listMBeans() {
|
||||
return this.axios.get(uri`actuator/jolokia/list`, {
|
||||
headers: {'Accept': 'application/json'},
|
||||
params: {canonicalNaming: false},
|
||||
transformResponse: Instance._toMBeans
|
||||
});
|
||||
}
|
||||
|
||||
async readMBeanAttributes(domain, mBean) {
|
||||
const body = {
|
||||
type: 'read',
|
||||
mbean: `${domain}:${mBean}`,
|
||||
config: {ignoreErrors: true}
|
||||
};
|
||||
return this.axios.post(uri`actuator/jolokia`, body, {
|
||||
headers: {'Accept': 'application/json', 'Content-Type': 'application/json'}
|
||||
});
|
||||
}
|
||||
|
||||
async writeMBeanAttribute(domain, mBean, attribute, value) {
|
||||
const body = {
|
||||
type: 'write',
|
||||
mbean: `${domain}:${mBean}`,
|
||||
attribute,
|
||||
value
|
||||
};
|
||||
return this.axios.post(uri`actuator/jolokia`, body, {
|
||||
headers: {'Accept': 'application/json', 'Content-Type': 'application/json'}
|
||||
});
|
||||
}
|
||||
|
||||
async invokeMBeanOperation(domain, mBean, operation, args) {
|
||||
const body = {
|
||||
type: 'exec',
|
||||
mbean: `${domain}:${mBean}`,
|
||||
operation,
|
||||
'arguments': args
|
||||
};
|
||||
return this.axios.post(uri`actuator/jolokia`, body, {
|
||||
headers: {'Accept': 'application/json', 'Content-Type': 'application/json'}
|
||||
});
|
||||
}
|
||||
|
||||
async fetchMappings() {
|
||||
return this.axios.get(uri`actuator/mappings`, {
|
||||
headers: {'Accept': actuatorMimeTypes}
|
||||
});
|
||||
}
|
||||
|
||||
static async fetchEvents() {
|
||||
return axios.get('instances/events');
|
||||
}
|
||||
|
||||
static getEventStream() {
|
||||
return concat(
|
||||
from(waitForPolyfill()).pipe(ignoreElements()),
|
||||
Observable.create(observer => {
|
||||
const eventSource = new EventSource('instances/events');
|
||||
eventSource.onmessage = message => observer.next({
|
||||
...message,
|
||||
data: JSON.parse(message.data)
|
||||
});
|
||||
eventSource.onerror = err => observer.error(err);
|
||||
return () => {
|
||||
eventSource.close();
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
static async get(id) {
|
||||
return axios.get(uri`instances/${id}`, {
|
||||
transformResponse: Instance._toInstance
|
||||
});
|
||||
}
|
||||
|
||||
static _toInstance(data) {
|
||||
if (!data) {
|
||||
return data;
|
||||
}
|
||||
const instance = JSON.parse(data);
|
||||
return Object.assign(new Instance(instance.id), instance);
|
||||
}
|
||||
|
||||
static _toLoggers(data) {
|
||||
if (!data) {
|
||||
return data;
|
||||
}
|
||||
const raw = JSON.parse(data);
|
||||
const loggers = transform(raw.loggers, (result, value, key) => {
|
||||
return result.push({name: key, ...value});
|
||||
}, []);
|
||||
return {levels: raw.levels, loggers};
|
||||
}
|
||||
|
||||
static _toMBeans(data) {
|
||||
if (!data) {
|
||||
return data;
|
||||
}
|
||||
const raw = JSON.parse(data);
|
||||
return Object.entries(raw.value).map(([domain, mBeans]) => ({
|
||||
domain,
|
||||
mBeans: Object.entries(mBeans).map(([descriptor, mBean]) => ({
|
||||
descriptor: descriptor,
|
||||
...mBean
|
||||
}))
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
export default Instance;
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
*
|
||||
* 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 axios from '@/utils/axios';
|
||||
import uri from '@/utils/uri';
|
||||
import moment from 'moment';
|
||||
|
||||
class NotificationFilter {
|
||||
constructor({expiry, ...filter}) {
|
||||
Object.assign(this, filter);
|
||||
this.expiry = expiry ? moment(expiry) : null;
|
||||
}
|
||||
|
||||
affects(obj) {
|
||||
if (!obj) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.isApplicationFilter) {
|
||||
return this.applicationName === obj.name;
|
||||
}
|
||||
|
||||
if (this.isInstanceFilter) {
|
||||
return this.instanceId === obj.id;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
get isApplicationFilter() {
|
||||
return this.hasOwnProperty('applicationName');
|
||||
}
|
||||
|
||||
get isInstanceFilter() {
|
||||
return this.hasOwnProperty('instanceId');
|
||||
}
|
||||
|
||||
async delete() {
|
||||
return axios.delete(uri`notifications/filters/${this.id}`);
|
||||
}
|
||||
|
||||
static async isSupported() {
|
||||
try {
|
||||
const response = await axios.options('notifications/filters');
|
||||
return response.headers['allow'] && response.headers['allow'].includes('POST');
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static async getFilters() {
|
||||
return axios.get('notifications/filters', {
|
||||
transformResponse: NotificationFilter._transformResponse
|
||||
});
|
||||
}
|
||||
|
||||
static async addFilter(object, ttl) {
|
||||
const params = {ttl};
|
||||
if (object.hasOwnProperty('name')) {
|
||||
params.applicationName = object.name;
|
||||
} else if (object.hasOwnProperty('id')) {
|
||||
params.instanceId = object.id;
|
||||
}
|
||||
return axios.post('notifications/filters', null, {
|
||||
params,
|
||||
transformResponse: NotificationFilter._transformResponse
|
||||
});
|
||||
}
|
||||
|
||||
static _transformResponse(data) {
|
||||
if (!data) {
|
||||
return data;
|
||||
}
|
||||
const json = JSON.parse(data);
|
||||
if (json instanceof Array) {
|
||||
return json.map(NotificationFilter._toNotificationFilters).filter(f => !f.expired);
|
||||
}
|
||||
return NotificationFilter._toNotificationFilters(json);
|
||||
}
|
||||
|
||||
static _toNotificationFilters(notificationFilter) {
|
||||
return new NotificationFilter(notificationFilter);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default NotificationFilter;
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div id="app">
|
||||
<sba-navbar :views="mainViews" :applications="applications" :error="error" />
|
||||
<router-view :views="childViews" :applications="applications" :error="error" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import sbaNavbar from './navbar';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
views: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
applications: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
error: {
|
||||
type: Error,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line vue/no-unused-components
|
||||
components: {sbaNavbar},
|
||||
computed: {
|
||||
mainViews() {
|
||||
return this.views.filter(view => !view.parent);
|
||||
},
|
||||
activeMainViewName() {
|
||||
const currentView = this.$route.meta.view;
|
||||
return currentView && (currentView.parent || currentView.name);
|
||||
},
|
||||
childViews() {
|
||||
return this.views.filter(view => view.parent === this.activeMainViewName);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,132 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<nav id="navigation" class="navbar is-fixed-top">
|
||||
<div class="container">
|
||||
<div class="navbar-brand">
|
||||
<router-link class="navbar-item logo" to="/" v-html="brand" />
|
||||
|
||||
<div class="navbar-burger burger" @click.stop="showMenu = !showMenu">
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-menu" :class="{'is-active' : showMenu}">
|
||||
<div class="navbar-end">
|
||||
<router-link class="navbar-item" v-for="view in enabledViews" :to="{name: view.name}" :key="view.name">
|
||||
<component :is="view.handle" :applications="applications" :error="error" />
|
||||
</router-link>
|
||||
|
||||
<div class="navbar-item has-dropdown is-hoverable" v-if="userName">
|
||||
<a class="navbar-link">
|
||||
<font-awesome-icon icon="user-circle" size="lg" /> <span v-text="userName" />
|
||||
</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item">
|
||||
<form action="logout" method="post">
|
||||
<input v-if="csrfToken" type="hidden" :name="csrfParameterName" :value="csrfToken">
|
||||
<button class="button is-icon" type="submit" value="logout">
|
||||
<font-awesome-icon icon="sign-out-alt" /> Log out
|
||||
</button>
|
||||
</form>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {compareBy} from '@/utils/collections';
|
||||
|
||||
const readCookie = (name) => {
|
||||
const match = document.cookie.match(new RegExp('(^|;\\s*)(' + name + ')=([^;]*)'));
|
||||
return (match ? decodeURIComponent(match[3]) : null);
|
||||
};
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
showMenu: false,
|
||||
brand: '<img src="assets/img/icon-spring-boot-admin.svg"><span>PigX 服务状态监控</span>',
|
||||
userName: null,
|
||||
csrfToken: null,
|
||||
csrfParameterName: null
|
||||
}),
|
||||
props: {
|
||||
views: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
applications: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
error: {
|
||||
type: Error,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
enabledViews() {
|
||||
return [...this.views].filter(
|
||||
view => view.handle && (typeof view.isEnabled === 'undefined' || view.isEnabled())
|
||||
).sort(compareBy(v => v.order));
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (global.SBA) {
|
||||
if (global.SBA.uiSettings) {
|
||||
this.brand = global.SBA.uiSettings.brand || this.brand;
|
||||
}
|
||||
|
||||
if (global.SBA.user) {
|
||||
this.userName = global.SBA.user.name;
|
||||
}
|
||||
}
|
||||
this.csrfToken = readCookie('XSRF-TOKEN');
|
||||
this.csrfParameterName = (global.SBA && global.SBA.csrf && global.SBA.csrf.parameterName) || '_csrf';
|
||||
},
|
||||
mounted() {
|
||||
document.documentElement.classList.add('has-navbar-fixed-top');
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.documentElement.classList.remove('has-navbar-fixed-top')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "~@/assets/css/utilities";
|
||||
|
||||
.logo {
|
||||
align-self: center;
|
||||
flex-basis: 12em;
|
||||
flex-basis: content;
|
||||
max-height: 2.25em;
|
||||
padding: 0.5em 1em 0.5em 0.5em;
|
||||
font-size: 1.5em;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
|
||||
img {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
*
|
||||
* 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 Application from '@/services/application';
|
||||
import {concat, concatMap, defer, delay, doFirst, map, retryWhen, tap} from '@/utils/rxjs';
|
||||
|
||||
export default class {
|
||||
constructor() {
|
||||
this.applications = [];
|
||||
this.applications.findInstance = (instanceId) => {
|
||||
for (let application of this.applications) {
|
||||
const instance = application.findInstance(instanceId);
|
||||
if (instance) {
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
this.applications.findApplicationForInstance = (instanceId) => {
|
||||
return this.applications.find(application => !!application.findInstance(instanceId));
|
||||
};
|
||||
this.applications.indexOfApplication = (name) => {
|
||||
return this.applications.findIndex(application => application.name === name);
|
||||
};
|
||||
|
||||
this._listeners = {
|
||||
added: [],
|
||||
updated: [],
|
||||
removed: []
|
||||
};
|
||||
}
|
||||
|
||||
addEventListener(type, listener) {
|
||||
if (!(type in this._listeners)) {
|
||||
this._listeners[type] = [];
|
||||
}
|
||||
this._listeners[type].push(listener);
|
||||
}
|
||||
|
||||
removeEventListener(type, listener) {
|
||||
if (!(type in this._listeners)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const idx = this._listeners[type].indexOf(listener);
|
||||
if (idx > 0) {
|
||||
this._listeners[type].splice(idx, 1);
|
||||
}
|
||||
}
|
||||
|
||||
_dispatchEvent(type, ...args) {
|
||||
if (!(type in this._listeners)) {
|
||||
return;
|
||||
}
|
||||
const target = this;
|
||||
this._listeners[type].forEach(
|
||||
listener => listener.call(target, ...args)
|
||||
)
|
||||
}
|
||||
|
||||
start() {
|
||||
const list = defer(() => Application.list())
|
||||
.pipe(concatMap(message => message.data));
|
||||
const stream = Application.getStream()
|
||||
.pipe(map(message => message.data));
|
||||
this.subscription = concat(list, stream)
|
||||
.pipe(
|
||||
doFirst(() => this._dispatchEvent('connected')),
|
||||
retryWhen(
|
||||
errors => errors.pipe(
|
||||
tap(error => this._dispatchEvent('error', error)),
|
||||
delay(5000)
|
||||
)
|
||||
)
|
||||
).subscribe({
|
||||
next: application => {
|
||||
const idx = this.applications.indexOfApplication(application.name);
|
||||
if (idx >= 0) {
|
||||
const oldApplication = this.applications[idx];
|
||||
if (application.instances.length > 0) {
|
||||
this.applications.splice(idx, 1, application);
|
||||
this._dispatchEvent('updated', application, oldApplication);
|
||||
} else {
|
||||
this.applications.splice(idx, 1);
|
||||
this._dispatchEvent('removed', oldApplication);
|
||||
}
|
||||
} else {
|
||||
this.applications.push(application);
|
||||
this._dispatchEvent('added', application);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.subscription) {
|
||||
try {
|
||||
!this.subscription.closed && this.subscription.unsubscribe();
|
||||
} finally {
|
||||
this.subscription = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
*
|
||||
* 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 _Autolinker from 'autolinker';
|
||||
|
||||
export const defaults = {
|
||||
urls: {
|
||||
schemeMatches: true,
|
||||
wwwMatches: false,
|
||||
tldMatches: false
|
||||
},
|
||||
email: false,
|
||||
phone: false,
|
||||
mention: false,
|
||||
hashtag: false,
|
||||
|
||||
stripPrefix: false,
|
||||
stripTrailingSlash: false,
|
||||
newWindow: true,
|
||||
|
||||
truncate: {
|
||||
length: 0,
|
||||
location: 'smart'
|
||||
},
|
||||
|
||||
className: ''
|
||||
};
|
||||
const autolinker = new _Autolinker(defaults);
|
||||
|
||||
export default s => autolinker.link(s);
|
||||
export function Autolink(cfg) {
|
||||
this.autolinker = new _Autolinker({...defaults, ...cfg});
|
||||
return s => this.autolinker.link(s)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
*
|
||||
* 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 autolink, {Autolink} from './autolink'
|
||||
|
||||
describe('autolink should', () => {
|
||||
it('return the input string for normal text', () => {
|
||||
const str = 'This is just a normal text containing no hyperlinks';
|
||||
expect(autolink(str)).toBe(str);
|
||||
});
|
||||
|
||||
it('return string with anchor tag for the hyperlink', () => {
|
||||
const str = 'Please visit http://example.com.';
|
||||
expect(autolink(str)).toBe('Please visit <a href="http://example.com" target="_blank" rel="noopener noreferrer">http://example.com</a>.');
|
||||
});
|
||||
|
||||
it('return string with anchor tag with shortened text for the hyperlink', () => {
|
||||
const str = 'Please visit http://extraordinary.com/very/very/log/hyperlink.';
|
||||
|
||||
const customAutolink = new Autolink({
|
||||
truncate: {
|
||||
length: 30,
|
||||
location: 'smart'
|
||||
},
|
||||
});
|
||||
expect(customAutolink(str)).toBe('Please visit <a href="http://extraordinary.com/very/very/log/hyperlink" target="_blank" rel="noopener noreferrer" title="http://extraordinary.com/very/very/log/hyperlink">extraordinary.com/very…rlink</a>.');
|
||||
});
|
||||
|
||||
it('return string with anchor for hyperlinks in dense json', () => {
|
||||
const str = '{"name":"John Smith","links":[{"rel":"random-link1","href":"https://localhost:8000/api/123/query?action=do_something&age=21","hreflang":null,"media":null,"title":null,"type":null,"deprecation":null}]}';
|
||||
expect(autolink(str)).toBe('{"name":"John Smith","links":[{"rel":"random-link1","href":"<a href="https://localhost:8000/api/123/query?action=do_something&age=21" target="_blank" rel="noopener noreferrer">https://localhost:8000/api/123/query?action=do_something&age=21</a>","hreflang":null,"media":null,"title":null,"type":null,"deprecation":null}]}');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
*
|
||||
* 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 axios from 'axios';
|
||||
|
||||
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
if (global.SBA && global.SBA.csrf && global.SBA.csrf.headerName) {
|
||||
axios.defaults.xsrfHeaderName = global.SBA.csrf.headerName;
|
||||
}
|
||||
|
||||
export const redirectOn401 = (predicate = () => true) => error => {
|
||||
if (error.response && error.response.status === 401 && predicate(error)) {
|
||||
window.location.assign(`login?redirectTo=${encodeURIComponent(window.location.href)}`);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
|
||||
};
|
||||
|
||||
const instance = axios.create();
|
||||
instance.interceptors.response.use(response => response, redirectOn401());
|
||||
instance.create = axios.create;
|
||||
|
||||
export default instance;
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
*
|
||||
* 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 {redirectOn401} from './axios';
|
||||
|
||||
describe('redirectOn401', () => {
|
||||
it('should not redirect on 500', async () => {
|
||||
window.location.assign = jest.fn();
|
||||
|
||||
const error = {
|
||||
response: {
|
||||
status: 500
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
await redirectOn401()(error);
|
||||
} catch (e) {
|
||||
expect(e).toBe(error);
|
||||
}
|
||||
|
||||
expect(window.location.assign).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should redirect on 401', async () => {
|
||||
window.location.assign = jest.fn();
|
||||
|
||||
const error = {
|
||||
response: {
|
||||
status: 401
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
await redirectOn401()(error);
|
||||
} catch (e) {
|
||||
expect(e).toBe(error);
|
||||
}
|
||||
|
||||
expect(window.location.assign).toBeCalledWith('login?redirectTo=http%3A%2F%2Fexample.com%2F');
|
||||
});
|
||||
|
||||
it('should not redirect on 401 for predicate yields false', async () => {
|
||||
window.location.assign = jest.fn();
|
||||
|
||||
const error = {
|
||||
response: {
|
||||
status: 401
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
await redirectOn401(() => false)(error);
|
||||
} catch (e) {
|
||||
expect(e).toBe(error);
|
||||
}
|
||||
|
||||
expect(window.location.assign).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
|
||||
export const compareBy = mapper => (a, b) => {
|
||||
const valA = mapper(a);
|
||||
const valB = mapper(b);
|
||||
return valA > valB ? 1 : valA < valB ? -1 : 0;
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
*
|
||||
* 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 array from 'd3-array';
|
||||
import * as axis from 'd3-axis';
|
||||
import * as brush from 'd3-brush';
|
||||
import * as scale from 'd3-scale';
|
||||
import * as selection from 'd3-selection';
|
||||
import * as shape from 'd3-shape';
|
||||
import * as time from 'd3-time';
|
||||
|
||||
export default {
|
||||
...array,
|
||||
...axis,
|
||||
...brush,
|
||||
...scale,
|
||||
...selection,
|
||||
...shape,
|
||||
...time
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export default async () => {
|
||||
if (typeof window.EventSource === 'undefined') {
|
||||
return import(/* webpackChunkName: "event-source-polyfill" */ 'event-source-polyfill');
|
||||
}
|
||||
return Promise.resolve();
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
*
|
||||
* 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 {concatMap, EMPTY, of, timer} from '@/utils/rxjs';
|
||||
|
||||
export default (getFn, interval, initialSize = 300 * 1024) => {
|
||||
let range = `bytes=-${initialSize}`;
|
||||
let size = 0;
|
||||
|
||||
return timer(0, interval)
|
||||
.pipe(
|
||||
concatMap(() => getFn({headers: {range, 'Accept': 'text/plain, */*'}})),
|
||||
concatMap(response => {
|
||||
const initial = size === 0;
|
||||
const contentLength = response.data.length;
|
||||
if (response.status === 200) {
|
||||
if (!initial) {
|
||||
throw 'Expected 206 - Partial Content on subsequent requests.';
|
||||
}
|
||||
size = contentLength;
|
||||
} else if (response.status === 206) {
|
||||
size = parseInt(response.headers['content-range'].split('/')[1]);
|
||||
} else {
|
||||
throw 'Unexpected response status: ' + response.status;
|
||||
}
|
||||
|
||||
// Reload the last byte to avoid a 416: Range unsatisfiable.
|
||||
// If the response has length = 1 the file hasn't beent changed.
|
||||
// If the response status is 416 the logfile has been truncated.
|
||||
range = `bytes=${size - 1}-`;
|
||||
|
||||
let addendum = null;
|
||||
let skipped = 0;
|
||||
|
||||
if (initial) {
|
||||
if (contentLength >= size) {
|
||||
addendum = response.data;
|
||||
} else {
|
||||
// In case of a partial response find the first line break.
|
||||
addendum = response.data.substring(response.data.indexOf('\n') + 1);
|
||||
skipped = size - addendum.length;
|
||||
}
|
||||
} else if (response.data.length > 1) {
|
||||
// Remove the first byte which has been part of the previos response.
|
||||
addendum = response.data.substring(1);
|
||||
}
|
||||
|
||||
return addendum ? of({
|
||||
totalBytes: size,
|
||||
skipped,
|
||||
addendum
|
||||
}) : EMPTY;
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
*
|
||||
* 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 isEmpty from 'lodash/isEmpty';
|
||||
import isObject from 'lodash/isObject';
|
||||
|
||||
const nonEmptyComplexValue = value => (Array.isArray(value) || isObject(value)) && !isEmpty(value);
|
||||
|
||||
const objToString = (obj, indent = '') => {
|
||||
if (Array.isArray(obj)) {
|
||||
if (isEmpty(obj)) {
|
||||
return indent + '[]';
|
||||
}
|
||||
|
||||
return obj.map(value => {
|
||||
if (nonEmptyComplexValue(value)) {
|
||||
return `${indent}-\n${objToString(value, indent + ' ')}`;
|
||||
}
|
||||
return `${indent}- ${objToString(value, '')}`;
|
||||
}).join('\n');
|
||||
}
|
||||
|
||||
if (isObject(obj)) {
|
||||
if (isEmpty(obj)) {
|
||||
return indent + '{}';
|
||||
}
|
||||
|
||||
return Object.entries(obj)
|
||||
.map(([key, value]) => {
|
||||
if (nonEmptyComplexValue(value)) {
|
||||
return `${indent}${key}:\n${objToString(value, indent + ' ')}`;
|
||||
}
|
||||
return `${indent}${key}: ${objToString(value, '')}`;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
if (obj === null) {
|
||||
return indent + 'null';
|
||||
}
|
||||
|
||||
if (typeof obj === 'undefined' || obj === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return indent + obj;
|
||||
};
|
||||
|
||||
export default objToString;
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
*
|
||||
* 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 objToString from './objToString'
|
||||
|
||||
describe('objToString should', () => {
|
||||
|
||||
it('return the input string for normal text', () => {
|
||||
const obj = {
|
||||
a: 'start',
|
||||
b: 1,
|
||||
c: true,
|
||||
d: [1, 2, [3, 4]],
|
||||
e: {
|
||||
f: '',
|
||||
g: 1,
|
||||
h: null,
|
||||
i: undefined,
|
||||
j: {},
|
||||
k: [1],
|
||||
l: [{a:1, b:'foo'}, {b:2}]
|
||||
}
|
||||
};
|
||||
const str = `a: start
|
||||
b: 1
|
||||
c: true
|
||||
d:
|
||||
- 1
|
||||
- 2
|
||||
-
|
||||
- 3
|
||||
- 4
|
||||
e:
|
||||
f:
|
||||
g: 1
|
||||
h: null
|
||||
i:
|
||||
j: {}
|
||||
k:
|
||||
- 1
|
||||
l:
|
||||
-
|
||||
a: 1
|
||||
b: foo
|
||||
-
|
||||
b: 2`;
|
||||
expect(objToString(obj)).toBe(str);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
*
|
||||
* 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 {defer} from 'rxjs/internal/observable/defer';
|
||||
import {tap} from 'rxjs/internal/operators/tap';
|
||||
|
||||
export {throwError} from 'rxjs/internal/observable/throwError';
|
||||
export {of} from 'rxjs/internal/observable/of';
|
||||
export {defer} from 'rxjs/internal/observable/defer';
|
||||
export {concat} from 'rxjs/internal/observable/concat';
|
||||
export {EMPTY} from 'rxjs/internal/observable/empty';
|
||||
export {from} from 'rxjs/internal/observable/from';
|
||||
export {timer} from 'rxjs/internal/observable/timer';
|
||||
export {Observable} from 'rxjs/internal/Observable';
|
||||
export {Subject} from 'rxjs/internal/Subject';
|
||||
export {animationFrame as animationFrameScheduler} from 'rxjs/internal/scheduler/animationFrame';
|
||||
export {concatMap} from 'rxjs/internal/operators/concatMap';
|
||||
export {delay} from 'rxjs/internal/operators/delay';
|
||||
export {debounceTime} from 'rxjs/internal/operators/debounceTime';
|
||||
export {merge} from 'rxjs/internal/operators/merge';
|
||||
export {map} from 'rxjs/internal/operators/map';
|
||||
export {retryWhen} from 'rxjs/internal/operators/retryWhen';
|
||||
export {tap} from 'rxjs/internal/operators/tap';
|
||||
export {filter} from 'rxjs/internal/operators/filter';
|
||||
export {concatAll} from 'rxjs/internal/operators/concatAll';
|
||||
export {ignoreElements} from 'rxjs/internal/operators/ignoreElements';
|
||||
|
||||
export const doOnSubscribe = cb => source =>
|
||||
defer(() => {
|
||||
cb();
|
||||
return source
|
||||
});
|
||||
|
||||
export const doFirst = cb => source => {
|
||||
let triggered;
|
||||
return defer(() => {
|
||||
triggered = false;
|
||||
return source;
|
||||
}).pipe(
|
||||
tap(v => {
|
||||
if (!triggered) {
|
||||
triggered = true;
|
||||
cb(v);
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const listen = (cb, execDelay = 150) => source => {
|
||||
let handle = null;
|
||||
return source.pipe(
|
||||
doOnSubscribe(() => handle = setTimeout(() => cb('executing'), execDelay)),
|
||||
tap({
|
||||
complete: () => {
|
||||
handle && clearTimeout(handle);
|
||||
cb('completed');
|
||||
},
|
||||
error: (error) => {
|
||||
console.warn('Operation failed:', error);
|
||||
handle && clearTimeout(handle);
|
||||
cb('failed');
|
||||
}
|
||||
})
|
||||
)
|
||||
};
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
*
|
||||
* 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 {concat, EMPTY, of, throwError} from '@/utils/rxjs';
|
||||
import {delay, doFirst, doOnSubscribe, listen} from './rxjs';
|
||||
|
||||
describe('doOnSubscribe', () => {
|
||||
it('should call callback when subscribing', done => {
|
||||
const cb = jest.fn();
|
||||
EMPTY.pipe(
|
||||
doOnSubscribe(cb)
|
||||
).subscribe({
|
||||
complete: () => {
|
||||
expect(cb).toHaveBeenCalledTimes(1);
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('doFirst', () => {
|
||||
it('should not call callback when empty', done => {
|
||||
const cb = jest.fn();
|
||||
EMPTY.pipe(
|
||||
doFirst(cb)
|
||||
).subscribe({
|
||||
complete: () => {
|
||||
expect(cb).not.toBeCalled();
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should call callback on first item', done => {
|
||||
const cb = jest.fn();
|
||||
of(1,2,3).pipe(
|
||||
doFirst(cb)
|
||||
).subscribe({
|
||||
complete: () => {
|
||||
expect(cb).toHaveBeenCalledTimes(1);
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('listen', () => {
|
||||
it('should call callback with complete', done => {
|
||||
const cb = jest.fn();
|
||||
EMPTY.pipe(
|
||||
listen(cb)
|
||||
).subscribe({
|
||||
complete: () => {
|
||||
expect(cb).toHaveBeenCalledTimes(1);
|
||||
expect(cb).toHaveBeenCalledWith('completed');
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should call callback with executing and complete', done => {
|
||||
const cb = jest.fn();
|
||||
of(1).pipe(
|
||||
delay(10),
|
||||
listen(cb, 1)
|
||||
).subscribe({
|
||||
complete: () => {
|
||||
expect(cb).toHaveBeenCalledTimes(2);
|
||||
expect(cb).toHaveBeenCalledWith('executing');
|
||||
expect(cb).toHaveBeenCalledWith('completed');
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should call callback with failed', done => {
|
||||
const cb = jest.fn();
|
||||
throwError(new Error('test')).pipe(
|
||||
listen(cb)
|
||||
).subscribe({
|
||||
error: () => {
|
||||
expect(cb).toHaveBeenCalledTimes(1);
|
||||
expect(cb).toHaveBeenCalledWith('failed');
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should call callback with executing and failed', done => {
|
||||
const cb = jest.fn();
|
||||
|
||||
concat(
|
||||
of(1).pipe(delay(10)),
|
||||
throwError(new Error('test'))
|
||||
).pipe(
|
||||
listen(cb, 1)
|
||||
).subscribe({
|
||||
error: () => {
|
||||
expect(cb).toHaveBeenCalledTimes(2);
|
||||
expect(cb).toHaveBeenCalledWith('executing');
|
||||
expect(cb).toHaveBeenCalledWith('failed');
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export default (fullName, targetLength) => {
|
||||
if (!fullName || fullName.length < targetLength) {
|
||||
return fullName;
|
||||
}
|
||||
|
||||
const tokens = fullName.split('.');
|
||||
let shortened = tokens.pop();
|
||||
while (tokens.length > 0) {
|
||||
const next = tokens.pop();
|
||||
if (next.length + 1 + shortened.length < targetLength) {
|
||||
shortened = next + '.' + shortened;
|
||||
} else {
|
||||
shortened = next[0] + '.' + shortened;
|
||||
}
|
||||
}
|
||||
return shortened;
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
*
|
||||
* 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 shortenClassname from './shortenClassname';
|
||||
|
||||
describe('shortenClassname', () => {
|
||||
|
||||
it('should shorten when too long', () => {
|
||||
expect(shortenClassname('de.codecentric.boot.admin.server.config.AdminServerAutoConfiguration', 40)).toBe('d.c.b.a.s.config.AdminServerAutoConfiguration')
|
||||
});
|
||||
|
||||
it('should not shorten when string is small enough', () => {
|
||||
expect(shortenClassname('de.codecentric.boot.admin.server.config.AdminServerAutoConfiguration', 300)).toBe('de.codecentric.boot.admin.server.config.AdminServerAutoConfiguration')
|
||||
});
|
||||
|
||||
it('should not shorten when no package is present', () => {
|
||||
expect(shortenClassname('AdminServerAutoConfiguration', 1)).toBe('AdminServerAutoConfiguration')
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export default (strings, ...values) => {
|
||||
let result = strings[0];
|
||||
for (let i = 0; i < values.length; ++i) {
|
||||
result += encodeURIComponent(values[i]) + strings[i + 1];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
*
|
||||
* 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 uri from './uri';
|
||||
|
||||
describe('uri', () => {
|
||||
|
||||
it('should escape uris properly', () => {
|
||||
expect(uri`http://app/${'foo/bar'}?q=${'???'}`).toBe('http://app/foo%2Fbar?q=%3F%3F%3F');
|
||||
expect(uri`http://app/${'foo/bar'}?q=1`).toBe('http://app/foo%2Fbar?q=1');
|
||||
expect(uri`http://app/${'foo/bar'}`).toBe('http://app/foo%2Fbar');
|
||||
expect(uri`http://app/foo`).toBe('http://app/foo');
|
||||
expect(uri``).toBe('');
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const createTextVNode = label => {
|
||||
return {
|
||||
render() {
|
||||
return this._v(label)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default class ViewRegistry {
|
||||
constructor() {
|
||||
this._views = [];
|
||||
this._redirects = [];
|
||||
}
|
||||
|
||||
get views() {
|
||||
return this._views;
|
||||
}
|
||||
|
||||
get routes() {
|
||||
return [
|
||||
...this._toRoutes(this._views, v => !v.parent),
|
||||
...this._redirects
|
||||
]
|
||||
}
|
||||
|
||||
addView(...views) {
|
||||
views.forEach(view => this._addView(view));
|
||||
}
|
||||
|
||||
addRedirect(path, redirectToView) {
|
||||
this._redirects.push({path, redirect: {name: redirectToView}});
|
||||
}
|
||||
|
||||
_addView(view) {
|
||||
if (view.label && !view.handle) {
|
||||
view.handle = createTextVNode(view.label);
|
||||
}
|
||||
this._views.push(view);
|
||||
}
|
||||
|
||||
_toRoutes(views, filter) {
|
||||
return views.filter(filter).map(
|
||||
p => {
|
||||
const children = this._toRoutes(views, v => v.parent === p.name);
|
||||
return ({
|
||||
path: p.path,
|
||||
name: children.length === 0 ? p.name : undefined,
|
||||
component: p.component,
|
||||
props: p.props,
|
||||
meta: {view: p},
|
||||
children
|
||||
});
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="applications-list">
|
||||
<div class="application-list__item card" :class="{'is-active': selected === application.name}"
|
||||
v-for="application in applications" :key="application.name" :id=" application.name"
|
||||
v-on-clickaway="(event) => deselect(event, application.name)"
|
||||
>
|
||||
<header class="hero application-list__item__header"
|
||||
:class="getHeaderClass(application)"
|
||||
@click.stop="select(application.name)"
|
||||
>
|
||||
<template v-if="selected !== application.name">
|
||||
<sba-status :status="application.status"
|
||||
:date="application.statusTimestamp"
|
||||
class="application-list__item__header__status"
|
||||
/>
|
||||
<p class="application-list__item__header__name">
|
||||
<span v-text="application.name" /><br>
|
||||
<span class="is-muted">
|
||||
<a v-if="application.instances.length === 1"
|
||||
v-text="application.instances[0].registration.serviceUrl || application.instances[0].registration.healthUrl"
|
||||
:href="application.instances[0].registration.serviceUrl || application.instances[0].registration.healthUrl"
|
||||
/>
|
||||
<span v-else
|
||||
v-text="`${application.instances.length} instances`"
|
||||
/>
|
||||
</span>
|
||||
</p>
|
||||
<p class="application-list__item__header__version" v-text="application.buildVersion" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<h1 class="title is-size-5 application-list__item__header__name" v-text="application.name" />
|
||||
</template>
|
||||
<div class="application-list__item__header__actions">
|
||||
<sba-icon-button :id="`nf-settings-${application.name}`"
|
||||
v-if="hasNotificationFiltersSupport"
|
||||
@click.stop="toggleNotificationFilterSettingsFor(application)"
|
||||
:icon="hasActiveFilter(application) ? 'bell-slash' : 'bell'"
|
||||
/>
|
||||
<sba-icon-button icon="trash"
|
||||
v-if="application.isUnregisterable"
|
||||
@click.stop="unregister(application)"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
<div class="card-content" v-if="selected === application.name">
|
||||
<table class="table is-hoverable is-selectable is-fullwidth application__instances">
|
||||
<tbody>
|
||||
<tr v-for="instance in application.instances" :key="instance.id" @click.stop="showDetails(instance)">
|
||||
<td class="instance__status">
|
||||
<sba-status :status="instance.statusInfo.status" :date="instance.statusTimestamp" />
|
||||
</td>
|
||||
<td class="is-narrow">
|
||||
<a v-text="instance.registration.serviceUrl || instance.registration.healthUrl"
|
||||
:href="instance.registration.serviceUrl || instance.registration.healthUrl"
|
||||
@click.stop
|
||||
/><br>
|
||||
<span class="is-muted" v-text="instance.id" />
|
||||
</td>
|
||||
<td>
|
||||
<sba-tags :tags="instance.tags" />
|
||||
</td>
|
||||
<td>
|
||||
<span v-text="instance.buildVersion" />
|
||||
</td>
|
||||
<td class="instance__actions">
|
||||
<sba-icon-button :id="`nf-settings-${instance.id}`"
|
||||
v-if="hasNotificationFiltersSupport"
|
||||
@click.stop="toggleNotificationFilterSettingsFor(instance)"
|
||||
:icon="hasActiveFilter(instance) ? 'bell-slash' : 'bell'"
|
||||
/>
|
||||
<sba-icon-button icon="trash"
|
||||
v-if="instance.isUnregisterable"
|
||||
@click.stop="unregister(instance)"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<notification-filter-settings v-if="showNotificationFilterSettingsObject"
|
||||
v-popper="`nf-settings-${showNotificationFilterSettingsObject.id || showNotificationFilterSettingsObject.name}`"
|
||||
:notification-filters="notificationFilters"
|
||||
:object="showNotificationFilterSettingsObject"
|
||||
@filter-added="handleFilterChange"
|
||||
@filter-deleted="handleFilterChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Popper from '@/directives/popper';
|
||||
import subscribing from '@/mixins/subscribing';
|
||||
import NotificationFilter from '@/services/notification-filter';
|
||||
import {concatMap, merge, Subject, timer} from '@/utils/rxjs';
|
||||
import {directive as onClickaway} from 'vue-clickaway';
|
||||
import NotificationFilterSettings from './notification-filter-settings';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
applications: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
selected: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
directives: {onClickaway, Popper},
|
||||
mixins: [subscribing],
|
||||
components: {NotificationFilterSettings},
|
||||
data: () => ({
|
||||
errors: [],
|
||||
hasNotificationFiltersSupport: false,
|
||||
notificationFilters: [],
|
||||
showNotificationFilterSettingsObject: null
|
||||
}),
|
||||
computed: {},
|
||||
methods: {
|
||||
select(name) {
|
||||
this.$router.replace({name: 'applications', params: {selected: name}});
|
||||
},
|
||||
deselect(event, expectedSelected) {
|
||||
this.toggleNotificationFilterSettingsFor(null);
|
||||
if (event && event.target instanceof HTMLAnchorElement) {
|
||||
return;
|
||||
}
|
||||
if (this.selected === expectedSelected || !expectedSelected) {
|
||||
this.$router.replace({name: 'applications'});
|
||||
}
|
||||
},
|
||||
showDetails(instance) {
|
||||
this.$router.push({name: 'instances/details', params: {instanceId: instance.id}});
|
||||
},
|
||||
async scrollIntoView(id, behavior) {
|
||||
if (id) {
|
||||
await this.$nextTick();
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
const top = el.getBoundingClientRect().top + window.scrollY - 100;
|
||||
window.scroll({top, left: window.scrollX, behavior: behavior || 'smooth'});
|
||||
}
|
||||
}
|
||||
},
|
||||
getHeaderClass(application) {
|
||||
if (this.selected !== application.name) {
|
||||
return 'is-selectable';
|
||||
}
|
||||
if (application.status === 'UP') {
|
||||
return 'is-primary';
|
||||
}
|
||||
if (application.status === 'RESTRICTED') {
|
||||
return 'is-warning';
|
||||
}
|
||||
if (application.status === 'DOWN') {
|
||||
return 'is-danger';
|
||||
}
|
||||
if (application.status === 'OUT_OF_SERVICE') {
|
||||
return 'is-danger';
|
||||
}
|
||||
if (application.status === 'OFFLINE') {
|
||||
return 'is-light';
|
||||
}
|
||||
return 'is-light';
|
||||
},
|
||||
async unregister(item) {
|
||||
try {
|
||||
item.unregister();
|
||||
} catch (e) {
|
||||
this.errors.push(e);
|
||||
}
|
||||
},
|
||||
createSubscription() {
|
||||
const vm = this;
|
||||
vm.notificationFilterSubject = new Subject();
|
||||
return timer(0, 60000)
|
||||
.pipe(
|
||||
merge(vm.notificationFilterSubject),
|
||||
concatMap(this.fetchNotificationFilters),
|
||||
)
|
||||
.subscribe({
|
||||
next: data => {
|
||||
vm.notificationFilters = data;
|
||||
},
|
||||
error: error => {
|
||||
console.warn('Fetching notification filters failed:', error);
|
||||
vm.errors.push(error);
|
||||
}
|
||||
});
|
||||
},
|
||||
async fetchNotificationFilters() {
|
||||
if (this.hasNotificationFiltersSupport) {
|
||||
const response = await NotificationFilter.getFilters();
|
||||
return response.data;
|
||||
}
|
||||
return [];
|
||||
},
|
||||
handleFilterChange(event) {
|
||||
this.toggleNotificationFilterSettingsFor(null);
|
||||
this.notificationFilterSubject.next(event);
|
||||
},
|
||||
toggleNotificationFilterSettingsFor(obj) {
|
||||
this.showNotificationFilterSettingsObject = obj ? obj : null;
|
||||
},
|
||||
hasActiveFilter(object) {
|
||||
return this.notificationFilters.findIndex(f => f.affects(object)) >= 0;
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
this.scrollIntoView(this.selected, 'instant');
|
||||
this.hasNotificationFiltersSupport = await NotificationFilter.isSupported();
|
||||
this.fetchNotificationFilters();
|
||||
},
|
||||
watch: {
|
||||
selected(newVal) {
|
||||
this.scrollIntoView(newVal);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "~@/assets/css/utilities";
|
||||
|
||||
.application-list__item {
|
||||
transition: all $easing $speed;
|
||||
|
||||
&.is-active {
|
||||
margin: 0.75rem -0.75rem;
|
||||
max-width: unset;
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
*:not(.is-active) > &:hover {
|
||||
background-color: $white-bis;
|
||||
}
|
||||
|
||||
& > *:not(:first-child) {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
&__status {
|
||||
width: $gap;
|
||||
}
|
||||
|
||||
&__name,
|
||||
&__version {
|
||||
flex-grow: 1;
|
||||
flex-basis: 50%;
|
||||
}
|
||||
|
||||
&__name.title {
|
||||
margin: 0.75rem 0;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
justify-self: end;
|
||||
opacity: 0;
|
||||
transition: all $easing $speed;
|
||||
will-change: opacity;
|
||||
margin-right: ($gap / 2);
|
||||
display: flex;
|
||||
|
||||
*:hover > &,
|
||||
*.is-active & {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
& > * {
|
||||
width: ($gap / 2);
|
||||
height: ($gap / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.application__instances td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.instance {
|
||||
&__status {
|
||||
width: $gap;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
text-align: right;
|
||||
opacity: 0;
|
||||
transition: all $easing $speed;
|
||||
will-change: opacity;
|
||||
margin-right: $gap;
|
||||
|
||||
*:hover > & {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
& > * {
|
||||
width: ($gap / 2);
|
||||
height: ($gap / 2);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,60 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<span>
|
||||
<span class="has-text-warning" v-if="error">
|
||||
<font-awesome-icon icon="exclamation-triangle" />
|
||||
</span>
|
||||
<span :class="{ 'badge is-badge-danger' : downCount > 0 }" :data-badge="downCount">
|
||||
Applications
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
applications: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
error: {
|
||||
type: Error,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
downCount() {
|
||||
return this.applications.reduce((current, next) => {
|
||||
return current + (next.instances.filter(instance => instance.statusInfo.status !== 'UP').length);
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
downCount(newVal, oldVal) {
|
||||
if ((newVal === 0) !== (oldVal === 0)) {
|
||||
this.updateFavicon(newVal === 0);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateFavicon(up) {
|
||||
document.querySelector('link[rel*="icon"]').href = up ? 'assets/img/favicon.png' : 'assets/img/favicon-danger.png';
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,130 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div v-if="error" class="message is-warning">
|
||||
<div class="message-body">
|
||||
<strong>
|
||||
<font-awesome-icon class="has-text-warning" icon="exclamation-triangle" />
|
||||
Server connection failed.
|
||||
</strong>
|
||||
<p v-text="error.message" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="level applications-stats">
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Applications</p>
|
||||
<p class="title" v-text="applicationsCount">1</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Instances</p>
|
||||
<p class="title" v-text="instancesCount">1</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div v-if="downCount === 0">
|
||||
<p class="heading">Status</p>
|
||||
<p class="title has-text-success">all up</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p class="heading">instances down</p>
|
||||
<p class="title has-text-danger" v-text="downCount" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="application-group" v-for="group in statusGroups" :key="group.status">
|
||||
<p class="heading" v-text="group.status" />
|
||||
<applications-list :applications="group.applications" :selected="selected" />
|
||||
</div>
|
||||
<div v-if="statusGroups.length === 0">
|
||||
<p class="is-muted">No applications registered.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import transform from 'lodash/transform';
|
||||
import applicationsList from './applications-list';
|
||||
import handle from './handle';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
applications: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
error: {
|
||||
type: Error,
|
||||
default: null
|
||||
},
|
||||
selected: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line vue/no-unused-components
|
||||
components: {applicationsList},
|
||||
computed: {
|
||||
statusGroups() {
|
||||
const byStatus = groupBy(this.applications, application => application.status);
|
||||
const list = transform(byStatus, (result, value, key) => {
|
||||
result.push({status: key, applications: sortBy(value, [application => application.name])})
|
||||
}, []);
|
||||
return sortBy(list, [item => item.status]);
|
||||
},
|
||||
applicationsCount() {
|
||||
return this.applications.length;
|
||||
},
|
||||
instancesCount() {
|
||||
return this.applications.reduce((current, next) => current + next.instances.length, 0);
|
||||
},
|
||||
downCount() {
|
||||
return this.applications.reduce((current, next) => {
|
||||
return current + (next.instances.filter(instance => instance.statusInfo.status !== 'UP').length);
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
install({viewRegistry}) {
|
||||
viewRegistry.addView({
|
||||
path: '/applications/:selected?',
|
||||
props: true,
|
||||
name: 'applications',
|
||||
label: 'Applications',
|
||||
handle,
|
||||
order: 0,
|
||||
component: this
|
||||
});
|
||||
viewRegistry.addRedirect('/', 'applications');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
@import "~@/assets/css/utilities";
|
||||
|
||||
.application-group {
|
||||
margin: $gap 0;
|
||||
}
|
||||
</style>
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="box">
|
||||
<template v-if="!activeFilter">
|
||||
<div class="field">
|
||||
<p class="control has-inline-text">
|
||||
Suppress notifications on <code v-text="object.id || object.name" /> for
|
||||
<span class="select">
|
||||
<select v-model="ttl" @click.stop>
|
||||
<option v-for="option in ttlOptions"
|
||||
:key="`ttl-instance-${option.value}`"
|
||||
:value="option.value" v-text="option.label"
|
||||
/>
|
||||
</select>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field is-grouped is-grouped-right">
|
||||
<div class="control">
|
||||
<button class="button is-warning" :class="{'is-loading' : actionState === 'executing'}"
|
||||
@click.stop="addFilter"
|
||||
><font-awesome-icon icon="bell-slash" /> Suppress
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="field">
|
||||
<p class="control has-inline-text">
|
||||
Notifications on <code v-text="object.id || object.name" /> are suppressed for
|
||||
<strong v-text="activeFilter.expiry ? activeFilter.expiry.locale('en').fromNow(true) : 'ever' " />.
|
||||
</p>
|
||||
</div>
|
||||
<div class="field is-grouped is-grouped-right">
|
||||
<div class="control">
|
||||
<button class="button" :class="{'is-loading' : actionState === 'executing'}" @click.stop="deleteActiveFilter">
|
||||
<font-awesome-icon icon="bell" /> Unsuppress
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import NotificationFilter from '@/services/notification-filter';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
object: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
notificationFilters: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data: () => ({
|
||||
ttl: 5 * 60 * 1000,
|
||||
ttlOptions: [
|
||||
{label: '5 minutes', value: 5 * 60 * 1000},
|
||||
{label: '15 minutes', value: 15 * 60 * 1000},
|
||||
{label: '30 minutes', value: 30 * 60 * 1000},
|
||||
{label: '1 hour', value: 60 * 60 * 1000},
|
||||
{label: '3 hours', value: 3 * 60 * 60 * 1000},
|
||||
{label: '8 hours', value: 8 * 60 * 60 * 1000},
|
||||
{label: '24 hours', value: 24 * 60 * 60 * 1000},
|
||||
{label: 'ever', value: -1}
|
||||
],
|
||||
actionState: null
|
||||
}),
|
||||
computed: {
|
||||
activeFilter() {
|
||||
return this.notificationFilters.find(f => f.affects(this.object));
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async addFilter() {
|
||||
this.actionState = 'executing';
|
||||
try {
|
||||
const response = await NotificationFilter.addFilter(this.object, this.ttl);
|
||||
this.actionState = 'completed';
|
||||
this.$emit('filter-added', response.data)
|
||||
} catch (error) {
|
||||
console.warn('Adding notification filter failed:', error);
|
||||
}
|
||||
},
|
||||
async deleteActiveFilter() {
|
||||
this.actionState = 'executing';
|
||||
try {
|
||||
await this.activeFilter.delete();
|
||||
this.actionState = 'completed';
|
||||
this.$emit('filter-deleted', this.activeFilter.id);
|
||||
} catch (error) {
|
||||
this.actionState = 'failed';
|
||||
console.warn('Deleting notification filter failed:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.control.has-inline-text {
|
||||
line-height: 2.25em;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const views = [];
|
||||
|
||||
/* global require */
|
||||
const context = require.context('.', true, /^\.\/.+\/index\.(js|vue)$/);
|
||||
context.keys().forEach(function (key) {
|
||||
const defaultExport = context(key).default;
|
||||
if (defaultExport && defaultExport.install) {
|
||||
views.push(defaultExport)
|
||||
}
|
||||
});
|
||||
|
||||
export default views;
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<table class="auditevents table is-hoverable is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Timestamp</th>
|
||||
<th>Event</th>
|
||||
<th>Principal</th>
|
||||
<th>Remote address</th>
|
||||
<th>Session Id</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="event in events">
|
||||
<tr class="is-selectable"
|
||||
:class="{ 'auditevents__event--is-detailed' : showDetails[event.key] }"
|
||||
@click="showDetails[event.key] ? $delete(showDetails, event.key) : $set(showDetails, event.key, true)"
|
||||
:key="event.key"
|
||||
>
|
||||
<td v-text="event.timestamp.format('L HH:mm:ss.SSS')" />
|
||||
<td>
|
||||
<span v-text="event.type" class="tag"
|
||||
:class="{ 'is-success' : event.isSuccess(), 'is-danger' : event.isFailure() }"
|
||||
/>
|
||||
</td>
|
||||
<td v-if="hasSessionEndpoint && event.principal">
|
||||
<router-link v-text="event.principal"
|
||||
:to="{ name: 'instances/sessions', params: { 'instanceId' : instance.id }, query: { username : event.principal} }"
|
||||
/>
|
||||
</td>
|
||||
<td v-else v-text="event.principal" />
|
||||
<td v-text="event.remoteAddress" />
|
||||
<td v-if="hasSessionEndpoint && event.sessionId">
|
||||
<router-link v-text="event.sessionId"
|
||||
:to="{ name: 'instances/sessions', params: { 'instanceId' : instance.id }, query: { sessionId : event.sessionId } }"
|
||||
/>
|
||||
</td>
|
||||
<td v-else v-text="event.sessionId" />
|
||||
</tr>
|
||||
<tr :key="`${event.key}-detail`" v-if="showDetails[event.key]">
|
||||
<td colspan="5">
|
||||
<pre class="auditevents__event-detail" v-text="toJson(event.data)" />
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<tr v-if="events.length === 0">
|
||||
<td class="is-muted" colspan="5">
|
||||
<p v-if="isLoading" class="is-loading">Loading Audit Events...</p>
|
||||
<p v-else>No Audit Events found.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Instance from '@/services/instance';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
events: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
instance: {
|
||||
type: Instance,
|
||||
required: true
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data: () => ({
|
||||
showDetails: {}
|
||||
}),
|
||||
computed: {
|
||||
hasSessionEndpoint() {
|
||||
return this.instance.hasEndpoint('sessions');
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
prettyBytes,
|
||||
toJson(obj) {
|
||||
return JSON.stringify(obj, null, 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.auditevents {
|
||||
table-layout: fixed;
|
||||
|
||||
td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
&__event--is-detailed td {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
&__event-detail {
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,214 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<section class="section">
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<p class="control is-expanded">
|
||||
<input
|
||||
class="input"
|
||||
type="search"
|
||||
placeholder="Principal"
|
||||
v-model.trim="filter.principal"
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<p class="control is-expanded">
|
||||
<input
|
||||
list="auditevent-type"
|
||||
class="input"
|
||||
type="search"
|
||||
placeholder="Type"
|
||||
v-model="filter.type"
|
||||
>
|
||||
<datalist id="auditevent-type">
|
||||
<option value="AUTHENTICATION_FAILURE" />
|
||||
<option value="AUTHENTICATION_SUCCESS" />
|
||||
<option value="AUTHENTICATION_SWITCH" />
|
||||
<option value="AUTHORIZATION_FAILURE" />
|
||||
</datalist>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<p class="control is-expanded">
|
||||
<input
|
||||
class="input"
|
||||
type="datetime-local"
|
||||
placeholder="Date"
|
||||
:value="formatDate(filter.after)"
|
||||
@input="filter.after = parseDate($event.target.value)"
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template>
|
||||
<div v-if="error" class="message is-danger">
|
||||
<div class="message-body">
|
||||
<strong>
|
||||
<font-awesome-icon
|
||||
class="has-text-danger"
|
||||
icon="exclamation-triangle"
|
||||
/>
|
||||
Fetching audit events failed.
|
||||
</strong>
|
||||
<p v-text="error.message" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="isOldAuditevents"
|
||||
class="message is-warning"
|
||||
>
|
||||
<div class="message-body">
|
||||
Audit Log is not supported for Spring Boot 1.x applications.
|
||||
</div>
|
||||
</div>
|
||||
<auditevents-list
|
||||
:instance="instance"
|
||||
:events="events"
|
||||
:is-loading="isLoading"
|
||||
/>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import subscribing from '@/mixins/subscribing';
|
||||
import Instance from '@/services/instance';
|
||||
import {concatMap, debounceTime, merge, Subject, tap, timer} from '@/utils/rxjs';
|
||||
import AuditeventsList from '@/views/instances/auditevents/auditevents-list';
|
||||
import uniqBy from 'lodash/uniqBy';
|
||||
import moment from 'moment';
|
||||
|
||||
class Auditevent {
|
||||
constructor({timestamp, ...event}) {
|
||||
Object.assign(this, event);
|
||||
this.timestamp = moment(timestamp);
|
||||
}
|
||||
|
||||
get key() {
|
||||
return `${this.timestamp}-${this.type}-${this.principal}`;
|
||||
}
|
||||
|
||||
get remoteAddress() {
|
||||
return this.data && this.data.details && this.data.details.remoteAddress || null;
|
||||
}
|
||||
|
||||
get sessionId() {
|
||||
return this.data && this.data.details && this.data.details.sessionId || null;
|
||||
}
|
||||
|
||||
isSuccess() {
|
||||
return this.type.toLowerCase().includes('success');
|
||||
}
|
||||
|
||||
isFailure() {
|
||||
return this.type.toLowerCase().includes('failure');
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
props: {
|
||||
instance: {
|
||||
type: Instance,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
mixins: [subscribing],
|
||||
components: {AuditeventsList},
|
||||
data: () => ({
|
||||
isLoading: false,
|
||||
error: null,
|
||||
events: [],
|
||||
filter: {
|
||||
after: moment().startOf('day'),
|
||||
type: null,
|
||||
principal: null
|
||||
},
|
||||
isOldAuditevents: false
|
||||
}),
|
||||
watch: {
|
||||
filter: {
|
||||
deep: true,
|
||||
handler() {
|
||||
this.filterChanged.next();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
formatDate(value) {
|
||||
return value.format(moment.HTML5_FMT.DATETIME_LOCAL);
|
||||
},
|
||||
parseDate(value) {
|
||||
return moment(value, moment.HTML5_FMT.DATETIME_LOCAL, true);
|
||||
},
|
||||
async fetchAuditevents() {
|
||||
this.isLoading = true;
|
||||
const response = await this.instance.fetchAuditevents(this.filter);
|
||||
const converted = response.data.events.map(event => new Auditevent(event));
|
||||
converted.reverse();
|
||||
this.isLoading = false;
|
||||
return converted;
|
||||
},
|
||||
createSubscription() {
|
||||
const vm = this;
|
||||
vm.filterChanged = new Subject();
|
||||
vm.error = null;
|
||||
return timer(0, 5000)
|
||||
.pipe(
|
||||
merge(vm.filterChanged.pipe(
|
||||
debounceTime(250),
|
||||
tap({
|
||||
next: () => vm.events = []
|
||||
})
|
||||
)),
|
||||
concatMap(this.fetchAuditevents)
|
||||
)
|
||||
.subscribe({
|
||||
next: events => {
|
||||
vm.addEvents(events);
|
||||
},
|
||||
error: error => {
|
||||
console.warn('Fetching audit events failed:', error);
|
||||
if (error.response.headers['content-type'].includes('application/vnd.spring-boot.actuator.v2')) {
|
||||
vm.error = error;
|
||||
} else {
|
||||
vm.isOldAuditevents = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
addEvents(events) {
|
||||
this.events = uniqBy(this.events ? events.concat(this.events) : events, event => event.key);
|
||||
}
|
||||
},
|
||||
install({viewRegistry}) {
|
||||
viewRegistry.addView({
|
||||
name: 'instances/auditevents',
|
||||
parent: 'instances',
|
||||
path: 'auditevents',
|
||||
component: this,
|
||||
label: 'Audit Log',
|
||||
group: 'Security',
|
||||
order: 600,
|
||||
isEnabled: ({instance}) => instance.hasEndpoint('auditevents')
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,141 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<table class="beans table is-fullwidth is-hoverable">
|
||||
<tbody>
|
||||
<template v-for="bean in beans">
|
||||
<tr
|
||||
class="is-selectable"
|
||||
:key="bean.name"
|
||||
@click="showDetails[bean.name] ? $delete(showDetails, bean.name) : $set(showDetails, bean.name, true)"
|
||||
>
|
||||
<td class="is-breakable">
|
||||
<span
|
||||
v-text="bean.shortName"
|
||||
:title="bean.name"
|
||||
/><br>
|
||||
<small
|
||||
class="is-muted"
|
||||
v-text="bean.shortType"
|
||||
:title="bean.type"
|
||||
/>
|
||||
</td>
|
||||
<td><span
|
||||
v-text="bean.scope"
|
||||
class="tag"
|
||||
/></td>
|
||||
</tr>
|
||||
<tr
|
||||
:key="`${bean.name}-detail`"
|
||||
v-if="showDetails[bean.name]"
|
||||
>
|
||||
<td
|
||||
colspan="2"
|
||||
class="has-background-white-ter"
|
||||
>
|
||||
<table class="table is-narrow is-fullwidth beans__bean-detail">
|
||||
<tbody>
|
||||
<tr v-if="bean.name !== bean.shortName">
|
||||
<th>
|
||||
<small>Name</small>
|
||||
</th>
|
||||
<td
|
||||
class="is-breakable"
|
||||
v-text="bean.name"
|
||||
/>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="(alias, idx) in bean.aliases"
|
||||
:key="alias"
|
||||
>
|
||||
<th
|
||||
v-if="idx === 0"
|
||||
:rowspan="bean.aliases.length"
|
||||
>
|
||||
<small>Aliases</small>
|
||||
</th>
|
||||
<td
|
||||
class="is-breakable"
|
||||
v-text="alias"
|
||||
/>
|
||||
</tr>
|
||||
<tr v-if="bean.type !== bean.shortType">
|
||||
<th>
|
||||
<small>Type</small>
|
||||
</th>
|
||||
<td
|
||||
class="is-breakable"
|
||||
v-text="bean.type"
|
||||
/>
|
||||
</tr>
|
||||
<tr v-if="bean.resource">
|
||||
<th>
|
||||
<small>Resource</small>
|
||||
</th>
|
||||
<td
|
||||
class="is-breakable"
|
||||
v-text="bean.resource"
|
||||
/>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="(dependency, idx) in bean.dependencies"
|
||||
:key="dependency"
|
||||
>
|
||||
<th
|
||||
v-if="idx === 0"
|
||||
:rowspan="bean.dependencies.length"
|
||||
>
|
||||
<small>Dependencies</small>
|
||||
</th>
|
||||
<td
|
||||
class="is-breakable"
|
||||
v-text="dependency"
|
||||
/>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
beans: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data: () => ({
|
||||
showDetails: {}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import "~@/assets/css/utilities";
|
||||
|
||||
.beans {
|
||||
table.beans__bean-detail {
|
||||
tbody tr {
|
||||
pointer-events: none;
|
||||
background-color: $white-ter;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,150 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<section class="section" :class="{isLoading: isLoading}">
|
||||
<div class="container">
|
||||
<div
|
||||
v-if="error"
|
||||
class="message is-danger"
|
||||
>
|
||||
<div class="message-body">
|
||||
<strong>
|
||||
<font-awesome-icon
|
||||
class="has-text-danger"
|
||||
icon="exclamation-triangle"
|
||||
/>
|
||||
Fetching beans failed.
|
||||
</strong>
|
||||
<p v-text="error.message" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<p class="control is-expanded">
|
||||
<input
|
||||
class="input"
|
||||
type="search"
|
||||
placeholder="filter"
|
||||
v-model="filter"
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<template v-for="context in filteredContexts">
|
||||
<h3 class="title" v-text="context.name" :key="context.name" />
|
||||
<beans-list :beans="context.beans" :key="`${context.name}-beans`" />
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Instance from '@/services/instance';
|
||||
import {compareBy} from '@/utils/collections';
|
||||
import shortenClassname from '@/utils/shortenClassname';
|
||||
import BeansList from '@/views/instances/beans/beans-list';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
|
||||
class Bean {
|
||||
constructor(name, bean) {
|
||||
Object.assign(this, bean);
|
||||
this.name = name;
|
||||
this.shortName = shortenClassname(this.name, 80);
|
||||
this.shortType = shortenClassname(this.type, 80);
|
||||
}
|
||||
}
|
||||
|
||||
const flattenBeans = beans => {
|
||||
return Object.keys(beans)
|
||||
.map((key) => {
|
||||
return new Bean(key, beans[key]);
|
||||
});
|
||||
};
|
||||
|
||||
const flattenContexts = beanData => {
|
||||
if (isEmpty(beanData.contexts)) {
|
||||
return [];
|
||||
}
|
||||
return Object.keys(beanData.contexts)
|
||||
.map((key) => ({
|
||||
beans: flattenBeans(beanData.contexts[key].beans),
|
||||
name: key,
|
||||
parent: beanData.contexts[key].parentId
|
||||
}));
|
||||
};
|
||||
|
||||
export default {
|
||||
components: {BeansList},
|
||||
props: {
|
||||
instance: {
|
||||
type: Instance,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data: () => ({
|
||||
isLoading: false,
|
||||
error: null,
|
||||
contexts: [],
|
||||
filter: '',
|
||||
}),
|
||||
computed: {
|
||||
filteredContexts() {
|
||||
const filterFn = this.getFilterFn();
|
||||
return this.contexts.map(ctx => ({
|
||||
...ctx,
|
||||
beans: ctx.beans.filter(filterFn).sort(compareBy(bean => bean.name))
|
||||
}));
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getFilterFn() {
|
||||
if (!this.filter || this.filter === '') {
|
||||
return () => true;
|
||||
}
|
||||
const regex = new RegExp(this.filter, 'i');
|
||||
return bean => (bean.name.match(regex) || bean.aliases.some(alias => alias.match(regex)));
|
||||
},
|
||||
async fetchBeans() {
|
||||
this.error = null;
|
||||
this.isLoading = true;
|
||||
try {
|
||||
const res = await this.instance.fetchBeans();
|
||||
this.contexts = flattenContexts(res.data);
|
||||
} catch (error) {
|
||||
console.warn('Fetching beans failed:', error);
|
||||
this.error = error;
|
||||
}
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.fetchBeans();
|
||||
},
|
||||
install({viewRegistry}) {
|
||||
viewRegistry.addView({
|
||||
name: 'instances/beans',
|
||||
parent: 'instances',
|
||||
path: 'beans',
|
||||
group: 'Insights',
|
||||
component: this,
|
||||
label: 'Beans',
|
||||
order: 110,
|
||||
isEnabled: ({instance}) => instance.hasEndpoint('beans')
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<table class="caches table is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Cache Manager</th>
|
||||
<th class="is-narrow">
|
||||
<sba-confirm-button
|
||||
class="button"
|
||||
:class="{'is-loading' : clearingAll === 'executing', 'is-info' : clearingAll === 'completed', 'is-danger' : clearingAll === 'failed'}"
|
||||
:disabled="clearingAll !== null"
|
||||
@click="clearCaches"
|
||||
>
|
||||
<span v-if="clearingAll === 'completed'">Cleared</span>
|
||||
<span v-else-if="clearingAll === 'failed'">Failed</span>
|
||||
<span v-else><font-awesome-icon icon="trash" /> Clear</span>
|
||||
</sba-confirm-button>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="cache in caches"
|
||||
:key="cache.key"
|
||||
>
|
||||
<td>
|
||||
<span
|
||||
class="is-breakable"
|
||||
v-text="cache.name"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
class="is-breakable"
|
||||
v-text="cache.cacheManager"
|
||||
/>
|
||||
</td>
|
||||
<td class="is-narrow">
|
||||
<button class="button"
|
||||
:class="{ 'is-loading' : clearing[cache.name] === 'executing', 'is-info' : clearing[cache.name] === 'completed', 'is-danger' : clearing[cache.name] === 'failed' }"
|
||||
:disabled="cache.name in clearing" @click="clearCache(cache.name)"
|
||||
>
|
||||
<span v-if="clearing[cache.name] === 'completed'">Cleared</span>
|
||||
<span v-else-if="clearing[cache.name] === 'failed'">Failed</span>
|
||||
<span v-else><font-awesome-icon icon="trash" /> Clear</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="caches.length === 0">
|
||||
<td class="is-muted" colspan="3 ">
|
||||
<p v-if="isLoading" class="is-loading">Loading Caches...</p>
|
||||
<p v-else>No caches found.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
<script>
|
||||
import Instance from '@/services/instance';
|
||||
import {concatMap, from, listen, of, tap} from '@/utils/rxjs';
|
||||
|
||||
|
||||
export default {
|
||||
name: 'CachesList',
|
||||
props: {
|
||||
caches: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
instance: {
|
||||
type: Instance,
|
||||
required: true
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data: () => ({
|
||||
clearing: {},
|
||||
clearingAll: null,
|
||||
}),
|
||||
methods: {
|
||||
clearCaches() {
|
||||
const vm = this;
|
||||
from(vm.instance.clearCaches())
|
||||
.pipe(listen(status => vm.clearingAll = status))
|
||||
.subscribe({
|
||||
complete: () => {
|
||||
setTimeout(() => vm.clearingAll = null, 2500);
|
||||
return vm.$emit('cleared', '*');
|
||||
}
|
||||
});
|
||||
},
|
||||
clearCache(cache) {
|
||||
const vm = this;
|
||||
vm._clearCache(cache)
|
||||
.pipe(listen(status => vm.$set(vm.clearing, cache, status)))
|
||||
.subscribe({
|
||||
complete: () => {
|
||||
setTimeout(() => vm.$delete(vm.clearing, cache), 2500);
|
||||
return vm.$emit('cleared', cache);
|
||||
},
|
||||
});
|
||||
},
|
||||
_clearCache(cache) {
|
||||
const vm = this;
|
||||
return of(cache)
|
||||
.pipe(
|
||||
concatMap(async cache => {
|
||||
await vm.instance.clearCache(cache);
|
||||
return cache;
|
||||
}),
|
||||
tap({
|
||||
error: error => {
|
||||
console.warn(`Clearing cache ${cache} failed:`, error);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.caches {
|
||||
td, th {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,138 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<section class="section">
|
||||
<div
|
||||
class="container"
|
||||
>
|
||||
<div
|
||||
v-if="error"
|
||||
class="message is-danger"
|
||||
>
|
||||
<div class="message-body">
|
||||
<strong>
|
||||
<font-awesome-icon
|
||||
class="has-text-danger"
|
||||
icon="exclamation-triangle"
|
||||
/>
|
||||
Fetching caches failed.
|
||||
</strong>
|
||||
<p v-text="error.message" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field has-addons">
|
||||
<p class="control is-expanded">
|
||||
<input
|
||||
class="input"
|
||||
type="search"
|
||||
placeholder="name filter"
|
||||
v-model="filter"
|
||||
>
|
||||
</p>
|
||||
<p class="control">
|
||||
<span class="button is-static">
|
||||
<span v-text="filteredCaches.length" />
|
||||
/
|
||||
<span v-text="caches.length" />
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<caches-list :instance="instance" :caches="filteredCaches" :is-loading="isLoading" />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Instance from '@/services/instance';
|
||||
import CachesList from '@/views/instances/caches/caches-list';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
|
||||
const flattenCaches = cacheData => {
|
||||
if (isEmpty(cacheData.cacheManagers)) {
|
||||
return [];
|
||||
}
|
||||
const mappend = Object.entries(cacheData.cacheManagers).flatMap(
|
||||
([cacheManagerName, v]) => Object.keys(v.caches)
|
||||
.map(cacheName => ({cacheManager: cacheManagerName, name: cacheName, key: `${cacheManagerName}:${cacheName}`}))
|
||||
);
|
||||
return mappend.sort((c1, c2) => c1.key.localeCompare(c2.key));
|
||||
};
|
||||
|
||||
export default {
|
||||
components: {CachesList},
|
||||
props: {
|
||||
instance: {
|
||||
type: Instance,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data: () => ({
|
||||
isLoading: false,
|
||||
error: null,
|
||||
caches: [],
|
||||
filter: '',
|
||||
}),
|
||||
computed: {
|
||||
filteredCaches() {
|
||||
let filterFn = this.getFilterFn();
|
||||
return filterFn ? this.caches.filter(filterFn) : this.caches;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async fetchCaches() {
|
||||
this.error = null;
|
||||
this.isLoading = true;
|
||||
try {
|
||||
const res = await this.instance.fetchCaches();
|
||||
this.caches = flattenCaches(res.data);
|
||||
} catch (error) {
|
||||
console.warn('Fetching caches failed:', error);
|
||||
this.error = error;
|
||||
}
|
||||
this.isLoading = false;
|
||||
},
|
||||
getFilterFn() {
|
||||
let filterFn = null;
|
||||
|
||||
if (this.filter) {
|
||||
const normalizedFilter = this.filter.toLowerCase();
|
||||
filterFn = cache => cache.name.toLowerCase().includes(normalizedFilter);
|
||||
}
|
||||
|
||||
return filterFn;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.fetchCaches();
|
||||
},
|
||||
install({viewRegistry}) {
|
||||
viewRegistry.addView({
|
||||
name: 'instances/caches',
|
||||
parent: 'instances',
|
||||
path: 'caches',
|
||||
group: 'Data',
|
||||
component: this,
|
||||
label: 'Caches',
|
||||
order: 970,
|
||||
isEnabled: ({instance}) => instance.hasEndpoint('caches')
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<section class="section" :class="{ 'is-loading' : !hasLoaded }">
|
||||
<div v-if="error" class="message is-danger">
|
||||
<div class="message-body">
|
||||
<strong>
|
||||
<font-awesome-icon class="has-text-danger" icon="exclamation-triangle" />
|
||||
Fetching configuration properties failed.
|
||||
</strong>
|
||||
<p v-text="error.message" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="field has-addons" v-if="configprops">
|
||||
<p class="control is-expanded">
|
||||
<input class="input" type="search" placeholder="name / value filter" v-model="filter">
|
||||
</p>
|
||||
</div>
|
||||
<sba-panel :header-sticks-below="['#navigation']"
|
||||
v-for="bean in configurationPropertiesBeans"
|
||||
:key="bean.name"
|
||||
:title=" bean.name"
|
||||
>
|
||||
<table class="table is-fullwidth"
|
||||
v-if="Object.keys(bean.properties).length > 0"
|
||||
>
|
||||
<tr v-for="(value, name) in bean.properties" :key="`${bean.name}-${name}`">
|
||||
<td v-text="name" />
|
||||
<td class="is-breakable" v-text="value" />
|
||||
</tr>
|
||||
</table>
|
||||
<p class="is-muted" v-else>No properties set</p>
|
||||
</sba-panel>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Instance from '@/services/instance';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import mapKeys from 'lodash/mapKeys';
|
||||
import pickBy from 'lodash/pickBy';
|
||||
|
||||
const filterProperty = (needle) => (value, name) => {
|
||||
return name.toString().toLowerCase().includes(needle) || value.toString().toLowerCase().includes(needle);
|
||||
};
|
||||
const filterProperties = (needle, properties) => pickBy(properties, filterProperty(needle));
|
||||
const filterConfigurationProperties = (needle) => (propertySource) => {
|
||||
if (!propertySource || !propertySource.properties) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...propertySource,
|
||||
properties: filterProperties(needle, propertySource.properties)
|
||||
};
|
||||
};
|
||||
|
||||
function flattenBean(obj, prefix = '') {
|
||||
if (Object(obj) !== obj) {
|
||||
return {[prefix]: obj};
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
if (obj.length === 0) {
|
||||
return {[prefix]: []};
|
||||
} else {
|
||||
return obj.map(
|
||||
(value, idx) => flattenBean(value, `${prefix}[${idx}]`)
|
||||
).reduce((c, n) => ({...c, ...n}), {});
|
||||
}
|
||||
} else {
|
||||
if (isEmpty(obj)) {
|
||||
return {[prefix]: {}};
|
||||
} else {
|
||||
return Object.entries(obj).map(
|
||||
([name, value]) => flattenBean(value, prefix ? `${prefix}.${name}` : name)
|
||||
).reduce((c, n) => ({...c, ...n}), {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const flattenConfigurationPropertiesBeans = (configprops) => {
|
||||
const propertySources = [];
|
||||
const contextNames = Object.keys(configprops.contexts);
|
||||
|
||||
for (const contextName of contextNames) {
|
||||
const context = configprops.contexts[contextName];
|
||||
const beanNames = Object.keys(context.beans);
|
||||
|
||||
for (const beanName of beanNames) {
|
||||
const bean = context.beans[beanName];
|
||||
const properties = mapKeys(flattenBean(bean.properties), (value, key) => `${bean.prefix}.${key}`);
|
||||
propertySources.push({
|
||||
name: contextNames.length > 1 ? `${contextName}: ${beanName}` : beanName,
|
||||
properties
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return propertySources;
|
||||
};
|
||||
|
||||
export default {
|
||||
props: {
|
||||
instance: {
|
||||
type: Instance,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data: () => ({
|
||||
hasLoaded: false,
|
||||
error: null,
|
||||
configprops: null,
|
||||
filter: null
|
||||
}),
|
||||
computed: {
|
||||
configurationPropertiesBeans() {
|
||||
if (!this.configprops) {
|
||||
return [];
|
||||
}
|
||||
const configurationProperties = flattenConfigurationPropertiesBeans(this.configprops);
|
||||
if (!this.filter) {
|
||||
return configurationProperties;
|
||||
}
|
||||
return configurationProperties
|
||||
.map(filterConfigurationProperties(this.filter.toLowerCase()))
|
||||
.filter(ps => ps && Object.keys(ps.properties).length > 0);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.fetchConfigprops();
|
||||
},
|
||||
methods: {
|
||||
async fetchConfigprops() {
|
||||
this.error = null;
|
||||
try {
|
||||
const configprops = await this.instance.fetchConfigprops();
|
||||
this.configprops = configprops.data;
|
||||
} catch (error) {
|
||||
console.warn('Fetching configuration properties failed:', error);
|
||||
this.error = error;
|
||||
}
|
||||
this.hasLoaded = true;
|
||||
}
|
||||
},
|
||||
install({viewRegistry}) {
|
||||
viewRegistry.addView({
|
||||
name: 'instances/configprops',
|
||||
parent: 'instances',
|
||||
path: 'configprops',
|
||||
component: this,
|
||||
label: 'Configuration Properties',
|
||||
group: 'Insights',
|
||||
order: 110,
|
||||
isEnabled: ({instance}) => instance.hasEndpoint('configprops')
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,139 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="cache-chart">
|
||||
<svg class="cache-chart__svg" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import d3 from '@/utils/d3';
|
||||
import moment from 'moment';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data: () => ({}),
|
||||
methods: {
|
||||
drawChart(_data) {
|
||||
const vm = this;
|
||||
const data = _data.length === 1 ? _data.concat([{..._data[0], timestamp: _data[0].timestamp + 1}]) : _data;
|
||||
|
||||
///setup x and y scale
|
||||
const extent = d3.extent(data, d => d.timestamp);
|
||||
const x = d3.scaleTime()
|
||||
.range([0, vm.width])
|
||||
.domain(extent);
|
||||
|
||||
const y = d3.scaleLinear()
|
||||
.range([vm.height, 0])
|
||||
.domain([0, d3.max(data, d => d.total) * 1.05]);
|
||||
|
||||
//draw areas
|
||||
const miss = vm.areas.selectAll('.cache-chart__area--miss')
|
||||
.data([data]);
|
||||
miss.enter().append('path')
|
||||
.merge(miss)
|
||||
.attr('class', 'cache-chart__area--miss')
|
||||
.attr('d', d3.area()
|
||||
.x(d => x(d.timestamp))
|
||||
.y0(d => y(d.hit))
|
||||
.y1(d => y(d.total)));
|
||||
miss.exit().remove();
|
||||
|
||||
const hit = vm.areas.selectAll('.cache-chart__area--hit')
|
||||
.data([data]);
|
||||
hit.enter().append('path')
|
||||
.merge(hit)
|
||||
.attr('class', 'cache-chart__area--hit')
|
||||
.attr('d', d3.area()
|
||||
.x(d => x(d.timestamp))
|
||||
.y0(y(0))
|
||||
.y1(d => y(d.hit)));
|
||||
hit.exit().remove();
|
||||
|
||||
//draw axis
|
||||
vm.xAxis.call(d3.axisBottom(x)
|
||||
.ticks(5)
|
||||
.tickFormat(d => moment(d).format('HH:mm:ss'))
|
||||
);
|
||||
|
||||
vm.yAxis.call(d3.axisLeft(y)
|
||||
.ticks(5)
|
||||
.tickFormat(d => d <= 1000 ? d : (d / 1000).toFixed(1) + 'K')
|
||||
);
|
||||
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const margin = {
|
||||
top: 5,
|
||||
right: 5,
|
||||
bottom: 30,
|
||||
left: 50,
|
||||
};
|
||||
|
||||
this.width = this.$el.getBoundingClientRect().width - margin.left - margin.right;
|
||||
this.height = this.$el.getBoundingClientRect().height - margin.top - margin.bottom;
|
||||
|
||||
this.chartLayer = d3.select(this.$el.querySelector('.cache-chart__svg'))
|
||||
.append('g')
|
||||
.attr('transform', `translate(${margin.left},${margin.top})`);
|
||||
|
||||
this.xAxis = this.chartLayer.append('g')
|
||||
.attr('class', 'cache-chart__axis-x')
|
||||
.attr('transform', `translate(0,${this.height})`);
|
||||
|
||||
this.yAxis = this.chartLayer.append('g')
|
||||
.attr('class', 'cache-chart__axis-y')
|
||||
.attr('stroke', null);
|
||||
|
||||
this.areas = this.chartLayer.append('g');
|
||||
|
||||
this.drawChart(this.data);
|
||||
},
|
||||
watch: {
|
||||
data: 'drawChart'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "~@/assets/css/utilities";
|
||||
|
||||
.cache-chart {
|
||||
&__svg {
|
||||
height: 159px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__area {
|
||||
&--miss {
|
||||
fill: $warning;
|
||||
opacity: 0.8;
|
||||
}
|
||||
&--hit {
|
||||
fill: $info;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="datasource-chart">
|
||||
<svg class="datasource-chart__svg" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import d3 from '@/utils/d3';
|
||||
import moment from 'moment';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
drawChart(_data) {
|
||||
const vm = this;
|
||||
const data = _data.length === 1 ? _data.concat([{..._data[0], timestamp: _data[0].timestamp + 1}]) : _data;
|
||||
|
||||
///setup x and y scale
|
||||
const extent = d3.extent(data, d => d.timestamp);
|
||||
const x = d3.scaleTime()
|
||||
.range([0, vm.width])
|
||||
.domain(extent);
|
||||
|
||||
const y = d3.scaleLinear()
|
||||
.range([vm.height, 0])
|
||||
.domain([0, d3.max(data, d => d.active) * 1.05]);
|
||||
|
||||
//draw max
|
||||
const max = vm.areas.selectAll('.datasource-chart__line--max')
|
||||
.data([data]);
|
||||
max.enter().append('path')
|
||||
.merge(max)
|
||||
.attr('class', 'datasource-chart__line--max')
|
||||
.attr('d', d3.line()
|
||||
.x(d => x(d.timestamp))
|
||||
.y(d => y(d.max)));
|
||||
max.exit().remove();
|
||||
|
||||
//draw areas
|
||||
const active = vm.areas.selectAll('.datasource-chart__area--active')
|
||||
.data([data]);
|
||||
active.enter().append('path')
|
||||
.merge(active)
|
||||
.attr('class', 'datasource-chart__area--active')
|
||||
.attr('d', d3.area()
|
||||
.x(d => x(d.timestamp))
|
||||
.y0(y(0))
|
||||
.y1(d => y(d.active)));
|
||||
active.exit().remove();
|
||||
|
||||
//draw axis
|
||||
vm.xAxis.call(d3.axisBottom(x)
|
||||
.ticks(5)
|
||||
.tickFormat(d => moment(d).format('HH:mm:ss'))
|
||||
);
|
||||
|
||||
vm.yAxis.call(d3.axisLeft(y)
|
||||
.ticks(5)
|
||||
);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const margin = {
|
||||
top: 5,
|
||||
right: 5,
|
||||
bottom: 30,
|
||||
left: 50,
|
||||
};
|
||||
|
||||
this.width = this.$el.getBoundingClientRect().width - margin.left - margin.right;
|
||||
this.height = this.$el.getBoundingClientRect().height - margin.top - margin.bottom;
|
||||
|
||||
this.chartLayer = d3.select(this.$el.querySelector('.datasource-chart__svg'))
|
||||
.append('g')
|
||||
.attr('transform', `translate(${margin.left},${margin.top})`);
|
||||
|
||||
this.xAxis = this.chartLayer.append('g')
|
||||
.attr('class', 'datasource-chart__axis-x')
|
||||
.attr('transform', `translate(0,${this.height})`);
|
||||
|
||||
this.yAxis = this.chartLayer.append('g')
|
||||
.attr('class', 'datasource-chart__axis-y')
|
||||
.attr('stroke', null);
|
||||
|
||||
this.areas = this.chartLayer.append('g');
|
||||
|
||||
this.drawChart(this.data);
|
||||
},
|
||||
watch: {
|
||||
data: 'drawChart'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "~@/assets/css/utilities";
|
||||
|
||||
.datasource-chart {
|
||||
&__svg {
|
||||
height: 159px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__area {
|
||||
&--active {
|
||||
fill: $info;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
&__line--max {
|
||||
stroke: $grey;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,144 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<sba-panel :title="`Cache: ${cacheName}`" v-if="hasLoaded">
|
||||
<div>
|
||||
<div v-if="error" class="message is-danger">
|
||||
<div class="message-body">
|
||||
<strong>
|
||||
<font-awesome-icon class="has-text-danger" icon="exclamation-triangle" />
|
||||
Fetching cache metrics failed.
|
||||
</strong>
|
||||
<p v-text="error.message" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="level cache-current" v-if="current">
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading has-bullet has-bullet-info">Hits</p>
|
||||
<p v-text="current.hit" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading has-bullet has-bullet-warning">Misses</p>
|
||||
<p v-text="current.miss" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Hit ratio</p>
|
||||
<p v-text="ratio" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="current.size" class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Size</p>
|
||||
<p v-text="current.size" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<cache-chart v-if="chartData.length > 0" :data="chartData" />
|
||||
</div>
|
||||
</sba-panel>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import subscribing from '@/mixins/subscribing';
|
||||
import Instance from '@/services/instance';
|
||||
import {concatMap, timer} from '@/utils/rxjs';
|
||||
import moment from 'moment';
|
||||
import cacheChart from './cache-chart';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
instance: {
|
||||
type: Instance,
|
||||
required: true
|
||||
},
|
||||
cacheName: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
mixins: [subscribing],
|
||||
// eslint-disable-next-line vue/no-unused-components
|
||||
components: {cacheChart},
|
||||
data: () => ({
|
||||
hasLoaded: false,
|
||||
error: null,
|
||||
current: null,
|
||||
disableSize: false,
|
||||
chartData: [],
|
||||
}),
|
||||
computed: {
|
||||
ratio() {
|
||||
if (this.current.total > 0) {
|
||||
return (this.current.hit / this.current.total * 100).toFixed(2) + '%';
|
||||
}
|
||||
return '-';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async fetchMetrics() {
|
||||
const responseHit = this.instance.fetchMetric('cache.gets', {name: this.cacheName, result: 'hit'});
|
||||
const responseMiss = this.instance.fetchMetric('cache.gets', {name: this.cacheName, result: 'miss'});
|
||||
let size = undefined;
|
||||
if (!this.disableSize) {
|
||||
const responsSize = this.instance.fetchMetric('cache.size', {name: this.cacheName});
|
||||
try {
|
||||
size = (await responsSize).data.measurements[0].value;
|
||||
} catch (error) {
|
||||
this.disableSize = true;
|
||||
console.warn('Fetching cache size failed - error is ignored', error)
|
||||
}
|
||||
}
|
||||
const hit = (await responseHit).data.measurements[0].value;
|
||||
const miss = (await responseMiss).data.measurements[0].value;
|
||||
return {
|
||||
hit,
|
||||
miss,
|
||||
total: hit + miss,
|
||||
size
|
||||
};
|
||||
},
|
||||
createSubscription() {
|
||||
const vm = this;
|
||||
return timer(0, 2500)
|
||||
.pipe(concatMap(vm.fetchMetrics))
|
||||
.subscribe({
|
||||
next: data => {
|
||||
vm.hasLoaded = true;
|
||||
vm.current = data;
|
||||
vm.chartData.push({...data, timestamp: moment().valueOf()});
|
||||
},
|
||||
error: error => {
|
||||
vm.hasLoaded = true;
|
||||
console.warn(`Fetching cache ${vm.cacheName} metrics failed:`, error);
|
||||
vm.error = error;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.datasource-current {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,65 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<details-cache v-for="cache in caches" :key="cache"
|
||||
:instance="instance" :cache-name="cache"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import subscribing from '@/mixins/subscribing';
|
||||
import Instance from '@/services/instance';
|
||||
import {concatMap, timer} from '@/utils/rxjs';
|
||||
import uniq from 'lodash/uniq';
|
||||
import detailsCache from './details-cache';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
instance: {
|
||||
type: Instance,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
mixins: [subscribing],
|
||||
// eslint-disable-next-line vue/no-unused-components
|
||||
components: {detailsCache},
|
||||
data: () => ({
|
||||
caches: [],
|
||||
}),
|
||||
methods: {
|
||||
async fetchCaches() {
|
||||
const response = await this.instance.fetchMetric('cache.gets');
|
||||
return uniq(response.data.availableTags.filter(tag => tag.tag === 'name')[0].values);
|
||||
},
|
||||
createSubscription() {
|
||||
const vm = this;
|
||||
return timer(0, 2500)
|
||||
.pipe(concatMap(this.fetchCaches))
|
||||
.subscribe({
|
||||
next: names => {
|
||||
vm.caches = names
|
||||
},
|
||||
error: error => {
|
||||
console.warn('Fetching caches failed:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<sba-panel :title="`Datasource: ${dataSource}`" v-if="hasLoaded">
|
||||
<div>
|
||||
<div v-if="error" class="message is-danger">
|
||||
<div class="message-body">
|
||||
<strong>
|
||||
<font-awesome-icon class="has-text-danger" icon="exclamation-triangle" />
|
||||
Fetching datasource metrics failed.
|
||||
</strong>
|
||||
<p v-text="error.message" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="level datasource-current" v-if="current">
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading has-bullet has-bullet-info">Active connections</p>
|
||||
<p v-text="current.active" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Min connections</p>
|
||||
<p v-text="current.min" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Max connections</p>
|
||||
<p v-if="current.max >= 0" v-text="current.max" />
|
||||
<p v-else>unlimited</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<datasource-chart v-if="chartData.length > 0" :data="chartData" />
|
||||
</div>
|
||||
</sba-panel>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import subscribing from '@/mixins/subscribing';
|
||||
import Instance from '@/services/instance';
|
||||
import {concatMap, timer} from '@/utils/rxjs';
|
||||
import moment from 'moment';
|
||||
import datasourceChart from './datasource-chart';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
instance: {
|
||||
type: Instance,
|
||||
required: true
|
||||
},
|
||||
dataSource: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
mixins: [subscribing],
|
||||
// eslint-disable-next-line vue/no-unused-components
|
||||
components: {datasourceChart},
|
||||
data: () => ({
|
||||
hasLoaded: false,
|
||||
error: null,
|
||||
current: null,
|
||||
chartData: [],
|
||||
}),
|
||||
methods: {
|
||||
async fetchMetrics() {
|
||||
const responseActive = this.instance.fetchMetric('data.source.active.connections', {name: this.dataSource});
|
||||
const responseMin = this.instance.fetchMetric('data.source.min.connections', {name: this.dataSource});
|
||||
const responseMax = this.instance.fetchMetric('data.source.max.connections', {name: this.dataSource});
|
||||
|
||||
return {
|
||||
active: (await responseActive).data.measurements[0].value,
|
||||
min: (await responseMin).data.measurements[0].value,
|
||||
max: (await responseMax).data.measurements[0].value
|
||||
};
|
||||
},
|
||||
createSubscription() {
|
||||
const vm = this;
|
||||
return timer(0, 2500)
|
||||
.pipe(concatMap(vm.fetchMetrics))
|
||||
.subscribe({
|
||||
next: data => {
|
||||
vm.hasLoaded = true;
|
||||
vm.current = data;
|
||||
vm.chartData.push({...data, timestamp: moment().valueOf()});
|
||||
},
|
||||
error: error => {
|
||||
vm.hasLoaded = true;
|
||||
console.warn(`Fetching datasource ${vm.dataSource} metrics failed:`, error);
|
||||
vm.error = error;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.datasource-current {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
</style>
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<details-datasource v-for="dataSource in dataSources" :key="dataSource"
|
||||
:instance="instance" :data-source="dataSource"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import subscribing from '@/mixins/subscribing';
|
||||
import Instance from '@/services/instance';
|
||||
import {concatMap, timer} from '@/utils/rxjs';
|
||||
import detailsDatasource from './details-datasource';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
instance: {
|
||||
type: Instance,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
mixins: [subscribing],
|
||||
// eslint-disable-next-line vue/no-unused-components
|
||||
components: {detailsDatasource},
|
||||
data: () => ({
|
||||
dataSources: [],
|
||||
}),
|
||||
methods: {
|
||||
async fetchDataSources() {
|
||||
const response = await this.instance.fetchMetric('data.source.active.connections');
|
||||
return response.data.availableTags.filter(tag => tag.tag === 'name')[0].values;
|
||||
},
|
||||
createSubscription() {
|
||||
const vm = this;
|
||||
return timer(0, 2500)
|
||||
.pipe(concatMap(this.fetchDataSources))
|
||||
.subscribe({
|
||||
next: names => {
|
||||
vm.dataSources = names
|
||||
},
|
||||
error: error => {
|
||||
console.warn('Fetching datasources failed:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,107 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<sba-panel title="Garbage Collection Pauses" v-if="hasLoaded">
|
||||
<div>
|
||||
<div v-if="error" class="message is-danger">
|
||||
<div class="message-body">
|
||||
<strong>
|
||||
<font-awesome-icon class="has-text-danger" icon="exclamation-triangle" />
|
||||
Fetching GC metrics failed.
|
||||
</strong>
|
||||
<p v-text="error.message" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="level" v-if="current">
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Count</p>
|
||||
<p v-text="current.count" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Total time spent</p>
|
||||
<p v-text="`${current.total_time.asSeconds().toFixed(4)}s`" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Max time spent</p>
|
||||
<p v-text="`${current.max.asSeconds().toFixed(4)}s`" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</sba-panel>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import subscribing from '@/mixins/subscribing';
|
||||
import Instance from '@/services/instance';
|
||||
import {concatMap, timer} from '@/utils/rxjs';
|
||||
import moment from 'moment';
|
||||
import {toMillis} from '../metrics/metric';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
instance: {
|
||||
type: Instance,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
mixins: [subscribing],
|
||||
data: () => ({
|
||||
hasLoaded: false,
|
||||
error: null,
|
||||
current: null,
|
||||
}),
|
||||
methods: {
|
||||
async fetchMetrics() {
|
||||
const response = await this.instance.fetchMetric('jvm.gc.pause');
|
||||
const measurements = response.data.measurements.reduce(
|
||||
(current, measurement) => ({
|
||||
...current,
|
||||
[measurement.statistic.toLowerCase()]: measurement.value
|
||||
}),
|
||||
{}
|
||||
);
|
||||
return {
|
||||
...measurements,
|
||||
total_time: moment.duration(toMillis(measurements.total_time, response.baseUnit)),
|
||||
max: moment.duration(toMillis(measurements.max, response.baseUnit)),
|
||||
};
|
||||
},
|
||||
createSubscription() {
|
||||
const vm = this;
|
||||
return timer(0, 2500)
|
||||
.pipe(concatMap(this.fetchMetrics))
|
||||
.subscribe({
|
||||
next: data => {
|
||||
vm.hasLoaded = true;
|
||||
vm.current = data;
|
||||
},
|
||||
error: error => {
|
||||
vm.hasLoaded = true;
|
||||
console.warn('Fetching GC metrics failed:', error);
|
||||
vm.error = error;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,72 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<sba-panel title="Health">
|
||||
<div>
|
||||
<div v-if="error" class="message is-warning">
|
||||
<div class="message-body">
|
||||
<strong>
|
||||
<font-awesome-icon class="has-text-warning" icon="exclamation-triangle" />
|
||||
Fetching live health status failed. This is the last known information.
|
||||
</strong>
|
||||
<p v-text="error.message" />
|
||||
</div>
|
||||
</div>
|
||||
<health-details name="Instance" :health="health" />
|
||||
</div>
|
||||
</sba-panel>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Instance from '@/services/instance';
|
||||
import healthDetails from './health-details';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
instance: {
|
||||
type: Instance,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line vue/no-unused-components
|
||||
components: {healthDetails},
|
||||
data: () => ({
|
||||
error: null,
|
||||
liveHealth: null,
|
||||
}),
|
||||
created() {
|
||||
this.fetchHealth();
|
||||
},
|
||||
computed: {
|
||||
health() {
|
||||
return this.liveHealth || this.instance.statusInfo;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async fetchHealth() {
|
||||
this.error = null;
|
||||
try {
|
||||
const res = await this.instance.fetchHealth();
|
||||
this.liveHealth = res.data;
|
||||
} catch (error) {
|
||||
console.warn('Fetching live health failed:', error);
|
||||
this.error = error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,95 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<sba-panel title="Info">
|
||||
<div>
|
||||
<div v-if="error" class="message is-warning">
|
||||
<div class="message-body">
|
||||
<strong>
|
||||
<font-awesome-icon class="has-text-warning" icon="exclamation-triangle" />
|
||||
Fetching live info failed. This is the last known information.
|
||||
</strong>
|
||||
<p v-text="error.message" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="content info">
|
||||
<table class="table" v-if="!isEmptyInfo">
|
||||
<tr v-for="(value, key) in info" :key="key">
|
||||
<td class="info__key" v-text="key" />
|
||||
<td>
|
||||
<sba-formatted-obj :value="value" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p v-else class="is-muted">No info provided.</p>
|
||||
</div>
|
||||
</div>
|
||||
</sba-panel>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Instance from '@/services/instance';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
instance: {
|
||||
type: Instance,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data: () => ({
|
||||
error: null,
|
||||
liveInfo: null
|
||||
}),
|
||||
computed: {
|
||||
info() {
|
||||
return this.liveInfo || this.instance.info
|
||||
},
|
||||
isEmptyInfo() {
|
||||
return Object.keys(this.info).length <= 0;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.fetchInfo();
|
||||
},
|
||||
methods: {
|
||||
async fetchInfo() {
|
||||
if (this.instance.hasEndpoint('info')) {
|
||||
try {
|
||||
this.error = null;
|
||||
const res = await this.instance.fetchInfo();
|
||||
this.liveInfo = res.data;
|
||||
} catch (error) {
|
||||
this.error = error;
|
||||
console.warn('Fetching info failed:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.info {
|
||||
overflow: auto;
|
||||
|
||||
&__key {
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<sba-panel :title="`Memory: ${name}`" v-if="hasLoaded">
|
||||
<div>
|
||||
<div v-if="error" class="message is-danger">
|
||||
<div class="message-body">
|
||||
<strong>
|
||||
<font-awesome-icon class="has-text-danger" icon="exclamation-triangle" />
|
||||
Fetching memory metrics failed.
|
||||
</strong>
|
||||
<p v-text="error.message" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="level memory-current" v-if="current">
|
||||
<div class="level-item has-text-centered" v-if="current.metaspace">
|
||||
<div>
|
||||
<p class="heading has-bullet has-bullet-primary">Metaspace</p>
|
||||
<p v-text="prettyBytes(current.metaspace)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading has-bullet has-bullet-info">Used</p>
|
||||
<p v-text="prettyBytes(current.used)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading has-bullet has-bullet-warning">Size</p>
|
||||
<p v-text="prettyBytes(current.committed)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered" v-if="current.max >= 0">
|
||||
<div>
|
||||
<p class="heading">Max</p>
|
||||
<p v-text="prettyBytes(current.max)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<mem-chart v-if="chartData.length > 0" :data="chartData" />
|
||||
</div>
|
||||
</sba-panel>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import subscribing from '@/mixins/subscribing';
|
||||
import Instance from '@/services/instance';
|
||||
import {concatMap, timer} from '@/utils/rxjs';
|
||||
import moment from 'moment';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import memChart from './mem-chart';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
instance: {
|
||||
type: Instance,
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
mixins: [subscribing],
|
||||
// eslint-disable-next-line vue/no-unused-components
|
||||
components: {memChart},
|
||||
data: () => ({
|
||||
hasLoaded: false,
|
||||
error: null,
|
||||
current: null,
|
||||
chartData: [],
|
||||
}),
|
||||
computed: {
|
||||
name() {
|
||||
switch (this.type) {
|
||||
case 'heap':
|
||||
return 'Heap';
|
||||
case 'nonheap':
|
||||
return 'Non heap';
|
||||
default:
|
||||
return this.type;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
prettyBytes,
|
||||
async fetchMetrics() {
|
||||
const responseMax = this.instance.fetchMetric('jvm.memory.max', {area: this.type});
|
||||
const responseUsed = this.instance.fetchMetric('jvm.memory.used', {area: this.type});
|
||||
const responeMetaspace = this.type === 'nonheap'
|
||||
? this.instance.fetchMetric('jvm.memory.used', {area: this.type, id: 'Metaspace'})
|
||||
: null;
|
||||
const responseCommitted = this.instance.fetchMetric('jvm.memory.committed', {area: this.type});
|
||||
return {
|
||||
max: (await responseMax).data.measurements[0].value,
|
||||
used: (await responseUsed).data.measurements[0].value,
|
||||
metaspace: responeMetaspace ? (await responeMetaspace).data.measurements[0].value : null,
|
||||
committed: (await responseCommitted).data.measurements[0].value
|
||||
};
|
||||
},
|
||||
createSubscription() {
|
||||
const vm = this;
|
||||
return timer(0, 2500)
|
||||
.pipe(concatMap(this.fetchMetrics))
|
||||
.subscribe({
|
||||
next: data => {
|
||||
vm.hasLoaded = true;
|
||||
vm.current = data;
|
||||
vm.chartData.push({...data, timestamp: moment().valueOf()});
|
||||
},
|
||||
error: error => {
|
||||
vm.hasLoaded = true;
|
||||
console.warn('Fetching memory metrics failed:', error);
|
||||
vm.error = error;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "~@/assets/css/utilities";
|
||||
|
||||
.memory-current {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
</style>
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<sba-panel title="Metadata">
|
||||
<div class="content metadata" v-if="metadata">
|
||||
<table class="table" v-if="!isEmptyMetadata">
|
||||
<tr v-for="(value, key) in metadata" :key="key">
|
||||
<td class="metadata__key" v-text="key" />
|
||||
<td>
|
||||
<sba-formatted-obj :value="value" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p v-else class="is-muted">No metadata provided.</p>
|
||||
</div>
|
||||
</sba-panel>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Instance from '@/services/instance';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
instance: {
|
||||
type: Instance,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
metadata() {
|
||||
return this.instance.registration.metadata;
|
||||
},
|
||||
isEmptyMetadata() {
|
||||
return Object.keys(this.metadata).length <= 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.metadata {
|
||||
overflow: auto;
|
||||
|
||||
&__key {
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<sba-panel title="Process" v-if="hasLoaded">
|
||||
<div>
|
||||
<div v-if="error" class="message is-danger">
|
||||
<div class="message-body">
|
||||
<strong>
|
||||
<font-awesome-icon class="has-text-danger" icon="exclamation-triangle" />
|
||||
Fetching process metrics failed.
|
||||
</strong>
|
||||
<p v-text="error.message" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="level">
|
||||
<div class="level-item has-text-centered" v-if="pid">
|
||||
<div>
|
||||
<p class="heading">PID</p>
|
||||
<p v-text="pid" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered" v-if="uptime">
|
||||
<div>
|
||||
<p class="heading">Uptime</p>
|
||||
<p>
|
||||
<process-uptime :value="toMillis(uptime.value, uptime.baseUnit)" />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered" v-if="processCpuLoad">
|
||||
<div>
|
||||
<p class="heading">Process CPU Usage</p>
|
||||
<p v-text="processCpuLoad.toFixed(2)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered" v-if="systemCpuLoad">
|
||||
<div>
|
||||
<p class="heading">System CPU Usage</p>
|
||||
<p v-text="systemCpuLoad.toFixed(2)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered" v-if="systemCpuCount">
|
||||
<div>
|
||||
<p class="heading">CPUs</p>
|
||||
<p v-text="systemCpuCount" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</sba-panel>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import subscribing from '@/mixins/subscribing';
|
||||
import Instance from '@/services/instance';
|
||||
import {concatMap, timer} from '@/utils/rxjs';
|
||||
import {toMillis} from '../metrics/metric';
|
||||
import processUptime from './process-uptime';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
instance: {
|
||||
type: Instance,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
mixins: [subscribing],
|
||||
// eslint-disable-next-line vue/no-unused-components
|
||||
components: {processUptime},
|
||||
data: () => ({
|
||||
hasLoaded: false,
|
||||
error: null,
|
||||
pid: null,
|
||||
uptime: {value: null, baseUnit: null},
|
||||
systemCpuLoad: null,
|
||||
processCpuLoad: null,
|
||||
systemCpuCount: null
|
||||
}),
|
||||
created() {
|
||||
this.fetchPid();
|
||||
this.fetchUptime();
|
||||
this.fetchCpuCount();
|
||||
},
|
||||
methods: {
|
||||
toMillis,
|
||||
async fetchUptime() {
|
||||
try {
|
||||
const response = await this.fetchMetric('process.uptime');
|
||||
this.uptime = {value: response.measurements[0].value, baseUnit: response.baseUnit};
|
||||
} catch (error) {
|
||||
this.error = error;
|
||||
console.warn('Fetching Uptime failed:', error);
|
||||
}
|
||||
this.hasLoaded = true;
|
||||
},
|
||||
async fetchPid() {
|
||||
if (this.instance.hasEndpoint('env')) {
|
||||
try {
|
||||
const response = await this.instance.fetchEnv('PID');
|
||||
this.pid = response.data.property.value;
|
||||
} catch (error) {
|
||||
console.warn('Fetching PID failed:', error);
|
||||
}
|
||||
this.hasLoaded = true;
|
||||
}
|
||||
},
|
||||
async fetchCpuCount() {
|
||||
try {
|
||||
this.systemCpuCount = (await this.fetchMetric('system.cpu.count')).measurements[0].value;
|
||||
} catch (error) {
|
||||
console.warn('Fetching Cpu Count failed:', error);
|
||||
}
|
||||
this.hasLoaded = true;
|
||||
},
|
||||
createSubscription() {
|
||||
const vm = this;
|
||||
return timer(0, 2500)
|
||||
.pipe(concatMap(this.fetchCpuLoadMetrics))
|
||||
.subscribe({
|
||||
next: data => {
|
||||
vm.processCpuLoad = data.processCpuLoad;
|
||||
vm.systemCpuLoad = data.systemCpuLoad;
|
||||
},
|
||||
error: error => {
|
||||
vm.hasLoaded = true;
|
||||
console.warn('Fetching CPU Usage metrics failed:', error);
|
||||
vm.error = error;
|
||||
}
|
||||
});
|
||||
},
|
||||
async fetchCpuLoadMetrics() {
|
||||
const fetchProcessCpuLoad = this.fetchMetric('process.cpu.usage');
|
||||
const fetchSystemCpuLoad = this.fetchMetric('system.cpu.usage');
|
||||
let processCpuLoad;
|
||||
let systemCpuLoad;
|
||||
try {
|
||||
processCpuLoad = (await fetchProcessCpuLoad).measurements[0].value
|
||||
} catch (error) {
|
||||
console.warn('Fetching Process CPU Load failed:', error);
|
||||
}
|
||||
try {
|
||||
systemCpuLoad = (await fetchSystemCpuLoad).measurements[0].value
|
||||
} catch (error) {
|
||||
console.warn('Fetching Sytem CPU Load failed:', error);
|
||||
}
|
||||
return {
|
||||
processCpuLoad,
|
||||
systemCpuLoad
|
||||
};
|
||||
},
|
||||
async fetchMetric(name) {
|
||||
const response = await this.instance.fetchMetric(name);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<sba-panel title="Threads" v-if="hasLoaded">
|
||||
<div>
|
||||
<div v-if="error" class="message is-danger">
|
||||
<div class="message-body">
|
||||
<strong>
|
||||
<font-awesome-icon class="has-text-danger" icon="exclamation-triangle" />
|
||||
Fetching threads metrics failed.
|
||||
</strong>
|
||||
<p v-text="error.message" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="level threads-current" v-if="current">
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading has-bullet has-bullet-warning">Live</p>
|
||||
<p v-text="current.live" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading has-bullet has-bullet-info">Daemon</p>
|
||||
<p v-text="current.daemon" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Peak Live</p>
|
||||
<p v-text="current.peak" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<threads-chart v-if="chartData.length > 0" :data="chartData" />
|
||||
</div>
|
||||
</sba-panel>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import subscribing from '@/mixins/subscribing';
|
||||
import Instance from '@/services/instance';
|
||||
import {concatMap, timer} from '@/utils/rxjs';
|
||||
import moment from 'moment';
|
||||
import threadsChart from './threads-chart';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
instance: {
|
||||
type: Instance,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
mixins: [subscribing],
|
||||
// eslint-disable-next-line vue/no-unused-components
|
||||
components: {threadsChart},
|
||||
data: () => ({
|
||||
hasLoaded: false,
|
||||
error: null,
|
||||
current: null,
|
||||
chartData: [],
|
||||
}),
|
||||
methods: {
|
||||
async fetchMetrics() {
|
||||
const responseLive = this.instance.fetchMetric('jvm.threads.live');
|
||||
const responsePeak = this.instance.fetchMetric('jvm.threads.peak');
|
||||
const responseDaemon = this.instance.fetchMetric('jvm.threads.daemon');
|
||||
|
||||
return {
|
||||
live: (await responseLive).data.measurements[0].value,
|
||||
peak: (await responsePeak).data.measurements[0].value,
|
||||
daemon: (await responseDaemon).data.measurements[0].value
|
||||
};
|
||||
},
|
||||
createSubscription() {
|
||||
const vm = this;
|
||||
return timer(0, 2500)
|
||||
.pipe(concatMap(this.fetchMetrics))
|
||||
.subscribe({
|
||||
next: data => {
|
||||
vm.hasLoaded = true;
|
||||
vm.current = data;
|
||||
vm.chartData.push({...data, timestamp: moment().valueOf()});
|
||||
},
|
||||
error: error => {
|
||||
vm.hasLoaded = true;
|
||||
console.warn('Fetching threads metrics failed:', error);
|
||||
vm.error = error;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.threads-current {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
</style>
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<table class="health-details table is-fullwidth">
|
||||
<tr>
|
||||
<th colspan="2">
|
||||
<span v-text="name" />
|
||||
<span class="health-details__status" :class="`health-details__status--${health.status}`"
|
||||
v-text="health.status"
|
||||
/>
|
||||
</th>
|
||||
</tr>
|
||||
|
||||
<tr v-if="details && details.length > 0">
|
||||
<td class="health-details__nested" colspan="2">
|
||||
<table class="health-details table is-fullwidth">
|
||||
<tr class="health-details__detail" v-for="detail in details" :key="detail.name">
|
||||
<td v-text="detail.name" />
|
||||
<td v-if="name === 'diskSpace'" v-text="prettyBytes(detail.value)" />
|
||||
<td class="is-breakable" v-else v-text="detail.value" />
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr v-for="child in childHealth" :key="child.name">
|
||||
<td class="health-details__nested" colspan="2">
|
||||
<health-details :name="child.name" :health="child.value" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
|
||||
const isChildHealth = (value) => {
|
||||
return value !== null && typeof value === 'object' && value.hasOwnProperty('status');
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'HealthDetails',
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
health: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
prettyBytes
|
||||
},
|
||||
computed: {
|
||||
details() {
|
||||
if (this.health.details) {
|
||||
return Object.entries(this.health.details)
|
||||
.filter(([, value]) => !isChildHealth(value))
|
||||
.map(([name, value]) => ({name, value}));
|
||||
}
|
||||
return [];
|
||||
},
|
||||
childHealth() {
|
||||
if (this.health.details) {
|
||||
return Object.entries(this.health.details)
|
||||
.filter(([, value]) => isChildHealth(value))
|
||||
.map(([name, value]) => ({name, value}));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "~@/assets/css/utilities";
|
||||
|
||||
td.health-details__nested {
|
||||
padding: 0 0 0 0.75em;
|
||||
}
|
||||
|
||||
td.health-details__nested {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.health-details__nested .table {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.health-details__status {
|
||||
float: right;
|
||||
|
||||
&--UP {
|
||||
color: $success;
|
||||
}
|
||||
|
||||
&--RESTRICTED {
|
||||
color: $warning;
|
||||
}
|
||||
|
||||
&--OUT_OF_SERVICE,
|
||||
&--DOWN {
|
||||
color: $danger;
|
||||
}
|
||||
|
||||
&--UNKNOWN,
|
||||
&--OFFLINE {
|
||||
color: $grey;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,198 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<section class="section">
|
||||
<div class="details-header">
|
||||
<h1 class="title" v-if="instance" v-text="instance.registration.name" />
|
||||
<h2 class="subtitle is-5" v-if="instance">Id: <span v-text="instance.id" /></h2>
|
||||
<sba-tags v-if="instance" :tags="instance.tags" />
|
||||
<div class="details-header__urls">
|
||||
<a class="button is-small " :href="instance.registration.serviceUrl">
|
||||
<font-awesome-icon icon="home" />
|
||||
<span v-text="instance.registration.serviceUrl" />
|
||||
</a>
|
||||
<a class="button is-small " :href="instance.registration.managementUrl">
|
||||
<font-awesome-icon icon="wrench" />
|
||||
<span v-text="instance.registration.managementUrl" />
|
||||
</a>
|
||||
<a class="button is-small " :href="instance.registration.healthUrl">
|
||||
<font-awesome-icon icon="heartbeat" />
|
||||
<span v-text="instance.registration.healthUrl" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div v-if="error" class="message is-danger">
|
||||
<div class="message-body">
|
||||
<strong>
|
||||
<font-awesome-icon class="has-text-danger" icon="exclamation-triangle" />
|
||||
Fetching metrics index failed.
|
||||
</strong>
|
||||
<p v-text="error.message" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="columns is-desktop">
|
||||
<div class="column is-half-desktop">
|
||||
<details-info v-if="hasInfo" :instance="instance" />
|
||||
<details-metadata v-if="hasMetadata" :instance="instance" />
|
||||
</div>
|
||||
<div class="column is-half-desktop">
|
||||
<details-health :instance="instance" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="columns is-desktop">
|
||||
<div class="column is-half-desktop">
|
||||
<details-process v-if="hasProcess" :instance="instance" />
|
||||
<details-gc v-if="hasGc" :instance="instance" />
|
||||
</div>
|
||||
<div class="column is-half-desktop">
|
||||
<details-threads v-if="hasThreads" :instance="instance" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="columns is-desktop">
|
||||
<div class="column is-half-desktop">
|
||||
<details-memory v-if="hasMemory" :instance="instance" type="heap" />
|
||||
</div>
|
||||
<div class="column is-half-desktop">
|
||||
<details-memory v-if="hasMemory" :instance="instance" type="nonheap" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="columns is-desktop">
|
||||
<div class="column is-half-desktop">
|
||||
<details-datasources v-if="hasDatasources" :instance="instance" />
|
||||
</div>
|
||||
<div class="column is-half-desktop">
|
||||
<details-caches v-if="hasCaches" :instance="instance" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import Instance from '@/services/instance';
|
||||
import detailsCaches from './details-caches';
|
||||
import detailsDatasources from './details-datasources';
|
||||
import detailsGc from './details-gc';
|
||||
import detailsHealth from './details-health';
|
||||
import detailsInfo from './details-info';
|
||||
import detailsMemory from './details-memory';
|
||||
import detailsMetadata from './details-metadata';
|
||||
import detailsProcess from './details-process';
|
||||
import detailsThreads from './details-threads';
|
||||
|
||||
export default {
|
||||
/* eslint-disable vue/no-unused-components */
|
||||
components: {
|
||||
detailsHealth,
|
||||
detailsInfo,
|
||||
detailsProcess,
|
||||
detailsThreads,
|
||||
detailsDatasources,
|
||||
detailsMemory,
|
||||
detailsGc,
|
||||
detailsCaches,
|
||||
detailsMetadata
|
||||
},
|
||||
props: {
|
||||
instance: {
|
||||
type: Instance,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data: () => ({
|
||||
hasLoaded: false,
|
||||
error: null,
|
||||
metrics: []
|
||||
}),
|
||||
computed: {
|
||||
hasCaches() {
|
||||
return this.hasMetric('cache.gets');
|
||||
},
|
||||
hasDatasources() {
|
||||
return this.hasMetric('data.source.active.connections');
|
||||
},
|
||||
hasGc() {
|
||||
return this.hasMetric('jvm.gc.pause');
|
||||
},
|
||||
hasInfo() {
|
||||
return this.instance.hasEndpoint('info');
|
||||
},
|
||||
hasMemory() {
|
||||
return this.hasMetric('jvm.memory.max');
|
||||
},
|
||||
hasProcess() {
|
||||
return this.hasMetric('process.uptime');
|
||||
},
|
||||
hasThreads() {
|
||||
return this.hasMetric('jvm.threads.live');
|
||||
},
|
||||
hasMetadata() {
|
||||
return this.instance.registration && this.instance.registration.metadata;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.fetchMetricIndex();
|
||||
},
|
||||
methods: {
|
||||
hasMetric(metric) {
|
||||
return this.metrics && this.metrics.includes(metric);
|
||||
},
|
||||
async fetchMetricIndex() {
|
||||
if (this.instance.hasEndpoint('metrics')) {
|
||||
this.error = null;
|
||||
try {
|
||||
const res = await this.instance.fetchMetrics();
|
||||
this.metrics = res.data.names;
|
||||
} catch (error) {
|
||||
console.warn('Fetching metric index failed:', error);
|
||||
this.error = error;
|
||||
}
|
||||
this.hasLoaded = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
install({viewRegistry}) {
|
||||
viewRegistry.addView({
|
||||
name: 'instances/details',
|
||||
parent: 'instances',
|
||||
path: 'details',
|
||||
component: this,
|
||||
label: 'Details',
|
||||
group: 'Insights',
|
||||
order: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
@import "~@/assets/css/utilities";
|
||||
|
||||
.details-header {
|
||||
margin-bottom: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
&__urls {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,170 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="mem-chart">
|
||||
<svg class="mem-chart__svg" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import d3 from '@/utils/d3';
|
||||
import moment from 'moment';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data: () => ({}),
|
||||
methods: {
|
||||
drawChart(_data) {
|
||||
const vm = this;
|
||||
const data = _data.length === 1 ? _data.concat([{..._data[0], timestamp: _data[0].timestamp + 1}]) : _data;
|
||||
|
||||
///setup x and y scale
|
||||
const extent = d3.extent(data, d => d.timestamp);
|
||||
const x = d3.scaleTime()
|
||||
.range([0, vm.width])
|
||||
.domain(extent);
|
||||
|
||||
const y = d3.scaleLinear()
|
||||
.range([vm.height, 0])
|
||||
.domain([0, d3.max(data, d => d.committed) * 1.05]);
|
||||
|
||||
//draw max
|
||||
const max = vm.areas.selectAll('.mem-chart__line--max')
|
||||
.data([data]);
|
||||
max.enter().append('path')
|
||||
.merge(max)
|
||||
.attr('class', 'mem-chart__line--max')
|
||||
.attr('d', d3.line()
|
||||
.x(d => x(d.timestamp))
|
||||
.y(d => y(d.max)));
|
||||
max.exit().remove();
|
||||
|
||||
//draw areas
|
||||
const committed = vm.areas.selectAll('.mem-chart__area--committed')
|
||||
.data([data]);
|
||||
committed.enter().append('path')
|
||||
.merge(committed)
|
||||
.attr('class', 'mem-chart__area--committed')
|
||||
.attr('d', d3.area()
|
||||
.x(d => x(d.timestamp))
|
||||
.y0(d => y(d.used))
|
||||
.y1(d => y(d.committed)));
|
||||
committed.exit().remove();
|
||||
|
||||
const used = vm.areas.selectAll('.mem-chart__area--used')
|
||||
.data([data]);
|
||||
used.enter().append('path')
|
||||
.merge(used)
|
||||
.attr('class', 'mem-chart__area--used')
|
||||
.attr('d', d3.area()
|
||||
.x(d => x(d.timestamp))
|
||||
.y0(d => y(d.metaspace || 0))
|
||||
.y1(d => y(d.used)));
|
||||
used.exit().remove();
|
||||
|
||||
const metaspace = vm.areas.selectAll('.mem-chart__area--metaspace')
|
||||
.data([data]);
|
||||
metaspace.enter().append('path')
|
||||
.merge(metaspace)
|
||||
.attr('class', 'mem-chart__area--metaspace')
|
||||
.attr('d', d3.area()
|
||||
.x(d => x(d.timestamp))
|
||||
.y0(y(0))
|
||||
.y1(d => y(d.metaspace)));
|
||||
metaspace.exit().remove();
|
||||
|
||||
//draw axis
|
||||
vm.xAxis.call(d3.axisBottom(x)
|
||||
.ticks(5)
|
||||
.tickFormat(d => moment(d).format('HH:mm:ss'))
|
||||
);
|
||||
|
||||
vm.yAxis.call(d3.axisLeft(y)
|
||||
.ticks(5)
|
||||
.tickFormat(prettyBytes)
|
||||
);
|
||||
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const margin = {
|
||||
top: 5,
|
||||
right: 5,
|
||||
bottom: 30,
|
||||
left: 50,
|
||||
};
|
||||
|
||||
this.width = this.$el.getBoundingClientRect().width - margin.left - margin.right;
|
||||
this.height = this.$el.getBoundingClientRect().height - margin.top - margin.bottom;
|
||||
|
||||
this.chartLayer = d3.select(this.$el.querySelector('.mem-chart__svg'))
|
||||
.append('g')
|
||||
.attr('transform', `translate(${margin.left},${margin.top})`);
|
||||
|
||||
this.xAxis = this.chartLayer.append('g')
|
||||
.attr('class', 'mem-chart__axis-x')
|
||||
.attr('transform', `translate(0,${this.height})`);
|
||||
|
||||
this.yAxis = this.chartLayer.append('g')
|
||||
.attr('class', 'mem-chart__axis-y')
|
||||
.attr('stroke', null);
|
||||
|
||||
this.areas = this.chartLayer.append('g');
|
||||
|
||||
this.drawChart(this.data);
|
||||
},
|
||||
watch: {
|
||||
data: 'drawChart'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "~@/assets/css/utilities";
|
||||
|
||||
.mem-chart {
|
||||
&__svg {
|
||||
height: 159px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__area {
|
||||
&--committed {
|
||||
fill: $warning;
|
||||
opacity: 0.8;
|
||||
}
|
||||
&--used {
|
||||
fill: $info;
|
||||
opacity: 0.8;
|
||||
}
|
||||
&--metaspace {
|
||||
fill: $primary;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
&__line--max {
|
||||
stroke: $grey;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2014-2018 the original author or authors.
|
||||
*
|
||||
* 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 subscribing from '@/mixins/subscribing';
|
||||
import {timer} from '@/utils/rxjs';
|
||||
import moment from 'moment';
|
||||
|
||||
export default {
|
||||
props: ['value'],
|
||||
mixins: [subscribing],
|
||||
data: () => ({
|
||||
startTs: null,
|
||||
offset: null
|
||||
}),
|
||||
render() {
|
||||
return this._v(this.clock);
|
||||
},
|
||||
computed: {
|
||||
clock() {
|
||||
if (!this.value) {
|
||||
return null;
|
||||
}
|
||||
const duration = moment.duration(this.value + this.offset);
|
||||
return `${Math.floor(duration.asDays())}d ${duration.hours()}h ${duration.minutes()}m ${duration.seconds()}s`;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value: 'subscribe'
|
||||
},
|
||||
methods: {
|
||||
createSubscription() {
|
||||
if (this.value) {
|
||||
const vm = this;
|
||||
vm.startTs = moment();
|
||||
vm.offset = 0;
|
||||
return timer(0, 1000).subscribe({
|
||||
next: () => {
|
||||
vm.offset = moment().valueOf() - vm.startTs.valueOf();
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="threads-chart">
|
||||
<svg class="threads-chart__svg" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import d3 from '@/utils/d3';
|
||||
import moment from 'moment';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
drawChart(_data) {
|
||||
const vm = this;
|
||||
const data = _data.length === 1 ? _data.concat([{..._data[0], timestamp: _data[0].timestamp + 1}]) : _data;
|
||||
|
||||
///setup x and y scale
|
||||
const extent = d3.extent(data, d => d.timestamp);
|
||||
const x = d3.scaleTime()
|
||||
.range([0, vm.width])
|
||||
.domain(extent);
|
||||
|
||||
const y = d3.scaleLinear()
|
||||
.range([vm.height, 0])
|
||||
.domain([0, d3.max(data, d => d.live) * 1.05]);
|
||||
|
||||
//draw max
|
||||
const live = vm.areas.selectAll('.threads-chart__area--live')
|
||||
.data([data]);
|
||||
live.enter().append('path')
|
||||
.merge(live)
|
||||
.attr('class', 'threads-chart__area--live')
|
||||
.attr('d', d3.area()
|
||||
.x(d => x(d.timestamp))
|
||||
.y0(d => y(d.daemon))
|
||||
.y1(d => y(d.live)));
|
||||
live.exit().remove();
|
||||
|
||||
//draw areas
|
||||
const daemon = vm.areas.selectAll('.threads-chart__area--daemon')
|
||||
.data([data]);
|
||||
daemon.enter().append('path')
|
||||
.merge(daemon)
|
||||
.attr('class', 'threads-chart__area--daemon')
|
||||
.attr('d', d3.area()
|
||||
.x(d => x(d.timestamp))
|
||||
.y0(y(0))
|
||||
.y1(d => y(d.daemon)));
|
||||
daemon.exit().remove();
|
||||
|
||||
//draw axis
|
||||
vm.xAxis.call(d3.axisBottom(x)
|
||||
.ticks(5)
|
||||
.tickFormat(d => moment(d).format('HH:mm:ss'))
|
||||
);
|
||||
|
||||
vm.yAxis.call(d3.axisLeft(y)
|
||||
.ticks(5)
|
||||
);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const margin = {
|
||||
top: 5,
|
||||
right: 5,
|
||||
bottom: 30,
|
||||
left: 50,
|
||||
};
|
||||
|
||||
this.width = this.$el.getBoundingClientRect().width - margin.left - margin.right;
|
||||
this.height = this.$el.getBoundingClientRect().height - margin.top - margin.bottom;
|
||||
|
||||
this.chartLayer = d3.select(this.$el.querySelector('.threads-chart__svg'))
|
||||
.append('g')
|
||||
.attr('transform', `translate(${margin.left},${margin.top})`);
|
||||
|
||||
this.xAxis = this.chartLayer.append('g')
|
||||
.attr('class', 'threads-chart__axis-x')
|
||||
.attr('transform', `translate(0,${this.height})`);
|
||||
|
||||
this.yAxis = this.chartLayer.append('g')
|
||||
.attr('class', 'threads-chart__axis-y')
|
||||
.attr('stroke', null);
|
||||
|
||||
this.areas = this.chartLayer.append('g');
|
||||
|
||||
this.drawChart(this.data);
|
||||
},
|
||||
watch: {
|
||||
data: 'drawChart'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "~@/assets/css/utilities";
|
||||
|
||||
.threads-chart {
|
||||
&__svg {
|
||||
height: 159px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__area {
|
||||
&--live {
|
||||
fill: $warning;
|
||||
opacity: 0.8;
|
||||
}
|
||||
&--daemon {
|
||||
fill: $info;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
+260
@@ -0,0 +1,260 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="box">
|
||||
<h1 class="is-size-5">Environment Manager</h1>
|
||||
<datalist id="allPropertyNames">
|
||||
<option v-for="name in allPropertyNames" :key="name" v-text="name" />
|
||||
</datalist>
|
||||
<div class="field is-horizontal" v-for="(prop, index) in managedProperties" :key="`managed-${index}`">
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input class="input" type="text" placeholder="Property name" list="allPropertyNames"
|
||||
v-model="prop.name" @input="handlePropertyNameChange(prop, index)"
|
||||
>
|
||||
</div>
|
||||
<p class="help is-danger" v-text="prop.validation" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="control has-icons-right" :class="{'is-loading' : prop.status === 'executing'}">
|
||||
<input class="input" type="text" placeholder="Value" v-model="prop.input"
|
||||
@input="prop.status = null"
|
||||
>
|
||||
<span class="icon is-right has-text-success" v-if="prop.status === 'completed'">
|
||||
<font-awesome-icon icon="check" />
|
||||
</span>
|
||||
<span class="icon is-right has-text-warning" v-else-if="prop.status === 'failed'">
|
||||
<font-awesome-icon icon="exclamation-triangle" />
|
||||
</span>
|
||||
<span class="icon is-right" v-else-if="prop.input !== prop.value">
|
||||
<font-awesome-icon icon="pencil-alt" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-body" v-if="instance.hasEndpoint('refresh')">
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<sba-confirm-button class="button is-light"
|
||||
:class="{'is-loading' : refreshStatus === 'executing', 'is-danger' : refreshStatus === 'failed', 'is-info' : refreshStatus === 'completed'}"
|
||||
:disabled="refreshStatus === 'executing'"
|
||||
@click="refreshContext"
|
||||
>
|
||||
<span v-if="refreshStatus === 'completed'">Context refreshed</span>
|
||||
<span v-else-if="refreshStatus === 'failed'">Failed</span>
|
||||
<span v-else>Refresh Context</span>
|
||||
</sba-confirm-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field is-grouped is-grouped-right">
|
||||
<div class="control">
|
||||
<button class="button is-light"
|
||||
:class="{'is-loading' : resetStatus === 'executing', 'is-danger' : resetStatus === 'failed', 'is-success' : resetStatus === 'completed'}"
|
||||
:disabled="!hasManagedProperty || resetStatus === 'executing'"
|
||||
@click="resetEnvironment"
|
||||
>
|
||||
<span v-if="resetStatus === 'completed'">Resetted</span>
|
||||
<span v-else-if="resetStatus === 'failed'">Failed</span>
|
||||
<span v-else>Reset</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button is-primary"
|
||||
:class="{'is-loading' : updateStatus === 'executing', 'is-danger' : updateStatus === 'failed', 'is-success' : updateStatus === 'completed'}"
|
||||
:disabled="hasErrorProperty || !hasChangedProperty || updateStatus === 'executing'"
|
||||
@click="updateEnvironment"
|
||||
>
|
||||
<span v-if="updateStatus === 'completed'">Updated</span>
|
||||
<span v-else-if="updateStatus === 'failed'">Failed</span>
|
||||
<span v-else>Update</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Instance from '@/services/instance';
|
||||
import {concatMap, filter, from, listen} from '@/utils/rxjs';
|
||||
import debounce from 'lodash/debounce';
|
||||
import uniq from 'lodash/uniq';
|
||||
|
||||
|
||||
export default {
|
||||
props: {
|
||||
instance: {
|
||||
type: Instance,
|
||||
required: true
|
||||
},
|
||||
propertySources: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data: () => ({
|
||||
error: null,
|
||||
refreshStatus: null,
|
||||
resetStatus: null,
|
||||
updateStatus: null,
|
||||
managedProperties: [{
|
||||
name: null,
|
||||
input: null,
|
||||
value: null,
|
||||
status: null,
|
||||
validation: null
|
||||
}]
|
||||
}),
|
||||
computed: {
|
||||
allPropertyNames() {
|
||||
return uniq(this.propertySources.map(ps => Object.keys(ps.properties))
|
||||
.reduce((result, names) => result.concat(names))
|
||||
.sort());
|
||||
},
|
||||
managerPropertySource() {
|
||||
return this.propertySources.find(ps => ps.name === 'manager') || {name: 'manager', properties: {}};
|
||||
},
|
||||
hasManagedProperty() {
|
||||
return this.managedProperties.findIndex(property => !!property.name) >= 0;
|
||||
},
|
||||
hasChangedProperty() {
|
||||
return this.managedProperties.findIndex(property => property.input !== property.value) >= 0;
|
||||
},
|
||||
hasErrorProperty() {
|
||||
return this.managedProperties.findIndex(property => property.validation !== null) >= 0;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handlePropertyNameChange: debounce(function (prop, idx) {
|
||||
if (prop.name && idx === this.managedProperties.length - 1) {
|
||||
this.managedProperties.push({
|
||||
name: null,
|
||||
input: null,
|
||||
value: null,
|
||||
status: null,
|
||||
validation: null
|
||||
});
|
||||
}
|
||||
}, 250),
|
||||
refreshContext() {
|
||||
const vm = this;
|
||||
from(vm.instance.refreshContext())
|
||||
.pipe(listen(status => vm.refreshStatus = status))
|
||||
.subscribe({
|
||||
complete: () => {
|
||||
setTimeout(() => vm.refreshStatus = null, 2500);
|
||||
return vm.$emit('reset');
|
||||
},
|
||||
error: () => vm.$emit('reset')
|
||||
});
|
||||
},
|
||||
updateEnvironment() {
|
||||
const vm = this;
|
||||
from(vm.managedProperties)
|
||||
.pipe(
|
||||
filter(property => !!property.name && property.input !== property.value),
|
||||
listen(status => vm.updateStatus = status),
|
||||
concatMap(
|
||||
property => from(vm.instance.setEnv(property.name, property.input))
|
||||
.pipe(listen(status => property.status = status))
|
||||
)
|
||||
)
|
||||
.subscribe({
|
||||
complete: () => {
|
||||
setTimeout(() => vm.updateStatus = null, 2500);
|
||||
return vm.$emit('update');
|
||||
},
|
||||
error: () => vm.$emit('update')
|
||||
});
|
||||
},
|
||||
resetEnvironment() {
|
||||
const vm = this;
|
||||
from(vm.instance.resetEnv())
|
||||
.pipe(listen(status => vm.resetStatus = status))
|
||||
.subscribe({
|
||||
complete: () => {
|
||||
vm.managedProperties = [{
|
||||
name: null,
|
||||
input: null,
|
||||
value: null,
|
||||
status: null,
|
||||
validation: null
|
||||
}];
|
||||
setTimeout(() => vm.resetStatus = null, 2500);
|
||||
return vm.$emit('refresh');
|
||||
},
|
||||
error: () => vm.$emit('refresh')
|
||||
});
|
||||
},
|
||||
updateManagedProperties(manager) {
|
||||
Object.entries(manager.properties).forEach(([name, property]) => {
|
||||
const managedProperty = this.managedProperties.find(property => property.name === name);
|
||||
if (managedProperty) {
|
||||
managedProperty.value = property.value
|
||||
} else {
|
||||
const idx = this.managedProperties.length - 1;
|
||||
this.managedProperties.splice(idx, 0, {
|
||||
name,
|
||||
input: property.value,
|
||||
value: property.value,
|
||||
status: null,
|
||||
validation: null
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
managerPropertySource: {
|
||||
handler: 'updateManagedProperties',
|
||||
immediate: true
|
||||
},
|
||||
managedProperties: {
|
||||
deep: true,
|
||||
handler() {
|
||||
const counts = this.managedProperties.reduce(
|
||||
(acc, v) => {
|
||||
if (v.name) {
|
||||
acc[v.name] = (acc[v.name] || 0) + 1;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
this.managedProperties.forEach(property => {
|
||||
if (!property.name) {
|
||||
if (property.input) {
|
||||
property.validation = 'Property name is required';
|
||||
}
|
||||
return;
|
||||
}
|
||||
const count = counts[property.name] || 0;
|
||||
if (count > 1) {
|
||||
property.validation = 'Property name must be unique';
|
||||
return;
|
||||
}
|
||||
property.validation = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,151 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<section class="section" :class="{ 'is-loading' : !hasLoaded }">
|
||||
<div v-if="error" class="message is-danger">
|
||||
<div class="message-body">
|
||||
<strong>
|
||||
<font-awesome-icon class="has-text-danger" icon="exclamation-triangle" />
|
||||
Fetching environment failed.
|
||||
</strong>
|
||||
<p v-text="error.message" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-grouped is-grouped-multiline" v-if="env && env.activeProfiles.length > 0">
|
||||
<div class="control" v-for="profile in env.activeProfiles" :key="profile">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-medium is-primary">Profile</span>
|
||||
<span class="tag is-medium" v-text="profile" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<sba-env-manager v-if="env && hasEnvManagerSupport"
|
||||
:instance="instance" :property-sources="env.propertySources"
|
||||
@refresh="fetchEnv()" @update="fetchEnv" @reset="fetchEnv()"
|
||||
/>
|
||||
<div class="field has-addons" v-if="env">
|
||||
<p class="control is-expanded">
|
||||
<input class="input" type="search" placeholder="name / value filter" v-model="filter">
|
||||
</p>
|
||||
</div>
|
||||
<sba-panel :header-sticks-below="['#navigation']"
|
||||
v-for="propertySource in propertySources" :key="propertySource.name"
|
||||
:title="propertySource.name"
|
||||
>
|
||||
<table class="table is-fullwidth"
|
||||
v-if="Object.keys(propertySource.properties).length > 0"
|
||||
>
|
||||
<tr v-for="(value, name) in propertySource.properties" :key="`${propertySource.name}-${name}`">
|
||||
<td>
|
||||
<span v-text="name" /><br>
|
||||
<small class="is-muted" v-if="value.origin" v-text="value.origin" />
|
||||
</td>
|
||||
<td class="is-breakable" v-text="value.value" />
|
||||
</tr>
|
||||
</table>
|
||||
<p class="is-muted" v-else>No properties set</p>
|
||||
</sba-panel>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Instance from '@/services/instance';
|
||||
import pickBy from 'lodash/pickBy';
|
||||
import sbaEnvManager from './env-manager';
|
||||
|
||||
const filterProperty = (needle) => (property, name) => {
|
||||
return name.toString().toLowerCase().includes(needle) || property.value.toString().toLowerCase().includes(needle);
|
||||
};
|
||||
const filterProperties = (needle, properties) => pickBy(properties, filterProperty(needle));
|
||||
const filterPropertySource = (needle) => (propertySource) => {
|
||||
if (!propertySource || !propertySource.properties) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...propertySource,
|
||||
properties: filterProperties(needle, propertySource.properties)
|
||||
};
|
||||
};
|
||||
|
||||
export default {
|
||||
props: {
|
||||
instance: {
|
||||
type: Instance,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line vue/no-unused-components
|
||||
components: {sbaEnvManager},
|
||||
data: () => ({
|
||||
hasLoaded: false,
|
||||
error: null,
|
||||
env: null,
|
||||
filter: null,
|
||||
hasEnvManagerSupport: false
|
||||
}),
|
||||
computed: {
|
||||
propertySources() {
|
||||
if (!this.env) {
|
||||
return [];
|
||||
}
|
||||
if (!this.filter) {
|
||||
return this.env.propertySources;
|
||||
}
|
||||
return this.env.propertySources
|
||||
.map(filterPropertySource(this.filter.toLowerCase()))
|
||||
.filter(ps => ps && Object.keys(ps.properties).length > 0);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.fetchEnv();
|
||||
this.determineEnvManagerSupport();
|
||||
},
|
||||
methods: {
|
||||
async fetchEnv() {
|
||||
this.error = null;
|
||||
try {
|
||||
const res = await this.instance.fetchEnv();
|
||||
this.env = res.data;
|
||||
} catch (error) {
|
||||
console.warn('Fetching environment failed:', error);
|
||||
this.error = error;
|
||||
}
|
||||
this.hasLoaded = true;
|
||||
},
|
||||
async determineEnvManagerSupport() {
|
||||
try {
|
||||
this.hasEnvManagerSupport = await this.instance.hasEnvManagerSupport();
|
||||
} catch (error) {
|
||||
console.warn('Determine env manager support failed:', error);
|
||||
this.hasEnvManagerSupport = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
install({viewRegistry}) {
|
||||
viewRegistry.addView({
|
||||
name: 'instances/env',
|
||||
parent: 'instances',
|
||||
path: 'env',
|
||||
component: this,
|
||||
label: 'Environment',
|
||||
group: 'Insights',
|
||||
order: 100,
|
||||
isEnabled: ({instance}) => instance.hasEndpoint('env')
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,140 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<section class="section" :class="{ 'is-loading' : !hasLoaded }">
|
||||
<template v-if="hasLoaded">
|
||||
<div v-if="error" class="message is-danger">
|
||||
<div class="message-body">
|
||||
<strong>
|
||||
<font-awesome-icon class="has-text-danger" icon="exclamation-triangle" />
|
||||
Fetching Flyway reports failed.
|
||||
</strong>
|
||||
<p v-text="error.message" />
|
||||
</div>
|
||||
</div>
|
||||
<template v-for="(context, ctxName) in contexts">
|
||||
<h3 class="title" v-text="ctxName" :key="ctxName" />
|
||||
<sba-panel v-for="(report, name) in context.flywayBeans" :key="`${ctxName}-${name}`" :title="name"
|
||||
:header-sticks-below="['#navigation']"
|
||||
class="migration"
|
||||
>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>Checksum</th>
|
||||
<th>Version</th>
|
||||
<th>Description</th>
|
||||
<th>Script</th>
|
||||
<th>State</th>
|
||||
<th>Installed by</th>
|
||||
<th>Installed on</th>
|
||||
<th>Installed rank</th>
|
||||
<th>Execution Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="migration in report.migrations" :key="migration.checksum">
|
||||
<td v-text="migration.type" />
|
||||
<td v-text="migration.checksum" />
|
||||
<td v-text="migration.version" />
|
||||
<td v-text="migration.description" />
|
||||
<td v-text="migration.script" />
|
||||
<td><span v-text="migration.state" class="tag"
|
||||
:class="stateClass(migration.state)"
|
||||
/></td>
|
||||
<td v-text="migration.installedBy" />
|
||||
<td v-text="migration.installedOn" />
|
||||
<td v-text="migration.installedRank" />
|
||||
<td v-text="`${migration.executionTime}ms`" />
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</sba-panel>
|
||||
</template>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Instance from '@/services/instance';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
instance: {
|
||||
type: Instance,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data: () => ({
|
||||
hasLoaded: false,
|
||||
error: null,
|
||||
contexts: null
|
||||
}),
|
||||
computed: {},
|
||||
created() {
|
||||
this.fetchFlyway();
|
||||
},
|
||||
methods: {
|
||||
async fetchFlyway() {
|
||||
this.error = null;
|
||||
try {
|
||||
const res = await this.instance.fetchFlyway();
|
||||
this.contexts = res.data.contexts;
|
||||
} catch (error) {
|
||||
console.warn('Fetching flyway reports failed:', error);
|
||||
this.error = error;
|
||||
}
|
||||
this.hasLoaded = true;
|
||||
},
|
||||
stateClass(state) {
|
||||
switch (state) {
|
||||
case 'BASELINE' :
|
||||
case 'MISSING_SUCCESS' :
|
||||
case 'SUCCESS' :
|
||||
case 'OUT_OF_ORDER' :
|
||||
case 'FUTURE_SUCCESS' :
|
||||
return 'is-success';
|
||||
case 'PENDING':
|
||||
case 'ABOVE_TARGET':
|
||||
case 'PREINIT':
|
||||
case 'BELOW_BASELINE':
|
||||
case 'IGNORED':
|
||||
return 'is-warning';
|
||||
case 'MISSING_FAILED':
|
||||
case 'FAILED':
|
||||
case 'FUTURE_FAILED':
|
||||
return 'is-danger';
|
||||
default:
|
||||
return 'is-light';
|
||||
}
|
||||
}
|
||||
},
|
||||
install({viewRegistry}) {
|
||||
viewRegistry.addView({
|
||||
name: 'instances/flyway',
|
||||
parent: 'instances',
|
||||
path: 'flyway',
|
||||
component: this,
|
||||
label: 'Flyway',
|
||||
group: 'Data',
|
||||
order: 900,
|
||||
isEnabled: ({instance}) => instance.hasEndpoint('flyway')
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,72 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<section class="section heapdump">
|
||||
<div>
|
||||
<div class="message is-warning">
|
||||
<div class="message-body">
|
||||
A heap dump may contain <strong>sensitive data</strong>. Please handle with care.
|
||||
</div>
|
||||
</div>
|
||||
<div class="message is-warning">
|
||||
<div class="message-body">
|
||||
Dumping the heap may be expensive in terms of cpu and disk space.
|
||||
</div>
|
||||
</div>
|
||||
<a class="button is-primary" :href="`instances/${instance.id}/actuator/heapdump`" target="_blank">
|
||||
<font-awesome-icon icon="download" /> Download Heap Dump
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Instance from '@/services/instance';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
instance: {
|
||||
type: Instance,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
install({viewRegistry}) {
|
||||
viewRegistry.addView({
|
||||
name: 'instances/heapdump',
|
||||
parent: 'instances',
|
||||
path: 'heapdump',
|
||||
component: this,
|
||||
label: 'Heap Dump',
|
||||
group: 'JVM',
|
||||
order: 800,
|
||||
isEnabled: ({instance}) => instance.hasEndpoint('heapdump')
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.heapdump {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
|
||||
& > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,249 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<section class="section" :class="{ 'is-loading' : !hasLoaded}">
|
||||
<template v-if="hasLoaded">
|
||||
<div v-if="error" class="message is-danger">
|
||||
<div class="message-body">
|
||||
<strong>
|
||||
<font-awesome-icon class="has-text-danger" icon="exclamation-triangle" />
|
||||
Fetching traces failed.
|
||||
</strong>
|
||||
<p v-text="error.message" />
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="traces">
|
||||
<div class="field-body">
|
||||
<div class="field has-addons">
|
||||
<p class="control is-expanded">
|
||||
<input class="input" type="search" placeholder="path filter" v-model="filter">
|
||||
</p>
|
||||
<p class="control">
|
||||
<span class="button is-static">
|
||||
<span v-text="filteredTraces.length" />
|
||||
/
|
||||
<span v-text="traces.length" />
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field is-narrow">
|
||||
<div class="control">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" v-model="showSuccess">
|
||||
success
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-narrow">
|
||||
<div class="control">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" v-model="showClientErrors">
|
||||
client errors
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-narrow">
|
||||
<div class="control">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" v-model="showServerErrors">
|
||||
server errors
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-narrow" v-if="actuatorPath">
|
||||
<div class="control">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" v-model="excludeActuator">
|
||||
exclude <span v-text="actuatorPath" />/**
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<sba-traces-chart :traces="filteredTraces" @selected="(d) => selection = d" />
|
||||
<sba-traces-list :traces="selectedTraces" />
|
||||
</template>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import subscribing from '@/mixins/subscribing';
|
||||
import Instance from '@/services/instance';
|
||||
import {concatMap, timer} from '@/utils/rxjs';
|
||||
import moment from 'moment';
|
||||
import sbaTracesChart from './traces-chart';
|
||||
import sbaTracesList from './traces-list';
|
||||
|
||||
const addToFilter = (oldFilter, addedFilter) =>
|
||||
!oldFilter
|
||||
? addedFilter
|
||||
: (val, key) => oldFilter(val, key) && addedFilter(val, key);
|
||||
|
||||
class Trace {
|
||||
constructor({timestamp, ...trace}) {
|
||||
Object.assign(this, trace);
|
||||
this.timestamp = moment(timestamp);
|
||||
}
|
||||
|
||||
get key() {
|
||||
return `${this.timestamp}-${this.request.method}-${this.request.uri}`;
|
||||
}
|
||||
|
||||
get contentLength() {
|
||||
const contentLength = this.response.headers['Content-Length'] && this.response.headers['Content-Length'][0];
|
||||
if (contentLength && /^\d+$/.test(contentLength)) {
|
||||
return parseInt(contentLength);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
get contentType() {
|
||||
const contentType = this.response.headers['Content-Type'] && this.response.headers['Content-Type'][0];
|
||||
if (contentType) {
|
||||
const idx = contentType.indexOf(';');
|
||||
return idx >= 0 ? contentType.substring(0, idx) : contentType;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
compareTo(other) {
|
||||
return this.timestamp - other.timestamp;
|
||||
}
|
||||
|
||||
isSuccess() {
|
||||
return this.response.status <= 399
|
||||
}
|
||||
|
||||
isClientError() {
|
||||
return this.response.status >= 400 && this.response.status <= 499
|
||||
}
|
||||
|
||||
isServerError() {
|
||||
return this.response.status >= 500 && this.response.status <= 599
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
props: {
|
||||
instance: {
|
||||
type: Instance,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
mixins: [subscribing],
|
||||
// eslint-disable-next-line vue/no-unused-components
|
||||
components: {sbaTracesList, sbaTracesChart},
|
||||
data: () => ({
|
||||
hasLoaded: false,
|
||||
error: null,
|
||||
traces: null,
|
||||
filter: null,
|
||||
excludeActuator: true,
|
||||
showSuccess: true,
|
||||
showClientErrors: true,
|
||||
showServerErrors: true,
|
||||
selection: null
|
||||
}),
|
||||
computed: {
|
||||
actuatorPath() {
|
||||
if (this.instance.registration.managementUrl.includes(this.instance.registration.serviceUrl)) {
|
||||
const appendix = this.instance.registration.managementUrl.substring(this.instance.registration.serviceUrl.length);
|
||||
if (appendix.length > 0) {
|
||||
return appendix.startsWith('/') ? appendix : `/${appendix}`;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
filteredTraces() {
|
||||
const filterFn = this.getFilterFn();
|
||||
return filterFn ? this.traces.filter(filterFn) : this.traces;
|
||||
},
|
||||
selectedTraces() {
|
||||
if (this.selection === null) {
|
||||
return this.filteredTraces;
|
||||
}
|
||||
const [start, end] = this.selection;
|
||||
return this.filteredTraces.filter(trace => !trace.timestamp.isBefore(start) && !trace.timestamp.isAfter(end));
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async fetchHttptrace() {
|
||||
const response = await this.instance.fetchHttptrace();
|
||||
const traces = response.data.traces.map(trace => new Trace(trace)).filter(
|
||||
trace => trace.timestamp.isAfter(this.lastTimestamp)
|
||||
);
|
||||
traces.sort((a, b) => -1 * a.compareTo(b));
|
||||
if (traces.length > 0) {
|
||||
this.lastTimestamp = traces[0].timestamp;
|
||||
}
|
||||
return traces;
|
||||
},
|
||||
createSubscription() {
|
||||
const vm = this;
|
||||
vm.lastTimestamp = moment(0);
|
||||
vm.error = null;
|
||||
return timer(0, 5000)
|
||||
.pipe(concatMap(vm.fetchHttptrace))
|
||||
.subscribe({
|
||||
next: traces => {
|
||||
vm.hasLoaded = true;
|
||||
vm.traces = vm.traces ? traces.concat(vm.traces) : traces;
|
||||
},
|
||||
error: error => {
|
||||
vm.hasLoaded = true;
|
||||
console.warn('Fetching traces failed:', error);
|
||||
vm.error = error;
|
||||
}
|
||||
});
|
||||
},
|
||||
getFilterFn() {
|
||||
let filterFn = null;
|
||||
if (this.actuatorPath !== null && this.excludeActuator) {
|
||||
filterFn = addToFilter(filterFn, (trace) => !trace.request.uri.includes(this.actuatorPath));
|
||||
}
|
||||
if (this.filter) {
|
||||
const normalizedFilter = this.filter.toLowerCase();
|
||||
filterFn = addToFilter(filterFn, (trace) => trace.request.uri.toLowerCase().includes(normalizedFilter));
|
||||
}
|
||||
if (!this.showSuccess) {
|
||||
filterFn = addToFilter(filterFn, (trace) => !trace.isSuccess());
|
||||
}
|
||||
if (!this.showClientErrors) {
|
||||
filterFn = addToFilter(filterFn, (trace) => !trace.isClientError());
|
||||
}
|
||||
if (!this.showServerErrors) {
|
||||
filterFn = addToFilter(filterFn, (trace) => !trace.isServerError());
|
||||
}
|
||||
return filterFn;
|
||||
}
|
||||
},
|
||||
install({viewRegistry}) {
|
||||
viewRegistry.addView({
|
||||
name: 'instances/httptrace',
|
||||
parent: 'instances',
|
||||
path: 'httptrace',
|
||||
component: this,
|
||||
label: 'Http Traces',
|
||||
group: 'Web',
|
||||
order: 500,
|
||||
isEnabled: ({instance}) => instance.hasEndpoint('httptrace')
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
+364
@@ -0,0 +1,364 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="trace-chart">
|
||||
<div class="trace-chart__tooltip"
|
||||
v-if="tooltipSelection "
|
||||
:class="`trace-chart__tooltip--${this.x(tooltipSelection[0]) > this.width / 2 ? 'left' : 'right'}`"
|
||||
>
|
||||
<table class="is-narrow is-size-7">
|
||||
<tr>
|
||||
<th>total requests</th>
|
||||
<td v-text="tooltipContent.totalCount" />
|
||||
</tr>
|
||||
<tr>
|
||||
<th>successful</th>
|
||||
<td v-text="tooltipContent.totalSuccess" />
|
||||
</tr>
|
||||
<tr>
|
||||
<th>status 4xx</th>
|
||||
<td v-text="tooltipContent.totalClientErrors" />
|
||||
</tr>
|
||||
<tr>
|
||||
<th>status 5xx</th>
|
||||
<td v-text="tooltipContent.totalServerErrors" />
|
||||
</tr>
|
||||
<tr>
|
||||
<th>max duration</th>
|
||||
<td v-text="`${tooltipContent.maxTime}ms`" />
|
||||
</tr>
|
||||
<tr>
|
||||
<th>ø duration</th>
|
||||
<td v-text="`${tooltipContent.avgTime}ms`" />
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<svg class="trace-chart__svg" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//see https://github.com/d3/d3/issues/2733#issuecomment-190743489
|
||||
import d3 from '@/utils/d3';
|
||||
import {event as d3Event} from 'd3-selection';
|
||||
import moment from 'moment';
|
||||
|
||||
const interval = 1000;
|
||||
export default {
|
||||
props: {
|
||||
traces: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data: () => ({
|
||||
brushSelection: null,
|
||||
hovered: null
|
||||
}),
|
||||
computed: {
|
||||
chartData() {
|
||||
if (this.traces.length <= 0) {
|
||||
return [];
|
||||
}
|
||||
const chartData = [];
|
||||
const now = moment().valueOf();
|
||||
let idx = this.traces.length - 1;
|
||||
const oldest = this.traces[this.traces.length - 1].timestamp.valueOf();
|
||||
|
||||
for (let time = Math.floor(oldest.valueOf() / interval) * interval; time < now; time += interval) {
|
||||
const bucket = {
|
||||
timeStart: time,
|
||||
timeEnd: time + interval,
|
||||
totalCount: 0,
|
||||
totalSuccess: 0,
|
||||
totalClientErrors: 0,
|
||||
totalServerErrors: 0,
|
||||
totalTime: 0,
|
||||
maxTime: 0
|
||||
};
|
||||
|
||||
while (idx >= 0 && this.traces[idx].timestamp.valueOf() < time + interval) {
|
||||
const trace = this.traces[idx];
|
||||
bucket.totalCount++;
|
||||
if (trace.isSuccess()) {
|
||||
bucket.totalSuccess++;
|
||||
} else if (trace.isClientError()) {
|
||||
bucket.totalClientErrors++;
|
||||
} else if (trace.isServerError()) {
|
||||
bucket.totalServerErrors++;
|
||||
}
|
||||
if (trace.timeTaken) {
|
||||
bucket.totalTime += trace.timeTaken;
|
||||
bucket.maxTime = Math.max(bucket.maxTime, trace.timeTaken);
|
||||
}
|
||||
idx--;
|
||||
}
|
||||
chartData.push(bucket);
|
||||
}
|
||||
|
||||
return chartData;
|
||||
},
|
||||
tooltipSelection() {
|
||||
return this.brushSelection ? this.brushSelection : this.hovered ? [this.hovered, this.hovered + interval] : null;
|
||||
},
|
||||
tooltipContent() {
|
||||
const selection = this.tooltipSelection;
|
||||
const totals = this.chartData.filter(
|
||||
bucket => bucket.timeStart.valueOf() >= selection[0] && bucket.timeStart.valueOf() < selection[1]
|
||||
).reduce(
|
||||
(current, next) => ({
|
||||
totalCount: current.totalCount + next.totalCount,
|
||||
totalSuccess: current.totalSuccess + next.totalSuccess,
|
||||
totalClientErrors: current.totalClientErrors + next.totalClientErrors,
|
||||
totalServerErrors: current.totalServerErrors + next.totalServerErrors,
|
||||
totalTime: current.totalTime + next.totalTime,
|
||||
maxTime: Math.max(current.maxTime, next.maxTime)
|
||||
}), {
|
||||
totalCount: 0,
|
||||
totalSuccess: 0,
|
||||
totalClientErrors: 0,
|
||||
totalServerErrors: 0,
|
||||
totalTime: 0,
|
||||
maxTime: 0
|
||||
});
|
||||
return {
|
||||
...totals,
|
||||
avgTime: totals.totalCount > 0 ? Math.floor(totals.totalTime / totals.totalCount) : 0
|
||||
};
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
drawChart(data) {
|
||||
const vm = this;
|
||||
|
||||
///setup x and y scale
|
||||
const x = d3.scaleTime()
|
||||
.range([0, vm.width])
|
||||
.domain(d3.extent(data, d => d.timeStart));
|
||||
this.x = x;
|
||||
|
||||
const y = d3.scaleLinear()
|
||||
.range([vm.height, 0])
|
||||
.domain([0, d3.max(data, d => d.totalCount)]);
|
||||
|
||||
//draw areas
|
||||
const area = d3.area()
|
||||
.x(d => x(d.data.timeStart))
|
||||
.y0(d => y(d[0]))
|
||||
.y1(d => y(d[1]));
|
||||
|
||||
const stack = d3.stack()
|
||||
.keys(['totalSuccess', 'totalClientErrors', 'totalServerErrors']);
|
||||
|
||||
const d = vm.areas.selectAll('.trace-chart__area')
|
||||
.data(stack(data));
|
||||
|
||||
d.enter().append('path')
|
||||
.merge(d)
|
||||
.attr('class', d => `trace-chart__area trace-chart__area--${d.key}`)
|
||||
.attr('d', area);
|
||||
|
||||
d.exit().remove();
|
||||
|
||||
//draw axis
|
||||
vm.xAxis.call(d3.axisBottom(x)
|
||||
.ticks(10)
|
||||
.tickFormat(d => moment(d).format('HH:mm:ss'))
|
||||
);
|
||||
|
||||
vm.yAxis.call(d3.axisRight(y)
|
||||
.ticks(Math.min(5, d3.max(data, d => d.totalCount)))
|
||||
.tickSize(this.width)
|
||||
).call(
|
||||
axis => axis.selectAll('.tick text')
|
||||
.attr('x', -2)
|
||||
.attr('dy', 2)
|
||||
.attr('text-anchor', 'end')
|
||||
);
|
||||
|
||||
//draw brush selection
|
||||
const brush = d3.brushX()
|
||||
.extent([[0, 0], [vm.width, vm.height]])
|
||||
.on('start', () => {
|
||||
if (d3Event.selection) {
|
||||
vm.isBrushing = true;
|
||||
vm.hovered = null;
|
||||
}
|
||||
})
|
||||
.on('brush', function () {
|
||||
if (d3Event.sourceEvent === null || d3Event.sourceEvent.type === 'brush') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (d3Event.selection) {
|
||||
const floor = Math.floor(x.invert(d3Event.selection[0]) / interval) * interval;
|
||||
const ceil = Math.ceil(x.invert(d3Event.selection[1]) / interval) * interval;
|
||||
d3.select(this).call(d3Event.target.move, [floor, ceil].map(x));
|
||||
vm.brushSelection = [floor, ceil];
|
||||
}
|
||||
})
|
||||
.on('end', () => {
|
||||
vm.isBrushing = false;
|
||||
if (!d3Event.selection) {
|
||||
vm.brushSelection = null;
|
||||
}
|
||||
});
|
||||
|
||||
vm.brushGroup.call(brush)
|
||||
.on('mousemove', () => {
|
||||
if (vm.isBrushing) {
|
||||
return;
|
||||
}
|
||||
const mouseX = d3.mouse(vm.brushGroup.select('.overlay').node())[0];
|
||||
vm.hovered = Math.floor(x.invert(mouseX) / interval) * interval;
|
||||
}).on('mouseout', () => {
|
||||
vm.hovered = null;
|
||||
});
|
||||
|
||||
brush.move(vm.brushGroup, vm.brushSelection ? vm.brushSelection.map(x) : null);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const margin = {
|
||||
top: 20,
|
||||
right: 20,
|
||||
bottom: 30,
|
||||
left: 20,
|
||||
};
|
||||
|
||||
this.width = this.$el.getBoundingClientRect().width - margin.left - margin.right;
|
||||
this.height = this.$el.getBoundingClientRect().height - margin.top - margin.bottom;
|
||||
|
||||
this.chartLayer = d3.select(this.$el.querySelector('.trace-chart__svg'))
|
||||
.append('g')
|
||||
.attr('transform', `translate(${margin.left},${margin.top})`);
|
||||
|
||||
this.xAxis = this.chartLayer.append('g')
|
||||
.attr('class', 'trace-chart__axis-x')
|
||||
.attr('transform', `translate(0,${this.height})`);
|
||||
|
||||
this.yAxis = this.chartLayer.append('g')
|
||||
.attr('class', 'trace-chart__axis-y')
|
||||
.attr('stroke', null);
|
||||
|
||||
this.areas = this.chartLayer.append('g');
|
||||
|
||||
this.hover = this.chartLayer.append('path')
|
||||
.attr('class', 'trace-chart__hover')
|
||||
.attr('opacity', 0);
|
||||
|
||||
this.brushGroup = this.chartLayer.append('g')
|
||||
.attr('class', 'trace-chart__brush');
|
||||
|
||||
this.drawChart(this.chartData);
|
||||
},
|
||||
watch: {
|
||||
chartData: 'drawChart',
|
||||
hovered(newVal) {
|
||||
if (newVal) {
|
||||
this.hover.attr('opacity', 1)
|
||||
.attr('d', `M${this.x(newVal)},${this.height} ${this.x(newVal)},0`);
|
||||
} else {
|
||||
this.hover.attr('opacity', 0);
|
||||
}
|
||||
},
|
||||
brushSelection(newVal) {
|
||||
this.$emit('selected', newVal);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "~@/assets/css/utilities";
|
||||
|
||||
.trace-chart {
|
||||
&__svg {
|
||||
height: 200px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__hover {
|
||||
stroke: $grey-light;
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
&__tooltip {
|
||||
position: absolute;
|
||||
background: $black;
|
||||
opacity: 0.8;
|
||||
pointer-events: none;
|
||||
border-radius: $radius-large;
|
||||
padding: 0.825em;
|
||||
width: 200px;
|
||||
|
||||
& table th,
|
||||
& table td {
|
||||
border: none;
|
||||
color: $grey-light;
|
||||
padding: 0.25em 0.75em;
|
||||
}
|
||||
|
||||
& table td {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&--left {
|
||||
left: 5px;
|
||||
}
|
||||
&--right {
|
||||
right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
& .selection {
|
||||
stroke: none;
|
||||
fill: rgba(0, 0, 0, 0.2);
|
||||
fill-opacity: 1;
|
||||
}
|
||||
|
||||
&__axis-y {
|
||||
& .domain {
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.tick:not(:first-of-type) {
|
||||
& line {
|
||||
stroke-dasharray: 2, 2;
|
||||
stroke: $grey-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__area {
|
||||
&--totalSuccess {
|
||||
fill: $success;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&--totalClientErrors {
|
||||
fill: $warning;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&--totalServerErrors {
|
||||
fill: $danger;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,140 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<table class="httptraces table is-hoverable is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="httptraces__trace-timestamp">Timestamp</th>
|
||||
<th class="httptraces__trace-method">Method</th>
|
||||
<th class="httptraces__trace-uri">Path</th>
|
||||
<th class="httptraces__trace-status">Status</th>
|
||||
<th class="httptraces__trace-contentType">Content-Type</th>
|
||||
<th class="httptraces__trace-contentLength">Length</th>
|
||||
<th class="httptraces__trace-timeTaken">Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="trace in traces">
|
||||
<tr class="is-selectable"
|
||||
:class="{ 'httptraces__trace---is-detailed' : showDetails[trace.key] }"
|
||||
@click="showDetails[trace.key] ? $delete(showDetails, trace.key) : $set(showDetails, trace.key, true)"
|
||||
:key="trace.key"
|
||||
>
|
||||
<td class="httptraces__trace-timestamp" v-text="trace.timestamp.format('L HH:mm:ss.SSS')" />
|
||||
<td class="httptraces__trace-method" v-text="trace.request.method" />
|
||||
<td class="httptraces__trace-uri" v-text="trace.request.uri" />
|
||||
<td class="httptraces__trace-status">
|
||||
<span v-text="trace.response.status" class="tag"
|
||||
:class="{ 'is-success' : trace.isSuccess(), 'is-warning' : trace.isClientError(), 'is-danger' : trace.isServerError() }"
|
||||
/>
|
||||
</td>
|
||||
<td class="httptraces__trace-contentType" v-text="trace.contentType" />
|
||||
<td class="httptraces__trace-contentLength"
|
||||
v-text="trace.contentLength ? prettyBytes(trace.contentLength) : ''"
|
||||
/>
|
||||
<td class="httptraces__trace-timeTaken"
|
||||
v-text="trace.timeTaken !== null && typeof trace.timeTaken !== 'undefined' ? `${trace.timeTaken} ms` : ''"
|
||||
/>
|
||||
</tr>
|
||||
<tr :key="`${trace.key}-detail`" v-if="showDetails[trace.key]">
|
||||
<td colspan="7">
|
||||
<pre class="httptraces__trace-detail" v-text="toJson(trace)" />
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<tr v-if="traces.length === 0">
|
||||
<td class="is-muted" colspan="7">No traces found.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
traces: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data: () => ({
|
||||
showDetails: {}
|
||||
}),
|
||||
methods: {
|
||||
prettyBytes,
|
||||
toJson(obj) {
|
||||
return JSON.stringify(obj, null, 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "~@/assets/css/utilities";
|
||||
|
||||
.httptraces {
|
||||
table-layout: fixed;
|
||||
|
||||
td {
|
||||
vertical-align: middle;
|
||||
overflow: hidden;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
&__trace {
|
||||
&--is-detailed td {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
&-timestamp {
|
||||
width: 130px;
|
||||
}
|
||||
|
||||
&-method {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
&-uri {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
&-status {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
&-contentType {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
&-contentLength {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
&-timeTaken {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
&-detail {
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<section class="section" :class="{ 'is-loading' : !hasLoaded }">
|
||||
<div v-if="error" class="message is-danger">
|
||||
<div class="message-body">
|
||||
<strong>
|
||||
<font-awesome-icon class="has-text-danger" icon="exclamation-triangle" />
|
||||
Fetching JMX Beans failed.
|
||||
</strong>
|
||||
<p v-text="error.message" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="columns">
|
||||
<div class="column" v-if="selectedDomain">
|
||||
<h1 class="heading">MBeans</h1>
|
||||
<div class="m-bean card" :class="{'is-active': mBean === selectedMBean}"
|
||||
v-for="mBean in selectedDomain.mBeans" :key="mBean.descriptor.raw" :id="mBean.descriptor.raw"
|
||||
v-on-clickaway="() => mBean === selectedMBean && select(selectedDomain)"
|
||||
>
|
||||
|
||||
<header class="m-bean--header hero"
|
||||
:class="{'is-primary': mBean === selectedMBean, 'is-selectable' : mBean !== selectedMBean }"
|
||||
@click="select(selectedDomain, mBean)"
|
||||
>
|
||||
<div class="level is-clipped">
|
||||
<div class="level-left">
|
||||
<div class="level-item is-narrow"
|
||||
v-for="attribute in mBean.descriptor.attributes"
|
||||
:key="`mBean-desc-${attribute.name}`"
|
||||
>
|
||||
<div class="is-clipped" :title="`${attribute.name} ${attribute.value}`">
|
||||
<p class="heading" v-text="attribute.name" />
|
||||
<p class="title is-size-6" v-text="attribute.value" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<sba-icon-button v-if="mBean === selectedMBean" :icon="['far', 'times-circle']"
|
||||
class="m-bean--header--close has-text-white"
|
||||
@click.stop="select(selectedDomain)"
|
||||
/>
|
||||
<div class="hero-foot tabs is-boxed" v-if="mBean === selectedMBean">
|
||||
<ul>
|
||||
<li v-if="mBean.attr" :class="{'is-active' : selected.view === 'attributes' }">
|
||||
<a @click.stop="select(selectedDomain, selectedMBean, 'attributes')">Attributes</a>
|
||||
</li>
|
||||
<li v-if="mBean.op" :class="{'is-active' : selected.view === 'operations' }">
|
||||
<a @click.stop="select(selectedDomain, selectedMBean, 'operations')">Operations</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="card-content" v-if="mBean === selectedMBean">
|
||||
<m-bean-attributes v-if="selected.view === 'attributes'" :instance="instance"
|
||||
:domain="selectedDomain.domain" :m-bean="mBean"
|
||||
/>
|
||||
<m-bean-operations v-if="selected.view === 'operations'" :instance="instance"
|
||||
:domain="selectedDomain.domain" :m-bean="mBean"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
<nav class="menu" v-sticks-below="['#navigation']">
|
||||
<p class="menu-label">domains</p>
|
||||
<ul class="menu-list">
|
||||
<li>
|
||||
<a class="" v-for="domain in domains" :key="domain.domain"
|
||||
:class="{'is-active' : domain === selectedDomain}"
|
||||
v-text="domain.domain" @click="select(domain)"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import sticksBelow from '@/directives/sticks-below';
|
||||
import Instance from '@/services/instance';
|
||||
import fromPairs from 'lodash/fromPairs';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import {directive as onClickaway} from 'vue-clickaway';
|
||||
import mBeanAttributes from './m-bean-attributes';
|
||||
import mBeanOperations from './m-bean-operations';
|
||||
|
||||
const getOperationName = (name, descriptor) => {
|
||||
const params = descriptor.args.map(arg => arg.type).join(',');
|
||||
return `${name}(${params})`;
|
||||
};
|
||||
|
||||
class MBeanDescriptor {
|
||||
constructor(raw) {
|
||||
Object.assign(this, MBeanDescriptor.parse(raw));
|
||||
this.raw = raw;
|
||||
}
|
||||
|
||||
static parse(raw) {
|
||||
const attributes = raw.split(',')
|
||||
.map(attribute => attribute.split('='))
|
||||
.map(([name, value]) => ({name, value}));
|
||||
const displayName = attributes.map(({value}) => value).join(' ').trim();
|
||||
return {attributes, displayName}
|
||||
}
|
||||
}
|
||||
|
||||
export class MBean {
|
||||
constructor({descriptor, op, ...mBean}) {
|
||||
Object.assign(this, mBean);
|
||||
this.descriptor = new MBeanDescriptor(descriptor);
|
||||
const flattenedOps = Object.entries(op || {}).flatMap(([name, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(v => [name, v]);
|
||||
} else {
|
||||
return [[name, value]];
|
||||
}
|
||||
}).map(([name, operation]) => [getOperationName(name, operation), operation]);
|
||||
this.op = flattenedOps.length > 0 ? fromPairs(flattenedOps) : null;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
props: {
|
||||
instance: {
|
||||
type: Instance,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line vue/no-unused-components
|
||||
components: {mBeanOperations, mBeanAttributes},
|
||||
directives: {onClickaway, sticksBelow},
|
||||
data: () => ({
|
||||
hasLoaded: false,
|
||||
error: null,
|
||||
domains: [],
|
||||
selected: {
|
||||
domain: null,
|
||||
mBean: null,
|
||||
view: null
|
||||
}
|
||||
}),
|
||||
computed: {
|
||||
selectedDomain() {
|
||||
return this.domains.find(d => d.domain === this.selected.domain)
|
||||
},
|
||||
selectedMBean() {
|
||||
return this.selectedDomain && this.selectedDomain.mBeans.find(b => b.descriptor.raw === this.selected.mBean)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.fetchMBeans();
|
||||
},
|
||||
watch: {
|
||||
'$route': {
|
||||
immediate: true,
|
||||
handler() {
|
||||
if (!isEmpty(this.$route.query)) {
|
||||
this.selected = this.$route.query;
|
||||
} else if (this.domains.length > 0) {
|
||||
this.select(this.domains[0]);
|
||||
}
|
||||
}
|
||||
},
|
||||
selected() {
|
||||
if (!isEqual(this.selected, this.$route.query)) {
|
||||
this.$router.replace({
|
||||
name: 'instances/jolokia',
|
||||
query: this.selected
|
||||
});
|
||||
}
|
||||
},
|
||||
async selectedMBean(newVal) {
|
||||
if (newVal) {
|
||||
await this.$nextTick();
|
||||
const el = document.getElementById(newVal.descriptor.raw);
|
||||
if (el) {
|
||||
const scrollingEl = document.scrollingElement;
|
||||
const navigation = document.querySelector('#navigation');
|
||||
const navbarOffset = (navigation ? navigation.getBoundingClientRect().bottom : 120) + 10;
|
||||
const top = scrollingEl.scrollTop + el.getBoundingClientRect().top - navbarOffset;
|
||||
if (scrollingEl.scrollTo) {
|
||||
scrollingEl.scrollTo({top, behavior: 'smooth'})
|
||||
} else {
|
||||
scrollingEl.scrollTop = top;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async fetchMBeans() {
|
||||
this.error = null;
|
||||
try {
|
||||
const res = await this.instance.listMBeans();
|
||||
const domains = sortBy(res.data, [d => d.domain]);
|
||||
this.domains = domains.map(domain => ({
|
||||
...domain,
|
||||
mBeans: sortBy(domain.mBeans.map(mBean => new MBean(mBean)), [b => b.descriptor.displayName])
|
||||
}));
|
||||
if (!this.selectedDomain && this.domains.length > 0) {
|
||||
this.select(this.domains[0]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Fetching MBeans failed:', error);
|
||||
this.error = error;
|
||||
}
|
||||
this.hasLoaded = true;
|
||||
},
|
||||
select(domain, mBean, view) {
|
||||
this.selected = {
|
||||
domain: domain && domain.domain,
|
||||
mBean: mBean && mBean.descriptor.raw,
|
||||
view: view || (mBean ? (mBean.attr ? 'attributes' : (mBean.op ? 'operations' : null)) : null)
|
||||
};
|
||||
}
|
||||
},
|
||||
install({viewRegistry}) {
|
||||
viewRegistry.addView({
|
||||
name: 'instances/jolokia',
|
||||
parent: 'instances',
|
||||
path: 'jolokia',
|
||||
component: this,
|
||||
label: 'JMX',
|
||||
group: 'JVM',
|
||||
order: 350,
|
||||
isEnabled: ({instance}) => instance.hasEndpoint('jolokia')
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "~@/assets/css/utilities";
|
||||
|
||||
.m-bean {
|
||||
transition: all $easing $speed;
|
||||
|
||||
&.is-active {
|
||||
margin: 0.75rem -0.75rem;
|
||||
max-width: unset;
|
||||
}
|
||||
|
||||
&.is-active .m-bean--header {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
&:not(.is-active) .m-bean--header:hover {
|
||||
background-color: $white-bis;
|
||||
}
|
||||
|
||||
&--header {
|
||||
& .level .level-left {
|
||||
width: 100%;
|
||||
|
||||
& .level-item {
|
||||
min-width: 0;
|
||||
flex-shrink: 1;
|
||||
|
||||
& p {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--close {
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="field">
|
||||
<label class="label">
|
||||
<span v-text="name" />
|
||||
<small class="is-muted has-text-weight-normal" v-text="descriptor.type" />
|
||||
</label>
|
||||
<div class="field-body">
|
||||
<div class="field is-expanded">
|
||||
<div class="field has-addons">
|
||||
<div class="control">
|
||||
<button class="button" :disabled="!descriptor.rw" @click="edit">
|
||||
<font-awesome-icon icon="pencil-alt" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="control is-expanded has-icons-right">
|
||||
<template v-if="!editing">
|
||||
<input v-if="!hasComplexValue" class="input" type="text" readonly :value="value" @dblclick="edit">
|
||||
<textarea v-else class="input m-bean-attribute--text" readonly v-text="jsonValue" />
|
||||
<span class="icon is-right has-text-warning" v-if="error">
|
||||
<font-awesome-icon icon="exclamation-triangle" />
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<input v-if="!hasComplexValue" class="input" type="text" ref="input"
|
||||
v-model="input" @keyup.esc="cancel" @keyup.enter="save"
|
||||
>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="help" v-text="descriptor.desc" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="control" v-if="editing">
|
||||
<button class="button is-light is-small" @click="cancel">Cancel</button>
|
||||
<button class="button is-primary is-small" :class="{'is-loading' : saving}" @click="save"
|
||||
:disabled="value === input"
|
||||
>Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
descriptor: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
value: {
|
||||
// eslint-disable-next-line vue/require-prop-type-constructor
|
||||
type: null,
|
||||
default: null
|
||||
},
|
||||
onSaveValue: {
|
||||
type: Function,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data: () => ({
|
||||
input: null,
|
||||
editing: false,
|
||||
saving: false,
|
||||
error: null
|
||||
}),
|
||||
computed: {
|
||||
hasComplexValue() {
|
||||
return this.value !== null && typeof this.value === 'object';
|
||||
},
|
||||
jsonValue() {
|
||||
return JSON.stringify(this.value, null, 4);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async edit() {
|
||||
if (this.descriptor.rw && !this.hasComplexValue) {
|
||||
this.input = this.value;
|
||||
this.editing = true;
|
||||
await this.$nextTick();
|
||||
this.$refs.input.focus();
|
||||
}
|
||||
},
|
||||
cancel() {
|
||||
this.editing = false;
|
||||
},
|
||||
async save() {
|
||||
this.saving = true;
|
||||
try {
|
||||
await this.onSaveValue(this.input);
|
||||
} catch (error) {
|
||||
console.warn(`Error saving attribute ${this.name}`, error);
|
||||
this.error = error;
|
||||
} finally {
|
||||
this.saving = false;
|
||||
this.editing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "~@/assets/css/utilities";
|
||||
|
||||
.m-bean-attribute--text {
|
||||
resize: vertical;
|
||||
min-height: 120px;
|
||||
}
|
||||
</style>
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="error" class="message is-danger">
|
||||
<div class="message-body">
|
||||
<strong>
|
||||
<font-awesome-icon class="has-text-danger" icon="exclamation-triangle" />
|
||||
Fetching attributes failed.
|
||||
</strong>
|
||||
<p v-text="error.message" />
|
||||
</div>
|
||||
</div>
|
||||
<m-bean-attribute v-for="(attribute, name) in mBean.attr" :key="`attr-${name}`"
|
||||
:name="name" :descriptor="attribute" :value="attributeValues && attributeValues[name]"
|
||||
:on-save-value="value => writeAttribute(name, value)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Instance from '@/services/instance';
|
||||
import {MBean} from './index';
|
||||
import mBeanAttribute from './m-bean-attribute';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
domain: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
mBean: {
|
||||
type: MBean,
|
||||
required: true
|
||||
},
|
||||
instance: {
|
||||
type: Instance,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line vue/no-unused-components
|
||||
components: {mBeanAttribute},
|
||||
data: () => ({
|
||||
attributeValues: null,
|
||||
error: null
|
||||
}),
|
||||
computed: {},
|
||||
methods: {
|
||||
async readAttributes() {
|
||||
try {
|
||||
const response = await this.instance.readMBeanAttributes(this.domain, this.mBean.descriptor.raw);
|
||||
this.attributeValues = response.data.value;
|
||||
} catch (error) {
|
||||
console.warn('Fetching MBean attributes failed:', error);
|
||||
this.error = error;
|
||||
}
|
||||
},
|
||||
async writeAttribute(attribute, value) {
|
||||
try {
|
||||
await this.instance.writeMBeanAttribute(this.domain, this.mBean.descriptor.raw, attribute, value);
|
||||
} finally {
|
||||
await this.readAttributes();
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.readAttributes();
|
||||
},
|
||||
}
|
||||
</script>
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="modal is-active">
|
||||
<div class="modal-background" @click="abort" />
|
||||
<div class="modal-content">
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title" v-text="name" />
|
||||
</header>
|
||||
|
||||
<template v-if="state === 'input-args'">
|
||||
<section class="modal-card-body" @keyup.ctrl.enter="invoke(args)">
|
||||
<div class="field" v-for="(arg, idx) in descriptor.args" :key="arg.name">
|
||||
<label class="label">
|
||||
<span v-text="arg.name" />
|
||||
<small class="is-muted has-text-weight-normal" v-text="arg.type" />
|
||||
</label>
|
||||
<div class="control">
|
||||
<input type="text" class="input" v-model="args[idx]">
|
||||
</div>
|
||||
<p class="help" v-text="arg.desc" />
|
||||
</div>
|
||||
</section>
|
||||
<footer class="modal-card-foot">
|
||||
<div class="field is-grouped is-grouped-right">
|
||||
<div class="control">
|
||||
<button class="button is-primary" @click="invoke(args)">Execute</button>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<template v-else-if="state === 'executing'">
|
||||
<section class="modal-card-body">
|
||||
<section class="section is-loading">
|
||||
<p>Executing...</p>
|
||||
</section>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<template v-else-if="state === 'completed'">
|
||||
<section class="modal-card-body">
|
||||
<div class="message is-success">
|
||||
<div class="message-body">
|
||||
<strong>Execution successful.</strong>
|
||||
</div>
|
||||
</div>
|
||||
<pre v-if="descriptor.ret !== 'void'" v-text="prettyPrintedResult" />
|
||||
</section>
|
||||
<footer class="modal-card-foot">
|
||||
<div class="field is-grouped is-grouped-right">
|
||||
<div class="control">
|
||||
<button class="button is-light" @click="abort"> Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<template v-else-if="state === 'failed'">
|
||||
<section class="modal-card-body">
|
||||
<div class="message is-danger">
|
||||
<div class="message-body">
|
||||
<strong>
|
||||
<font-awesome-icon class="has-text-danger"
|
||||
icon="exclamation-triangle"
|
||||
/>
|
||||
Execution failed.
|
||||
</strong>
|
||||
<p v-text="error.message" />
|
||||
</div>
|
||||
</div>
|
||||
<pre v-if="error.stacktrace"
|
||||
v-text="error.stacktrace"
|
||||
/>
|
||||
<pre v-if="error.response && error.response.data"
|
||||
v-text="error.response.data"
|
||||
/>
|
||||
</section>
|
||||
<footer class="modal-card-foot">
|
||||
<div class="field is-grouped is-grouped-right">
|
||||
<div class="control">
|
||||
<button class="button is-light" @click="abort"> Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
descriptor: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
value: {
|
||||
// eslint-disable-next-line vue/require-prop-type-constructor
|
||||
type: null,
|
||||
default: null
|
||||
},
|
||||
onClose: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
onExecute: {
|
||||
type: Function,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data: () => ({
|
||||
state: null,
|
||||
error: null,
|
||||
args: null,
|
||||
result: null
|
||||
}),
|
||||
computed: {
|
||||
prettyPrintedResult() {
|
||||
if (this.result && typeof this.result === 'string') {
|
||||
try {
|
||||
const o = JSON.parse(this.result);
|
||||
return JSON.stringify(o, undefined, 4);
|
||||
} catch (e) {
|
||||
return this.result;
|
||||
}
|
||||
} else if (typeof result === 'object') {
|
||||
return JSON.stringify(this.result, undefined, 4);
|
||||
}
|
||||
return this.result;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
abort() {
|
||||
this.onClose();
|
||||
},
|
||||
invoke(args) {
|
||||
this.state = (args || this.descriptor.args.length === 0) ? 'prepared' : 'input-args';
|
||||
this.args = args || new Array(this.descriptor.args.length);
|
||||
this.error = null;
|
||||
this.result = null;
|
||||
|
||||
if (this.state === 'prepared') {
|
||||
this.execute()
|
||||
}
|
||||
},
|
||||
async execute() {
|
||||
this.state = 'executing';
|
||||
try {
|
||||
const result = await this.onExecute(this.args);
|
||||
if (result.data.status < 400) {
|
||||
this.result = result.data.value;
|
||||
this.state = 'completed';
|
||||
} else {
|
||||
const error = new Error(`Execution failed: ${result.data.error}`);
|
||||
error.stacktrace = result.data.stacktrace;
|
||||
this.state = 'failed';
|
||||
this.error = error;
|
||||
console.warn('Invocation failed', error);
|
||||
}
|
||||
} catch (error) {
|
||||
this.state = 'failed';
|
||||
this.error = error;
|
||||
console.warn('Invocation failed', error);
|
||||
}
|
||||
},
|
||||
keyHandler(event) {
|
||||
if (event.keyCode === 27) {
|
||||
this.abort()
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.invoke();
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener('keyup', this.keyHandler)
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('keyup', this.keyHandler)
|
||||
},
|
||||
}
|
||||
</script>
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<button class="button is-light is-fullwidth" @click="$emit('click', $event)">
|
||||
<small class="is-light is-muted" v-text="descriptor.ret" /> <span v-text="name" />
|
||||
</button>
|
||||
<p class="help" v-text="descriptor.desc" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
descriptor: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<mBeanOperation v-for="(operation, name) in mBean.op" :key="`op-${name}`"
|
||||
:name="name" :descriptor="operation" @click="invoke(name, operation)"
|
||||
/>
|
||||
<m-bean-operation-invocation v-if="invocation" :name="invocation.name" :descriptor="invocation.descriptor"
|
||||
:on-execute="execute" :on-close="closeInvocation"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Instance from '@/services/instance';
|
||||
import {MBean} from './index';
|
||||
import mBeanOperation from './m-bean-operation';
|
||||
import mBeanOperationInvocation from './m-bean-operation-invocation';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
domain: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
mBean: {
|
||||
type: MBean,
|
||||
required: true
|
||||
},
|
||||
instance: {
|
||||
type: Instance,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line vue/no-unused-components
|
||||
components: {mBeanOperation, mBeanOperationInvocation},
|
||||
data: () => ({
|
||||
invocation: null
|
||||
}),
|
||||
computed: {},
|
||||
methods: {
|
||||
closeInvocation() {
|
||||
this.invocation = null
|
||||
},
|
||||
invoke(name, descriptor) {
|
||||
this.invocation = {name, descriptor};
|
||||
},
|
||||
execute(args) {
|
||||
return this.instance.invokeMBeanOperation(this.domain, this.mBean.descriptor.raw, this.invocation.name, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,161 @@
|
||||
<!--
|
||||
- Copyright 2014-2018 the original author or authors.
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<section class="section" :class="{ 'is-loading' : !hasLoaded }">
|
||||
<template v-if="hasLoaded">
|
||||
<div v-if="error" class="message is-danger">
|
||||
<div class="message-body">
|
||||
<strong>
|
||||
<font-awesome-icon class="has-text-danger" icon="exclamation-triangle" />
|
||||
Fetching Liquibase migrations failed.
|
||||
</strong>
|
||||
<p v-text="error.message" />
|
||||
</div>
|
||||
</div>
|
||||
<template v-for="(context, ctxName) in contexts">
|
||||
<h3 class="title" v-text="ctxName" :key="ctxName" />
|
||||
<template v-for="(report, name) in context.liquibaseBeans">
|
||||
<sba-panel :key="`${ctxName}-${name}`" :title="`name`" class="change-set"
|
||||
:header-sticks-below="['#navigation']"
|
||||
>
|
||||
<table class="table is-hoverable is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Execution</th>
|
||||
<th>Description</th>
|
||||
<th>Tag</th>
|
||||
<th>Contexts</th>
|
||||
<th>Labels</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="changeSet in report.changeSets">
|
||||
<tr :key="`${ctxName}-${name}-${changeSet.id}`" class="is-selectable"
|
||||
@click="showDetails[changeSet.checksum] ? $delete(showDetails, changeSet.checksum) : $set(showDetails, changeSet.checksum, true)"
|
||||
>
|
||||
<td v-text="changeSet.id" />
|
||||
<td>
|
||||
<span v-text="changeSet.execType" class="tag" :class="execClass(execType)" />
|
||||
</td>
|
||||
<td v-text="changeSet.description" />
|
||||
<td v-text="changeSet.tag" />
|
||||
<td v-text="changeSet.contexts.join(', ')" />
|
||||
<td>
|
||||
<span v-for="label in changeSet.labels" :key="`${ctxName}-${name}-${changeSet.id}-${label}`"
|
||||
class="tag is-info" v-text="label"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="showDetails[changeSet.checksum]" :key="`${ctxName}-${name}-${changeSet.id}-details`">
|
||||
<td colspan="6">
|
||||
<table class="table is-fullwidth">
|
||||
<tr>
|
||||
<th>Changelog</th>
|
||||
<td colspan="3" v-text="changeSet.changeLog" />
|
||||
<th>Author</th>
|
||||
<td v-text="changeSet.author" />
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Checksum</th>
|
||||
<td v-text="changeSet.checksum" />
|
||||
<th>Comments</th>
|
||||
<td colspan="3" v-text="changeSet.comments" />
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Execution Order</th>
|
||||
<td v-text="changeSet.orderExecuted" />
|
||||
<th>ExecutionDate</th>
|
||||
<td v-text="changeSet.dateExecuted" />
|
||||
<th>DeploymentId</th>
|
||||
<td v-text="changeSet.deploymentId" />
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</sba-panel>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Instance from '@/services/instance';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
instance: {
|
||||
type: Instance,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data: () => ({
|
||||
hasLoaded: false,
|
||||
error: null,
|
||||
contexts: null,
|
||||
showDetails: {}
|
||||
}),
|
||||
computed: {},
|
||||
created() {
|
||||
this.fetchLiquibase();
|
||||
},
|
||||
methods: {
|
||||
async fetchLiquibase() {
|
||||
this.error = null;
|
||||
try {
|
||||
const res = await this.instance.fetchLiquibase();
|
||||
this.contexts = res.data.contexts;
|
||||
} catch (error) {
|
||||
console.warn('Fetching Liquibase changeSets failed:', error);
|
||||
this.error = error;
|
||||
}
|
||||
this.hasLoaded = true;
|
||||
},
|
||||
execClass(execType) {
|
||||
switch (execType) {
|
||||
case 'EXECUTED':
|
||||
return 'is-success';
|
||||
case 'FAILED':
|
||||
return 'is-danger';
|
||||
case 'SKIPPED':
|
||||
return 'is-light';
|
||||
case 'RERAN':
|
||||
case 'MARK_RAN':
|
||||
return 'is-warning';
|
||||
default:
|
||||
return 'is-info';
|
||||
}
|
||||
}
|
||||
},
|
||||
install({viewRegistry}) {
|
||||
viewRegistry.addView({
|
||||
name: 'instances/liquibase',
|
||||
parent: 'instances',
|
||||
path: 'liquibase',
|
||||
component: this,
|
||||
label: 'Liquibase',
|
||||
group: 'Data',
|
||||
order: 900,
|
||||
isEnabled: ({instance}) => instance.hasEndpoint('liquibase')
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>å
|
||||
Loaded 100 of 146 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user