Compare commits

...

408 Commits

Author SHA1 Message Date
w940853815
cce6a5a216 perf: streamline onUpdate actions in PlayBook and Adhoc components 2025-02-21 14:20:59 +08:00
ibuler
8ef4b466d7 perf: update execution 2025-02-20 18:43:22 +08:00
ibuler
4ccc7ac56b perf: merge with remote 2025-02-20 18:41:15 +08:00
ibuler
7fb24cadea perf: update formatter 2025-02-20 18:40:16 +08:00
feng
e89be5f278 perf: execution list 2025-02-20 14:41:50 +08:00
zhaojisen
b746e1d553 Perf: Add Dashboard Items 2025-02-20 14:17:03 +08:00
w940853815
ffee484d6c perf: add drawer functionality and title formatting to AccountChangeSecret and AccountPush execution lists 2025-02-19 18:49:02 +08:00
w940853815
d5099c3a04 perf: add update actions and detail formatters to various components 2025-02-19 15:35:34 +08:00
zhaojisen
2558cfa4c2 Fixed: Dashboard 2025-02-19 11:41:30 +08:00
w940853815
6357056600 fix: table row index error 2025-02-19 11:13:21 +08:00
w940853815
c1b60e6298 perf: Session list with detail drawer 2025-02-18 19:01:17 +08:00
ibuler
f2b45c5084 Merge branch 'pam' of github.com:jumpserver/lina into pam 2025-02-18 16:51:33 +08:00
ibuler
a451f38701 perf: update some page 2025-02-18 16:51:26 +08:00
w940853815
27e2d34171 fix: Create playbook job form error 2025-02-18 11:18:36 +08:00
zhaojisen
fa2faecb7c Fixed: Fix card overflow issue on screen resize 2025-02-17 17:19:40 +08:00
zhaojisen
e6cd2306c9 Fixed: Fix responsive issues 2025-02-17 17:09:48 +08:00
feng
ded7920a8a perf: translate 2025-02-13 18:57:20 +08:00
ibuler
f1956b4982 perf: update some i18n 2025-02-13 16:41:06 +08:00
ibuler
0cd9f6dcd6 Merge branch 'pam' of github.com:jumpserver/lina into pam 2025-02-13 15:55:43 +08:00
ibuler
b2abee724d perf: update detail card 2025-02-13 15:55:20 +08:00
w940853815
3e3cb309ce perf: Merge More into Info and update detail page 2025-02-13 15:26:17 +08:00
ibuler
55c8dfa549 Merge branch 'pam' of github.com:jumpserver/lina into pam 2025-02-13 15:08:14 +08:00
ibuler
9740c58291 perf: update detail 2025-02-13 15:08:04 +08:00
feng
77558186a4 perf: Translate 2025-02-13 15:01:34 +08:00
feng
b524848742 perf: Translate 2025-02-12 18:43:48 +08:00
w940853815
339c1ab227 perf: Add Java, Node, Go, and cURL demo code 2025-02-12 18:16:31 +08:00
ibuler
1bdc27ea98 Merge branch 'pam' of github.com:jumpserver/lina into pam 2025-02-12 18:07:40 +08:00
ibuler
19dc42cd2c perf: update detail page 2025-02-12 18:07:24 +08:00
zhaojisen
7375dc58a3 Perf: Change Mission Chart Size 2025-02-11 18:52:11 +08:00
w940853815
ee678289d4 perf: Add demo code docs 2025-02-11 18:20:10 +08:00
feng
c932fbb5f3 perf: Pam dashboard 2025-02-11 18:20:02 +08:00
ibuler
768cae7abc Merge branch 'pam' of github.com:jumpserver/lina into pam 2025-02-11 15:56:11 +08:00
ibuler
5abb8a867f perf: update detail api url 2025-02-11 15:55:54 +08:00
zhaojisen
c4bba7e74d Perf: Change AccountConnectFormatter To Common Component 2025-02-11 14:47:02 +08:00
feng
e365f874bc perf: Platform 2025-02-11 14:40:51 +08:00
ibuler
f54e2c8ddc perf: 修改授权 2025-02-11 14:32:08 +08:00
ibuler
ff769179a8 Merge branch 'pam' of github.com:jumpserver/lina into pam 2025-02-10 17:43:36 +08:00
ibuler
8546388439 perf: update user quick filters 2025-02-10 17:39:18 +08:00
w940853815
7dcecd4421 perf: modify markdown code style 2025-02-10 16:42:27 +08:00
w940853815
efef672157 perf: code copy 2025-02-08 18:26:35 +08:00
ibuler
ec8c8e7d4b Merge branch 'pam' of github.com:jumpserver/lina into pam 2025-02-08 17:28:50 +08:00
ibuler
c14970e232 perf: update update 2025-02-08 17:28:44 +08:00
jiangweidong
bee45ee7a0 feat: Custom change password supports configuration of interactive items 2025-02-08 16:57:01 +08:00
feng
7bfc0ee507 perf: push record header 2025-02-08 16:03:35 +08:00
ibuler
731ae82bc5 Merge branch 'pam' of github.com:jumpserver/lina into pam 2025-02-06 19:18:24 +08:00
ibuler
8019ee550e perf: move to roles manage to form 2025-02-06 19:18:16 +08:00
w940853815
d7affe1034 perf: integrations application detail add account tab page 2025-02-06 19:11:38 +08:00
ibuler
5f35e0bb94 perf: merge with remote 2025-02-06 12:50:43 +08:00
ibuler
75b70d33ee perf: some feat free 2025-02-06 12:48:16 +08:00
zhaojisen
e3c17ef96d Perf: Add Account Valid License 2025-02-06 11:05:57 +08:00
w940853815
0f6ae3f626 fix: profile preferences api request 404 2025-02-05 18:46:03 +08:00
ibuler
175b819e8e Merge branch 'pam' of github.com:jumpserver/lina into pam 2025-02-05 16:34:44 +08:00
ibuler
9933f68ba9 feat: add xpack lock feat 2025-02-05 16:34:17 +08:00
feng
44e05e7f80 perf: pam dashboard 2025-02-05 15:47:33 +08:00
zhaojisen
f518e4cb10 Perf: Add Cache Option 2025-02-05 11:42:19 +08:00
zhaojisen
b127849388 perf: Perf optimism build cach 2025-02-05 11:20:44 +08:00
feng
7524b6f895 perf: change secret push account record 2025-01-26 17:27:01 +08:00
feng
0a96331218 perf: Push account record 2025-01-26 16:40:41 +08:00
ibuler
caf34a96e6 Merge branch 'pam' of github.com:jumpserver/lina into pam 2025-01-23 14:03:17 +08:00
ibuler
5645d253e5 perf: update pam account 2025-01-23 14:01:54 +08:00
w940853815
e2ad3f3749 fix: PermUser name column is undefined 2025-01-23 10:55:19 +08:00
zhaojisen
b1137249f1 Perf: Perf Dashboard 2025-01-22 18:52:29 +08:00
ibuler
369247d987 perf: update detail header border 2025-01-22 17:47:23 +08:00
ibuler
fe8f594cb4 perf: remove footer 2025-01-22 17:09:16 +08:00
ibuler
64db1176a6 Merge branch 'pam' of github.com:jumpserver/lina into pam 2025-01-22 16:30:43 +08:00
ibuler
27d175a06e perf: update zone detail 2025-01-22 16:30:31 +08:00
zhaojisen
3b96b3c90b Perf: Optimism Interactive Effect 2025-01-21 18:55:51 +08:00
zhaojisen
32183336a2 Perf: Optimism Summery Card 2025-01-21 18:36:32 +08:00
ibuler
fb9d9d6480 perf: merge with remote 2025-01-21 17:50:26 +08:00
ibuler
7394ff27a5 perf: update menu 2025-01-21 17:48:44 +08:00
feng
6093d8915c perf: engine 2025-01-21 17:09:06 +08:00
ibuler
83bb13b806 Merge branch 'pam' of github.com:jumpserver/lina into pam 2025-01-20 19:34:34 +08:00
ibuler
c9062a9e26 perf: update i18n 2025-01-20 19:34:20 +08:00
feng
6dc7cc0b37 perf: pam perm 2025-01-20 18:11:05 +08:00
ibuler
aa631678f8 perf: add it 2025-01-20 18:05:29 +08:00
ibuler
1c36d1b3c6 perf: use close to reslove except confirm 2025-01-20 18:05:01 +08:00
feng
90050bf87d perf: checkaccountautomation perm 2025-01-20 16:37:48 +08:00
zhaojisen
58bb69c252 Perf: Remove isPam item 2025-01-20 11:07:27 +08:00
ibuler
14c3b249a7 perf: update pam 2025-01-15 17:20:17 +08:00
ibuler
4589c4abaf Merge branch 'pam' of github.com:jumpserver/lina into pam 2025-01-15 17:04:49 +08:00
ibuler
94f1959d40 perf: 修改添加 update secret 2025-01-15 17:04:31 +08:00
zhaojisen
e7c63b2d41 Perf: Add Console Account Formatter 2025-01-15 13:22:08 +08:00
ibuler
e2904acff6 perf: update execution 2025-01-15 10:26:44 +08:00
ibuler
3d1a2d6833 Merge branch 'pam' of github.com:jumpserver/lina into pam 2025-01-14 18:12:56 +08:00
ibuler
d1e340f104 perf: update pam risk handle 2025-01-14 18:12:38 +08:00
w940853815
6d8f159975 perf: Optimize risk action checks and filters 2025-01-14 14:52:46 +08:00
ibuler
7075b5c90a perf: update drawer 2025-01-13 11:11:32 +08:00
ibuler
66b3018ace perf: 修改 drawer 2025-01-10 18:33:34 +08:00
ibuler
87ae6242a1 merge: with remote 2025-01-10 18:08:00 +08:00
zhaojisen
de5f1a8bd1 Fixed: Fixed z-index issue between footer and card components\ 2025-01-10 17:58:10 +08:00
ibuler
9cefe70061 perf: update pam 2025-01-10 17:53:44 +08:00
zhaojisen
73cbd4d8d2 Optimism ReviewDrawer Item Style 2025-01-10 17:44:00 +08:00
zhaojisen
02d9f6b116 Perf: Add RiskSummery Item 2025-01-10 15:52:40 +08:00
ibuler
1f5cc25b86 Merge branch 'pam' of github.com:jumpserver/lina into pam 2025-01-09 19:19:05 +08:00
ibuler
63200bca26 perf: 优化 platforms 2025-01-09 19:18:51 +08:00
zhaojisen
305029649e Perf: Add Mission Card 2025-01-09 18:04:08 +08:00
ibuler
cfb7292f36 perf: update tab height 2025-01-09 16:43:12 +08:00
ibuler
582b83d4f5 Merge branch 'pam' of github.com:jumpserver/lina into pam 2025-01-09 16:28:32 +08:00
ibuler
42eaa4996d perf: update page height 2025-01-09 16:28:21 +08:00
feng
df465eef29 perf: dashboard 2025-01-09 16:12:49 +08:00
zhaojisen
b3f8c61df0 Perf: Adjust Card Style 2025-01-09 14:09:11 +08:00
zhaojisen
6f57ef5bcf Perf: Fit SummeryCard style 2025-01-08 17:22:58 +08:00
jiangweidong
7ca47d9015 perf: Check for leaked duplicate passwords. 2025-01-08 16:07:02 +08:00
zhaojisen
b9a0bbf12d Perf: Optimize the DataSummary style 2025-01-08 16:05:07 +08:00
w940853815
4a8c84ee9d perf: Add risk change_password_add handle 2025-01-08 14:34:58 +08:00
zhaojisen
e8e1b35cd3 perf: Add Dashboard Summery 2025-01-08 14:19:57 +08:00
zhaojisen
edd85e700e Fixed: Resolved the issue of duplicate requests by implementing a check for the value of zero. 2025-01-08 11:47:56 +08:00
ibuler
a7378d297f Merge branch 'pam' of github.com:jumpserver/lina into pam 2025-01-08 10:47:42 +08:00
ibuler
dd75e160c3 perf: update filter 2025-01-08 10:47:29 +08:00
feng
84c78f993b perf: account backup execution detail 2025-01-07 17:54:31 +08:00
zhaojisen
25ba669f67 Perf: Disabled Password Input And Hidden Template Username Input 2025-01-07 15:21:09 +08:00
zhaojisen
3bb3361e2f Perf: Optimize some details 2025-01-07 14:59:27 +08:00
ibuler
d82e557fd1 merge: with remote 2025-01-07 14:36:47 +08:00
ibuler
3c9de27e66 perf: update i18n 2025-01-07 14:35:21 +08:00
feng
276b1a7255 perf: pam translate 2025-01-07 14:20:50 +08:00
feng
31c630ca25 perf: account gilter 2025-01-06 17:29:53 +08:00
feng626
e5eaa05bdb Merge pull request #4575 from jumpserver/pr@pam@pam_dashboard
perf: Pam dashboard
2025-01-06 17:03:24 +08:00
feng626
2431755f2b Merge branch 'pam' into pr@pam@pam_dashboard 2025-01-06 16:55:33 +08:00
feng
29f35d590e perf: Pam dashboard 2025-01-06 16:51:32 +08:00
zhaojisen
ac9451dd1c Perf: Change Connect Formatter To More Higher-level Encapsulation 2025-01-06 15:21:21 +08:00
zhaojisen
b4ee023b93 Perf: Change Connect To Formatter Component 2025-01-06 14:46:50 +08:00
ibuler
28e7321754 perf: update account prop 2025-01-06 14:03:48 +08:00
ibuler
daab079e3c Merge branch 'pam' of github.com:jumpserver/lina into pam 2025-01-06 11:27:55 +08:00
ibuler
a32f818abd perf: update pam 2025-01-06 11:27:47 +08:00
feng
d8980e66e3 perf: Change secret check_conn_after_change 2025-01-06 10:53:37 +08:00
zhaojisen
153c29fbcc Perf: Add Filer Icon 2025-01-03 18:45:47 +08:00
ibuler
feb3dbabc2 perf: 优化折叠 2025-01-03 18:37:20 +08:00
ibuler
0ac1e0d75a perf: 优化 form 折叠 2025-01-03 18:32:24 +08:00
ibuler
572e61046f Merge branch 'pam' of github.com:jumpserver/lina into pam 2025-01-03 15:47:08 +08:00
ibuler
9f47b50dac perf: update icon 2025-01-03 15:46:53 +08:00
zhaojisen
5022ca9075 Perf: Remove Unused Tips and Function 2025-01-03 14:51:06 +08:00
fit2bot
fee2123b49 Perf: Add Account Change Or Move To Different Asset (#4567)
* Perf: Add Account Change Or Move To Different Asset

* Fixed: Fixed Add Account And Template input disabled

* Perf: Add Account Operation Bulk Result

---------

Co-authored-by: zhaojisen <1301338853@qq.com>
2025-01-03 11:05:49 +08:00
ibuler
78e6d4e367 Merge branch 'pam' of github.com:jumpserver/lina into pam 2025-01-02 19:21:45 +08:00
ibuler
a5b8d51747 perf: update account create 2025-01-02 19:21:31 +08:00
w940853815
e9245dfd19 perf: Add redirect and permissions to AccountCheck, update formatter 2025-01-02 18:20:38 +08:00
ibuler
f605192fe1 perf: update create drawer 2025-01-02 17:19:52 +08:00
ibuler
504adad9fe perf: 优化弹窗 2025-01-02 11:27:06 +08:00
ibuler
cd82d8ee92 Merge branch 'pam' of github.com:jumpserver/lina into pam 2025-01-02 09:47:17 +08:00
feng626
d97dbc612c Revert "perf: drawer el-radio-group css"
This reverts commit 368bad6771.
2024-12-31 16:53:24 +08:00
feng
368bad6771 perf: drawer el-radio-group css 2024-12-31 16:39:58 +08:00
ibuler
41cc618a49 perf: 修改更新 2024-12-31 16:31:10 +08:00
w940853815
4f0f1b372e perf: Account discover use drawer component 2024-12-31 16:03:45 +08:00
ibuler
acfac6d45f perf: form 上下结构 2024-12-31 15:48:23 +08:00
ibuler
1a3f483654 Merge branch 'pam' of github.com:jumpserver/lina into pam 2024-12-31 14:23:07 +08:00
ibuler
559177ea32 perf: 优化 detail 2024-12-31 14:22:50 +08:00
zhaojisen
c9f9264c87 Perf: Remove different jump paths for different types of hosts 2024-12-30 18:47:05 +08:00
ibuler
8fabd32426 perf: update detail card 2024-12-30 18:01:13 +08:00
ibuler
6a34bb426c Merge branch 'pam' of github.com:jumpserver/lina into pam 2024-12-30 17:29:08 +08:00
ibuler
a1edc55ca0 perf: 修改详情布局 2024-12-30 17:29:01 +08:00
zhaojisen
d1366f161d Perf: Connect when select custom protocol 2024-12-30 16:57:07 +08:00
zhaojisen
b58754b6c5 Perf: Add Connect Protocol 2024-12-30 16:22:14 +08:00
feng
ee3e68f3ac perf: Change secret push account add drwaer 2024-12-30 15:21:59 +08:00
ibuler
3a0af18f74 Merge branch 'pam' of github.com:jumpserver/lina into pam 2024-12-27 19:09:04 +08:00
ibuler
24f30ab198 perf: update detail 2024-12-27 19:08:51 +08:00
w940853815
72b9447b12 fix: Update query param names in AssetCreateUpdate and BaseList 2024-12-27 18:08:21 +08:00
zhaojisen
7e24454743 Perf: Change Connect Params 2024-12-27 17:22:14 +08:00
ibuler
48d81dbd19 perf: update create form 2024-12-27 17:20:59 +08:00
ibuler
cdf0a9eed1 Merge branch 'pam' of github.com:jumpserver/lina into pam 2024-12-27 11:04:17 +08:00
ibuler
59cf22a421 perf: 优化 drawer 2024-12-27 11:04:03 +08:00
w940853815
41a34fb2d7 fix: View account discover execution detail 2024-12-27 10:41:41 +08:00
ibuler
4164ea4086 Merge branch 'pam' of github.com:jumpserver/lina into pam 2024-12-26 19:09:17 +08:00
ibuler
a2db27163e perf: 修改 col 宽度 2024-12-26 19:09:05 +08:00
w940853815
5a0b4ec6aa fix: Update discover AccountDetail api url 2024-12-26 19:08:28 +08:00
ibuler
4cfb1a6f37 perf: update account create 2024-12-26 17:18:04 +08:00
ibuler
e2f7fd0d77 perf: update using drawer 2024-12-26 14:34:54 +08:00
ibuler
80d530a2ed perf: update drawer create 2024-12-24 19:20:30 +08:00
ibuler
a1a06e1cbf Merge branch 'pam' of github.com:jumpserver/lina into pam 2024-12-24 18:40:23 +08:00
ibuler
6bec731396 perf: update pam 2024-12-24 18:37:58 +08:00
w940853815
ac40823319 perf: Update status action to handle multiple accounts 2024-12-24 18:36:27 +08:00
zhaojisen
ef063898ca Perf: Add Windows GUI connect 2024-12-24 16:07:48 +08:00
ibuler
b27a801b04 Merge branch 'pam' of github.com:jumpserver/lina into pam 2024-12-24 15:35:52 +08:00
ibuler
4bcee2fc2d perf: update draw create 2024-12-24 15:35:39 +08:00
feng
b0672bb543 perf: Push account 2024-12-24 10:22:52 +08:00
ibuler
7b0ea02ad4 perf: update asset create 2024-12-23 19:25:52 +08:00
ibuler
eaf7ba5f43 Merge branch 'pam' of github.com:jumpserver/lina into pam 2024-12-20 18:53:50 +08:00
ibuler
28b4aa0d55 perf: update asset create 2024-12-20 18:53:35 +08:00
wangruidong
d9ec7917f3 perf: Delete gather_account 2024-12-20 18:53:02 +08:00
feng
ac6040177a perf: Account push list 2024-12-20 11:03:57 +08:00
ibuler
48e4027525 perf: update draw 2024-12-19 11:07:47 +08:00
ibuler
2955b4800f perf: update asset create 2024-12-18 19:46:57 +08:00
ibuler
395b204da3 perf: update action 2024-12-18 11:41:42 +08:00
ibuler
71ebd6bf56 Merge branch 'pam_new_draw' into pam 2024-12-16 17:59:55 +08:00
ibuler
0494ce7f5b Merge branch 'pam' of github.com:jumpserver/lina into pam 2024-12-16 17:59:40 +08:00
ibuler
fdf869cd46 perf: update pam 2024-12-16 17:59:32 +08:00
ibuler
7dfca604c2 perf: update draw create update 2024-12-16 14:41:52 +08:00
ibuler
b4abcd4c90 perf: update draw 2024-12-12 19:03:03 +08:00
wangruidong
70777f8335 perf: Add AccountDetail page 2024-12-12 16:01:31 +08:00
ibuler
d62c87b858 Merge branch 'pam' of github.com:jumpserver/lina into pam 2024-12-12 09:44:28 +08:00
wangruidong
743187b4b3 fix: risk check perms 2024-12-11 16:36:38 +08:00
ibuler
5c1f24af6a perf: update template 2024-12-11 11:39:27 +08:00
ibuler
9477bfa2c1 Merge branch 'pam' of github.com:jumpserver/lina into pam 2024-12-11 10:09:12 +08:00
ibuler
1ba58f476f perf: update async 2024-12-11 10:09:07 +08:00
feng
02600d7a1b perf: Remove push account extra api 2024-12-10 19:36:06 +08:00
ibuler
2126c92e07 perf: update status 2024-12-10 16:51:22 +08:00
ibuler
af774a8835 Merge branch 'pam' of github.com:jumpserver/lina into pam 2024-12-10 15:46:44 +08:00
ibuler
b93fad1f91 perf: update check 2024-12-10 15:46:38 +08:00
zhaojisen
ef43be0cb7 perf: Add drawer 2024-12-09 18:52:46 +08:00
ibuler
8b6eea0267 Merge branch 'pam' of github.com:jumpserver/lina into pam 2024-12-09 17:10:36 +08:00
ibuler
d0da22738f perf: update handler 2024-12-09 17:10:30 +08:00
wangruidong
18eda16851 perf: delete gather account 2024-12-09 16:17:39 +08:00
feng
610e9b9efa perf: Account push 2024-12-09 11:19:18 +08:00
ibuler
a0c7d60719 perf: update table actions 2024-12-09 09:38:49 +08:00
zhaojisen
60ba0d8f02 perf: drawer realize 2024-12-06 19:06:09 +08:00
ibuler
b9afb05f1b perf: update risk batch selected 2024-12-06 19:00:41 +08:00
ibuler
fcf9ea2b79 Merge branch 'pam' of github.com:jumpserver/lina into pam 2024-12-06 11:38:40 +08:00
ibuler
430b1117c9 perf: update quick filter style 2024-12-06 11:38:27 +08:00
zhaojisen
a65023c8f7 style: Optimized style 2024-12-06 11:36:20 +08:00
zhaojisen
d6de85ffdd perf:Universal Drawer action assembly 2024-12-05 19:35:34 +08:00
ibuler
5f11d8b54f Merge branch 'pam' of github.com:jumpserver/lina into pam 2024-12-05 19:10:16 +08:00
ibuler
c7ce602d4c perf: update filter 2024-12-05 19:09:52 +08:00
zhaojisen
d0988da277 perf:Universal Drawer assembly 2024-12-05 18:10:40 +08:00
ibuler
7f13ef35a7 perf: update appliations 2024-12-05 16:07:58 +08:00
ibuler
44348de4ab perf: update card table 2024-12-05 11:15:35 +08:00
ibuler
be82fe1bde Merge branch 'pam' of github.com:jumpserver/lina into pam 2024-12-04 18:34:42 +08:00
ibuler
2e472dad93 perf: update name space 2024-12-04 18:34:10 +08:00
feng
bbfb237f23 perf: Account backup report 2024-12-04 16:30:19 +08:00
feng
390224613a perf: Change secret report 2024-12-02 19:10:29 +08:00
zhaojisen
7e0c677ad3 perf: Remove back icon 2024-12-02 17:23:04 +08:00
zhaojisen
9936f2b806 perf: Added pam asset drawer 2024-12-02 17:16:46 +08:00
zhaojisen
bfde8bd28b style: Adjust style 2024-12-02 16:07:24 +08:00
ibuler
4b309d950c Merge branch 'pam' of github.com:jumpserver/lina into pam 2024-12-02 15:20:41 +08:00
zhaojisen
688f06ebac style: Adjust style 2024-12-02 14:26:02 +08:00
ibuler
6fb3b552eb perf: pam update 2024-12-02 14:24:28 +08:00
feng
f27e7bdad4 perf: Change secret record table dashboard 2024-12-02 11:37:49 +08:00
fit2bot
2ee139c92b perf: Change secret dashboard (#4473)
* perf: Change secret dashboard

* style: Modify layout

---------

Co-authored-by: feng <1304903146@qq.com>
Co-authored-by: zhaojisen <1301338853@qq.com>
2024-12-02 10:33:46 +08:00
fit2bot
7a6c156aaa feat: PAM Service (#4474)
* feat: PAM Service

* perf: Remove useless

* perf: Add go module download value

---------

Co-authored-by: jiangweidong <1053570670@qq.com>
2024-12-02 10:33:13 +08:00
zhaojisen
11d40b4be1 perf: Add pam template 2024-11-28 18:05:04 +08:00
wangruidong
b772580f99 feat: Add Account Activity List 2024-11-28 17:02:11 +08:00
zhaojisen
fafdb6be5b perf: Basic drawer show 2024-11-28 10:48:37 +08:00
ibuler
2f984530e3 perf: update handler 2024-11-27 19:38:58 +08:00
ibuler
68b39bbc3d Merge branch 'pam' of github.com:jumpserver/lina into pam 2024-11-25 17:16:42 +08:00
ibuler
d21dc57305 perf: update pam 2024-11-25 17:16:09 +08:00
zhaojisen
c58d826898 fixed: Change path naming method 2024-11-22 15:00:11 +08:00
zhaojisen
7e80361635 perf: Add pam connect 2024-11-22 15:00:11 +08:00
ibuler
b25d2016a6 perf: using checkbox replace switch 2024-11-18 14:35:43 +08:00
ibuler
636630fe57 perf: update pam 2024-11-14 19:01:25 +08:00
ibuler
e18726efc2 perf: update accout check 2024-11-13 18:46:24 +08:00
ibuler
823c26aa5e Merge branch 'pam' of github.com:jumpserver/lina into pam 2024-11-13 11:03:05 +08:00
ibuler
c0c9d56408 perf: 修改 pam 2024-11-13 11:02:04 +08:00
zhaojisen
ff4adde897 perf: Add pam connect 2024-11-11 18:38:01 +08:00
ibuler
45ae7cab21 perf: 阶段完成页面 2024-11-06 16:40:35 +08:00
ibuler
429f5aed90 perf: change table 2024-11-05 17:20:46 +08:00
ibuler
d8999ffc06 perf: update icon 2024-11-05 17:08:37 +08:00
ibuler
fd745f0a26 perf: update account risk list 2024-11-04 18:41:20 +08:00
ibuler
322d12f27f perf: update accout check 2024-11-01 19:06:39 +08:00
ibuler
a1bc8ac5bc perf: update accout check 2024-11-01 18:04:53 +08:00
ibuler
9271cb2e1a perf: 更新账号发现 2024-11-01 16:38:21 +08:00
ibuler
0e7c682f72 perf: update gather account 2024-10-29 19:23:29 +08:00
ibuler
c0b4029917 perf: add account status action 2024-10-28 18:57:03 +08:00
ibuler
b1acb62889 perf: update table action 2024-10-25 10:55:35 +08:00
ibuler
26fd9b1813 perf: 更新 quick filter 2024-10-24 14:42:39 +08:00
ibuler
4fabdfdc5f perf: 完善过滤 2024-10-23 18:52:50 +08:00
ibuler
6e894c31a1 perf: update route 2024-10-22 17:30:30 +08:00
ibuler
d8a6fd96ce perf: support options 2024-10-21 17:02:22 +08:00
ibuler
7c5c5f966d perf: asset add account info 2024-10-18 15:21:58 +08:00
ibuler
8b25fd198e perf: change account 2024-10-16 18:48:37 +08:00
ibuler
762fa4c17e perf: 完成一些快速筛选 2024-10-15 18:26:53 +08:00
ibuler
73cc319e7b perf: table action flex 2024-10-15 15:15:46 +08:00
ibuler
d90aba37cf perf: update loader 2024-10-15 13:58:56 +08:00
ibuler
d775ffa501 perf: format 2024-10-14 18:10:07 +08:00
ibuler
e8cf8347e9 perf: 基本完成框架 2024-10-14 14:49:20 +08:00
ibuler
7ff1da71d4 perf: updat asset list and account list action 2024-10-11 19:22:39 +08:00
ibuler
a23a0d0197 perf: update perm 2024-10-11 17:59:00 +08:00
ibuler
1477712c78 perf: 修改 Pam 2024-09-19 09:57:54 +08:00
ibuler
77a0100add perf: add pam panel 2024-09-12 18:56:34 +08:00
ibuler
833e44024f perf: change pkg deps 2024-09-12 11:33:33 +08:00
ibuler
9b7c4ed353 chore: lock the pull request when workflow start 2024-09-12 10:22:01 +08:00
wangruidong
41f841532f feat: LDAP HA 2024-09-11 18:26:49 +08:00
feng
0b9f47dd84 feat: Postgresql support ssl 2024-09-11 18:12:33 +08:00
wangruidong
d32a376e8c feat: Support playbook, adhoc share 2024-09-11 18:02:03 +08:00
ibuler
0f8a8845df perf: change gateway gateway tip 2024-09-11 15:26:57 +08:00
wangruidong
8bb2c66b99 perf: Add task description 2024-09-09 18:59:25 +08:00
ibuler
b3d0be2f60 perf: update platform icon 2024-09-06 11:15:07 +08:00
ibuler
5a94ddd976 perf: api can set radio and checkbox tip 2024-09-06 11:14:03 +08:00
ibuler
7b9627a80b perf: support change gateway platform 2024-09-06 11:10:46 +08:00
feng
5fcfecc060 perf: Acl action add notify and warn 2024-09-06 11:08:21 +08:00
wangruidong
f4d7a2283c feat: Add announcement start and end dates 2024-09-06 10:56:03 +08:00
feng
40bb8410d3 perf: System settings task cleanup add change secret and push record retention days 2024-09-02 10:18:13 +08:00
fit2bot
45344ac620 perf: ldap import user error msg (#4333)
* perf: ldap import user error msg

* fix: Duplication on new code

---------

Co-authored-by: wangruidong <940853815@qq.com>
2024-08-30 17:01:18 +08:00
feng
5b894c9667 perf: View the internal message and convert the content into markdown 2024-08-29 17:28:18 +08:00
ZhaoJiSen
d89bd15b6d Merge pull request #4337 from jumpserver/pr@dev@fix_table_reload
fixed: Fixed an issue where tables could not be refreshed
2024-08-29 13:06:40 +08:00
zhaojisen
5e640dd45c fixed: Fixed an issue where tables could not be refreshed 2024-08-29 11:50:23 +08:00
fit2bot
09aa750794 chore: update checkout action 2024-08-28 11:34:09 +08:00
吴小白
744b215800 feat: bump node from 16.20 to 20.15 2024-08-27 11:07:37 +08:00
ibuler
f4e11da053 perf: revert pre org if logout 2024-08-23 15:40:16 +08:00
ibuler
2e6b5706d5 perf: remove tab in query 2024-08-21 18:04:19 +08:00
feng
440a5b27ef perf: Asset authorization request asset optimization mini limit 300 2024-08-21 11:34:17 +08:00
feng
932e16844e perf: Role detail title translate 2024-08-20 11:29:02 +08:00
feng
aff2e439dd perf: Ztree cannot be dragged under global organization 2024-08-20 10:45:21 +08:00
ZhaoJiSen
a618794c14 Merge pull request #4319 from jumpserver/pr@dev@fix_assets_tree
fixed: asset tree height
2024-08-15 17:51:17 +08:00
zhaojisen
8dd66c400a fixed: asset tree height 2024-08-15 17:49:14 +08:00
ZhaoJiSen
ac3ce464b7 Merge pull request #4318 from jumpserver/pr@dev@fix_aoumnt_formatter
fixed: Fixed an issue where the details list data would not refresh after the data was updated
2024-08-15 17:43:07 +08:00
zhaojisen
4d01c6dabe fixed: Fixed an issue where the details list data would not refresh after the data was updated 2024-08-15 17:39:59 +08:00
ZhaoJiSen
47eeac23eb Merge pull request #4317 from jumpserver/pr@dev@ticket_flow
perf: Ticket flow detail error
2024-08-15 16:42:58 +08:00
feng
592f783245 perf: Ticket flow detail error 2024-08-15 16:42:01 +08:00
Ewall555
c34ac6a56f perf: Translate ticket cancel button 2024-08-15 15:50:36 +08:00
Bai
9a7162cc9e perf: i18n for ldap user import 2024-08-15 15:49:25 +08:00
ZhaoJiSen
5edf2f34dd Merge pull request #4313 from jumpserver/pr@dev@fix_cloud_button
fixed: Fixed the issue that can be edited and deleted under the default cloud synchronization policy
2024-08-15 12:07:42 +08:00
zhaojisen
859d824195 fixed: Fixed the issue that can be edited and deleted under the default cloud synchronization policy 2024-08-15 12:06:36 +08:00
ZhaoJiSen
c382cc633e Merge pull request #4312 from jumpserver/pr@dev@fix_quick_job
fixed: Fixed the issue that the shortcut command could not get the account
2024-08-15 11:06:21 +08:00
zhaojisen
653e24773f fixed: Fixed the issue that the shortcut command could not get the account 2024-08-15 10:59:13 +08:00
wangruidong
ae489610ee perf: Translate batch approval 2024-08-14 19:28:11 +08:00
ZhaoJiSen
b5e85c66e4 Merge pull request #4311 from jumpserver/pr@dev@fix_icon_show
fixed: common icon
2024-08-14 19:01:49 +08:00
zhaojisen
4060fa4c4a fixed: common icon 2024-08-14 18:59:13 +08:00
ZhaoJiSen
9630b79daa Merge pull request #4310 from jumpserver/pr@dev@fix_common_icon
fixed: common icon
2024-08-14 18:55:52 +08:00
zhaojisen
ad93917387 fixed: common icon 2024-08-14 18:52:40 +08:00
feng
691590da7b perf: Cloud sync task interval to number 2024-08-14 18:52:19 +08:00
feng
71828fd78b perf: Translate 2024-08-14 16:38:04 +08:00
ZhaoJiSen
5a45f31ac4 Merge pull request #4305 from jumpserver/pr@dev@fix_ticket_tree
fixed: Fixed missing action book nodes in ticket details
2024-08-14 16:32:37 +08:00
zhaojisen
44901eee23 fixed: Fixed missing action book nodes in ticket details 2024-08-14 16:30:49 +08:00
ZhaoJiSen
2d91fa107d Merge pull request #4304 from jumpserver/pr@dev@fix_storage_textarea
fixed:Fixed the common item in the storage Settings to be a text field
2024-08-14 15:48:20 +08:00
zhaojisen
7a6ade4bd9 fixed:Fixed the common item in the storage Settings to be a text field 2024-08-14 15:46:31 +08:00
wangruidong
951778668f perf: profile page go to some page error 2024-08-13 18:16:04 +08:00
zhaojisen
0aca23bf38 fixed: internationalization issues 2024-08-13 17:50:49 +08:00
wangruidong
9b97dc9a74 perf: add auto generate ssh-key 2024-08-13 17:43:47 +08:00
feng
32cd030479 fix: Cloud sync task regions display 2024-08-09 17:40:50 +08:00
wangruidong
607bb476db perf: asset name edit add icon 2024-08-09 16:33:58 +08:00
feng
1d676ec9b7 perf: Domain detail gateway translate 2024-08-09 10:45:52 +08:00
wangruidong
614ebb4121 fix: platform page asset not allow go to detail page 2024-08-08 17:34:42 +08:00
ZhaoJiSen
8c2719a95d Merge pull request #4291 from jumpserver/pr@dev@fix_load
perf: add button loading status
2024-08-08 15:00:45 +08:00
zhaojisen
c6b0a958a6 perf: add button loading status 2024-08-08 14:53:42 +08:00
ZhaoJiSen
3edebf3f4f Merge pull request #4290 from jumpserver/pr@dev@fix_loading
perf: add button loading status
2024-08-08 14:47:26 +08:00
zhaojisen
f63405978e perf: add button loading status 2024-08-08 14:46:39 +08:00
fit2bot
c582c8de98 chore: remove build test 2024-08-08 13:50:57 +08:00
fit2bot
c44e79ed3b perf: add button loading status (#4285)
* perf: add button loading status

* perf: add button loading status

* perf: add button loading status

---------

Co-authored-by: zhaojisen <1301338853@qq.com>
2024-08-08 11:37:38 +08:00
feng
fc7d5b9c29 perf: Asset authorization: The number of accounts displayed is incorrect ignore @SPEC 2024-08-07 17:51:15 +08:00
Eric
335febd4a5 perf: update session page i18n 2024-08-07 17:45:52 +08:00
wangruidong
5ea2918fe7 feat: Allow users to customize asset name and comment 2024-08-07 16:47:54 +08:00
ZhaoJiSen
7668d10ba5 Merge pull request #4282 from jumpserver/pr@dev@fix_continue_add
fixed: Restore continues to add the ability to clear the form
2024-08-06 14:20:53 +08:00
zhaojisen
df15d141da fixed: Restore continues to add the ability to clear the form 2024-08-06 14:14:37 +08:00
wangruidong
e2f4bbde79 feat: support configuring multiple SSH keys for users 2024-08-05 15:21:14 +08:00
feng
3ca7de42af perf: Optimization settings auth org select2 2024-08-02 17:51:25 +08:00
Bai
e6a577ba9b perf: assets internal display for false 2024-08-02 16:04:59 +08:00
feng
6290179460 perf: Third-party user login settings default organization 2024-08-02 15:52:42 +08:00
ZhaoJiSen
4f3c9e9353 Merge pull request #4276 from jumpserver/pr@dev@fix_add_continue
fixed: Fixed the issue when adding
2024-08-01 14:57:54 +08:00
zhaojisen
aa28a8f765 fixed: Fixed the issue when adding 2024-08-01 14:57:01 +08:00
ZhaoJiSen
98d0a52fa2 Merge pull request #4275 from jumpserver/pr@dev@fix_filter_param
fixed: Filtering parameters
2024-07-31 14:30:54 +08:00
zhaojisen
8d9e1ffadb fixed: Filtering parameters 2024-07-31 14:29:49 +08:00
zhaojisen
b3be312a4d fixed: Fixed page refresh when tree nodes are clicked 2024-07-31 11:23:07 +08:00
feng
7c41d148aa perf: Support WeCom DingTalk FeiShu Lark Slack attribute mapping 2024-07-30 17:51:55 +08:00
feng
6dee911642 perf: Approval process role selection supports multiple strategies 2024-07-30 16:37:32 +08:00
wangruidong
6dd35e5173 feat: add assets amount field to platform page 2024-07-26 13:44:22 +08:00
halo
fdaa33f68d perf: Email service authentication username is optional 2024-07-26 11:39:31 +08:00
feng
0b813bb719 perf: Del profile scheduled request 2024-07-25 18:22:26 +08:00
fit2bot
7ba6b8d4e4 perf: fix session i18n default (#4258)
* perf: fix session i18n default

* perf: Update Dockerfile with new base image tag

---------

Co-authored-by: Bai <baijiangjie@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Bryan <jiangjie.bai@fit2cloud.com>
2024-07-24 10:03:24 +08:00
ibuler
ef99e25fde perf: workflow 2024-07-23 17:12:37 +08:00
github-actions[bot]
d1e7b907e3 perf: Update Dockerfile with new base image tag 2024-07-23 16:52:36 +08:00
ibuler
83f7dda5e7 perf: change base image 2024-07-23 16:52:36 +08:00
ibuler
80f929fdea perf: change build
perf: using cache

perf: using pnpm
2024-07-23 16:13:36 +08:00
feng
1ec3d02933 perf: You can modify sudo permissions multiple times 2024-07-22 18:17:50 +08:00
ZhaoJiSen
fdf148cc2b Merge pull request #4250 from jumpserver/pr@dev@fix_validation_refresh
fixed: Fixed an issue where the validation date does not refresh
2024-07-22 14:57:28 +08:00
ZhaoJiSen
c9e6ef89dc Merge pull request #4249 from jumpserver/pr@dev@fix_add_continue
fixed: Fixed an issue where the data would not refresh after the Save and Continue Add button was successfully added
2024-07-22 14:57:03 +08:00
zhaojisen
59f9f88f8b fixed: Fixed an issue where the validation date does not refresh 2024-07-22 14:48:51 +08:00
zhaojisen
16417ae843 fixed: Fixed an issue where the data would not refresh after the Save and Continue Add button was successfully added 2024-07-22 10:31:54 +08:00
ZhaoJiSen
58bd0a17c8 Merge pull request #4246 from jumpserver/pr@dev@fix_asset_refresh
fixed: Fixed an issue with asset refresh when clicked
2024-07-18 12:36:55 +08:00
zhaojisen
c4a1eb6938 fixed: Fixed an issue with asset refresh when clicked 2024-07-18 12:33:28 +08:00
feng
b7d9031889 perf: Translate 2024-07-17 18:05:19 +08:00
feng
96b29a9dc2 perf: Feishu lark support attributes settings 2024-07-17 17:00:02 +08:00
zhaojisen
336e176639 style: Code Editor style change 2024-07-17 16:52:03 +08:00
zhuoyang
2df4a9d66d fix:vue.config.js 2024-07-17 16:11:17 +08:00
feng
45a102cff1 perf: Translate 2024-07-17 11:40:17 +08:00
ZhaoJiSen
57ebfa0812 Merge pull request #4235 from jumpserver/pr@dev@fix_editor_style
style: Editor style adjustments
2024-07-17 11:14:53 +08:00
zhaojisen
4e9dd57efe style: Editor style adjustments 2024-07-17 11:13:05 +08:00
ZhaoJiSen
3fcc4ca160 Merge pull request #4229 from jumpserver/pr@dev@fix_params_push
fixed: Fixed the issue that parameter push parameters could not be saved
2024-07-17 11:01:02 +08:00
feng626
958760811c Merge pull request #4230 from jumpserver/pr@dev@ticket
perf: Del ticket comment mistake date
2024-07-17 10:54:50 +08:00
feng
b60e0251c2 perf: Del ticket comment mistake date 2024-07-17 10:51:40 +08:00
zhaojisen
3db0ed756e fixed: Fixed the issue that parameter push parameters could not be saved 2024-07-17 10:51:15 +08:00
feng626
453c4b1e4e Merge pull request #4222 from jumpserver/pr@dev@download
perf: Downloading files does not trigger the beforeunload event
2024-07-16 12:35:32 +08:00
feng
bf4d8ce7a6 perf: Downloading files does not trigger the beforeunload event 2024-07-16 12:34:18 +08:00
wangruidong
34effdbe15 perf: 社区版移除magnus 2024-07-15 19:27:16 +08:00
zhaojisen
274db466f2 fixed:Fixed an issue where the user was unable to enter non-MD content 2024-07-15 19:09:05 +08:00
ZhaoJiSen
cbe697f9dc Merge pull request #4218 from jumpserver/pr@dev@fix_job_tooltip
fixed: Fixed the tooltip shortcut command interface issue
2024-07-15 18:31:28 +08:00
zhaojisen
fb7acb100e fixed: Fixed the tooltip shortcut command interface issue 2024-07-15 18:28:15 +08:00
feng626
ebb36847df Merge pull request #4213 from jumpserver/pr@dev@cloud_sync_install
perf: Cloud sync instance execution add trigger
2024-07-15 18:02:46 +08:00
feng
beba4f1994 perf: Cloud sync instance execution add trigger 2024-07-11 17:15:58 +08:00
ZhaoJiSen
452796e3f5 Merge pull request #4212 from jumpserver/pr@dev@fix_params_remain
fixed: Fixed an issue where push parameters could not be saved when adding an asset account
2024-07-11 15:34:42 +08:00
zhaojisen
02a7969d90 fixed: Fixed an issue where push parameters could not be saved when adding an asset account 2024-07-11 15:32:19 +08:00
zhaojisen
798dbec151 fixed :Event bus shutdown 2024-07-11 15:20:51 +08:00
ibuler
0e11a56e37 perf: table search two times, one init one search 2024-07-11 15:20:01 +08:00
ibuler
be5344344c perf: view cache 2024-07-11 15:18:41 +08:00
ibuler
e75d711e0a perf: nest field change may be lead blink 2024-07-11 15:17:07 +08:00
wangruidong
46ee116f3e fix: role display error when update user 2024-07-11 15:07:29 +08:00
ZhaoJiSen
f4b304338f Merge pull request #4207 from jumpserver/pr@dev@fix_switch_showTooltip
fixed: Fixed tooltip issue with switch button
2024-07-11 10:48:46 +08:00
zhaojisen
e93e78307c fixed: Fixed tooltip issue with switch button 2024-07-11 10:47:33 +08:00
wangruidong
d22079446f perf: profile improvement mfa disabled 2024-07-10 14:21:06 +08:00
ibuler
d57b99b00c perf: phone code api 2024-07-09 19:24:07 +08:00
ZhaoJiSen
e7321356af Merge pull request #4197 from jumpserver/pr@dev@fix_card_style
style: Fine-tuning styles
2024-07-09 18:58:39 +08:00
zhaojisen
a0edc2c527 style: Fine-tuning styles 2024-07-09 18:54:45 +08:00
ibuler
10ebcfd64d perf: tags display inline 2024-07-09 16:08:55 +08:00
ibuler
a3fedb9697 perf: license display for community edition 2024-07-09 16:03:18 +08:00
吴小白
1eb8a18c66 fix: FromAsCasing keywords 2024-07-09 16:02:18 +08:00
wangruidong
f491c57c34 perf: display newly invited users at the top of the list 2024-07-09 15:54:31 +08:00
ZhaoJiSen
50b7f54652 Merge pull request #4193 from jumpserver/pr@dev@fix_menu_heighlight
fixed: Missing menu highlight in command group detail page
2024-07-09 11:12:00 +08:00
zhaojisen
5bf298a5bf fixed: Missing menu highlight in command group detail page 2024-07-09 11:07:01 +08:00
wangruidong
124ff9a8c2 fix: foot content allow blank 2024-07-08 19:24:34 +08:00
ZhaoJiSen
387ab4f1b3 Merge pull request #4186 from jumpserver/pr@dev@fix_item_empty
fixed: Fixed an issue where the field is empty
2024-07-08 18:55:00 +08:00
zhaojisen
d6fbdfa7ea fixed: Fixed an issue where the field is empty 2024-07-08 18:51:10 +08:00
feng626
cd4260fd8d Merge pull request #4184 from jumpserver/pr@dev@translate
perf: Translate
2024-07-05 16:37:29 +08:00
feng
baeece62d3 perf: Translate 2024-07-05 16:36:24 +08:00
wangruidong
576c8f5891 fix: update platform with update protocols 2024-07-05 15:07:10 +08:00
feng626
5e97600807 Merge pull request #4182 from jumpserver/pr@dev@translate
perf: Translate
2024-07-04 18:15:04 +08:00
feng
4e2eb3a37d perf: Translate 2024-07-04 18:13:49 +08:00
feng626
e92173f8e8 Merge pull request #4181 from jumpserver/pr@dev@applet
perf: Virtualapp and appprovider tab perm
2024-07-04 16:46:45 +08:00
feng
412d9c804e perf: Virtualapp and appprovider tab perm 2024-07-04 16:46:00 +08:00
ZhaoJiSen
b84a725d4a Merge pull request #4179 from jumpserver/pr@dev@fix_split_job
perf: split job management
2024-07-04 16:37:39 +08:00
feng626
58b39743e0 Merge pull request #4180 from jumpserver/pr@dev@apple
perf: Apple host detail perm
2024-07-04 16:33:39 +08:00
feng
7651443c25 perf: Apple host detail perm 2024-07-04 16:32:20 +08:00
zhaojisen
8d64e331d1 perf: split job management 2024-07-04 16:04:26 +08:00
490 changed files with 16556 additions and 5233 deletions

72
.github/workflows/build-base-image.yml vendored Normal file
View File

@@ -0,0 +1,72 @@
name: Build and Push Base Image
on:
push:
branches:
- 'pr*'
paths:
- 'package.json'
- 'package-lock.json'
- 'yarn.lock'
- 'Dockerfile-base'
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Lock Pull Request
run: |
curl -X POST -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-d '{"state":"pending", "description":"Action running, merge disabled", "context":"Lock PR"}' \
"https://api.github.com/repos/${{ github.repository }}/statuses/${{ github.sha }}"
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract date
id: vars
run: echo "IMAGE_TAG=$(date +'%Y%m%d_%H%M%S')" >> $GITHUB_ENV
- name: Extract repository name
id: repo
run: echo "REPO=$(basename ${{ github.repository }})" >> $GITHUB_ENV
- name: Build and push multi-arch image
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64
push: true
file: Dockerfile-base
tags: jumpserver/${{ env.REPO }}-base:${{ env.IMAGE_TAG }}
- name: Update Dockerfile
run: |
sed -i 's|-base:.* AS stage-build|-base:${{ env.IMAGE_TAG }} AS stage-build|' Dockerfile
- name: Commit changes
run: |
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
git add Dockerfile
git commit -m "perf: Update Dockerfile with new base image tag"
git push
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Unlock Pull Request
run: |
curl -X POST -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-d '{"state":"success", "description":"Action running, merge disabled", "context":"Lock PR"}' \
"https://api.github.com/repos/${{ github.repository }}/statuses/${{ github.sha }}"

View File

@@ -1,46 +0,0 @@
name: "Run Build Test"
on:
push:
paths:
- 'Dockerfile'
- 'Dockerfile*'
- 'package.json'
- 'yarn.lock'
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
component: [lina]
version: [v4]
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- name: Prepare Build
run: |
sed -i 's@registry.npmmirror.com@registry.yarnpkg.com@g' yarn.lock
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Image
uses: docker/build-push-action@v5
with:
context: .
push: true
file: Dockerfile
tags: ghcr.io/jumpserver/${{ matrix.component }}:${{ matrix.version }}
platforms: linux/amd64
build-args: |
VERSION=${{ matrix.version }}
APT_MIRROR=http://deb.debian.org
NPM_REGISTRY=https://registry.yarnpkg.com
outputs: type=image,oci-mediatypes=true,compression=zstd,compression-level=3,force-compression=true
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -0,0 +1,46 @@
name: "Run Build Test"
on:
push:
paths:
- 'Dockerfile'
- 'Dockerfile*'
- 'package.json'
- 'yarn.lock'
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
component: [ lina ]
version: [ v4 ]
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- name: Prepare Build
run: |
sed -i 's@registry.npmmirror.com@registry.yarnpkg.com@g' yarn.lock
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Image
uses: docker/build-push-action@v5
with:
context: .
push: true
file: Dockerfile
tags: ghcr.io/jumpserver/${{ matrix.component }}:${{ matrix.version }}
platforms: linux/amd64
build-args: |
VERSION=${{ matrix.version }}
APT_MIRROR=http://deb.debian.org
NPM_REGISTRY=https://registry.yarnpkg.com
outputs: type=image,oci-mediatypes=true,compression=zstd,compression-level=3,force-compression=true
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -31,7 +31,7 @@ jobs:
tag: ${{ steps.get_version.outputs.TAG }}
- uses: actions/setup-node@v2
with:
node-version: '16.20'
node-version: '20.15'
- name: Install dependencies
run: yarn install
- name: Build web

2
.gitignore vendored
View File

@@ -17,3 +17,5 @@ tests/**/coverage/
*.sln
.env.development
.python-version
helper.json

View File

@@ -1,34 +1,4 @@
FROM node:16.20-bullseye-slim as stage-build
ARG TARGETARCH
ARG DEPENDENCIES=" \
g++ \
make \
python3"
ARG APT_MIRROR=http://mirrors.ustc.edu.cn
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
set -ex \
&& rm -f /etc/apt/apt.conf.d/docker-clean \
&& echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' >/etc/apt/apt.conf.d/keep-cache \
&& sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
&& apt-get update \
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
&& echo "no" | dpkg-reconfigure dash
ARG NPM_REGISTRY="https://registry.npmmirror.com"
RUN set -ex \
&& npm config set registry ${NPM_REGISTRY} \
&& yarn config set registry ${NPM_REGISTRY}
WORKDIR /data
RUN --mount=type=cache,target=/usr/local/share/.cache/yarn,sharing=locked \
--mount=type=bind,source=package.json,target=package.json \
--mount=type=bind,source=yarn.lock,target=yarn.lock \
yarn install
FROM jumpserver/lina-base:20240723_084702 AS stage-build
ARG VERSION
ENV VERSION=$VERSION

22
Dockerfile-base Normal file
View File

@@ -0,0 +1,22 @@
FROM node:20.15-bullseye-slim
ARG DEPENDENCIES=" \
g++ \
make \
python3"
RUN set -ex \
&& rm -f /etc/apt/apt.conf.d/docker-clean \
&& echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache \
&& apt-get update \
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
&& echo "no" | dpkg-reconfigure dash
WORKDIR /data
COPY package.json yarn.lock ./
ARG NPM_MIRROR="https://registry.npmjs.org"
RUN --mount=type=cache,target=/usr/local/share/.cache/yarn,sharing=locked,id=yarn-cache \
sed -i "s|https://registry.npmmirror.com|${NPM_MIRROR}|g" yarn.lock \
&& yarn install

View File

@@ -0,0 +1,10 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"prettier.printWidth": 100,
"prettier.singleAttributePerLine": true,
"editor.wordWrap": "on"
}

View File

@@ -5,5 +5,6 @@
"@/*": ["src/*"]
}
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -1,13 +1,13 @@
{
"name": "Lina",
"name": "lina",
"version": "v4.0.0",
"description": "JumpServer Web UI",
"author": "JumpServer Team <support@fit2cloud.com>",
"license": "GPL-3.0-or-later",
"scripts": {
"dev": "vue-cli-service serve",
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"dev": "NODE_OPTIONS=--openssl-legacy-provider vue-cli-service serve",
"serve": "NODE_OPTIONS=--openssl-legacy-provider vue-cli-service serve",
"build": "NODE_OPTIONS=--openssl-legacy-provider vue-cli-service build",
"build:prod": "vue-cli-service build",
"build:stage": "vue-cli-service build --mode staging",
"preview": "node build/index.js --preview",
@@ -30,11 +30,13 @@
"@ztree/ztree_v3": "3.5.44",
"axios": "0.28.0",
"axios-retry": "^3.1.9",
"caniuse-lite": "^1.0.30001642",
"cron-parser": "^4.0.0",
"crypto-js": "^4.1.1",
"css-color-function": "^1.3.3",
"decimal.js": "^10.4.3",
"deepmerge": "^4.2.2",
"dompurify": "^3.1.6",
"echarts": "4.7.0",
"element-ui": "2.15.14",
"eslint-plugin-html": "^6.0.0",
@@ -92,6 +94,7 @@
"@vue/cli-plugin-unit-jest": "3.6.3",
"@vue/cli-service": "3.6.0",
"@vue/test-utils": "1.0.0-beta.29",
"@vue/runtime-dom": "3.5.13",
"autoprefixer": "^9.5.1",
"babel-core": "7.0.0-bridge.0",
"babel-eslint": "10.0.1",
@@ -118,7 +121,7 @@
"serve-static": "^1.13.2",
"strip-ansi": "^7.1.0",
"svg-sprite-loader": "4.1.3",
"svgo": "1.2.2",
"svgo": "1.2.4",
"vue-i18n-extract": "^1.1.1",
"vue-template-compiler": "2.6.10"
},

View File

@@ -2,37 +2,70 @@
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta http-equiv="Expires" content="0">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Cache-control" content="no-cache">
<meta http-equiv="Cache" content="no-cache">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible">
<meta content="0" http-equiv="Expires">
<meta content="no-cache" http-equiv="Pragma">
<meta content="no-cache" http-equiv="Cache-control">
<meta content="no-cache" http-equiv="Cache">
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
<title><%= webpackConfig.name %></title>
<link rel="stylesheet" href="<%= BASE_URL %>theme/element-ui.css">
<link href="<%= BASE_URL %>theme/element-ui.css" rel="stylesheet">
<style>
#loading {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background-color: white;
z-index: 9999;
}
.spinner {
width: 50px;
height: 50px;
border: 5px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
border-top-color: #3498db;
animation: spin 1s infinite linear;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>
</head>
<body>
<noscript>
<strong>We're sorry but <%= webpackConfig.name %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<script>
window.onload = function() {
if (location.pathname === '/') {
location.pathname = '/ui/'
}
const pathname = window.location.pathname
if (pathname.startsWith('/core')) {
return
}
if(pathname.indexOf('/ui') === -1) {
window.location.href = window.location.origin + '/ui/#' + pathname
}
if (pathname.startsWith('/ui/#/chat')) {
window.location.href = window.location.origin + pathname
}
window.onload = function () {
if (location.pathname === '/') {
location.pathname = '/ui/'
}
const pathname = window.location.pathname
if (pathname.startsWith('/core')) {
return
}
if (pathname.indexOf('/ui') === -1) {
window.location.href = window.location.origin + '/ui/#' + pathname
}
if (pathname.startsWith('/ui/#/chat')) {
window.location.href = window.location.origin + pathname
}
}
</script>
<div id="app"></div>
<div id="app">
</div>
<div id="loading">
<div class="spinner"></div>
</div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@@ -65,10 +65,6 @@ export function logout() {
})
}
export function refreshSessionIdAge() {
return getProfile()
}
export default {
getProfile,
getUserList

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 466 B

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 961 B

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 673 B

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 916 B

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 278 B

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 940 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -5,6 +5,7 @@ import AutomationParamsForm from '@/views/assets/Platform/AutomationParamsSettin
export const accountFieldsMeta = (vm) => {
const defaultPrivilegedAccounts = ['root', 'administrator']
return {
assets: {
component: Select2,
@@ -27,6 +28,9 @@ export const accountFieldsMeta = (vm) => {
component: Select2,
rules: [Required],
el: {
get disabled() {
return vm.isDisabled
},
multiple: false,
ajax: {
url: '/api/v1/accounts/account-templates/',
@@ -43,6 +47,11 @@ export const accountFieldsMeta = (vm) => {
rules: [Required],
label: vm.$t('AccountPolicy'),
helpTip: vm.$t('AccountPolicyHelpText'),
el: {
get disabled() {
return vm.isDisabled
}
},
hidden: () => {
return vm.platform || vm.asset
}
@@ -50,6 +59,11 @@ export const accountFieldsMeta = (vm) => {
name: {
label: vm.$t('Name'),
rules: [RequiredChange],
el: {
get disabled() {
return vm.isDisabled
}
},
on: {
input: ([value], updateForm) => {
if (!vm.usernameChanged) {
@@ -69,7 +83,9 @@ export const accountFieldsMeta = (vm) => {
},
username: {
el: {
disabled: !!vm.account?.name
get disabled() {
return !!vm.account?.name || vm.isDisabled
}
},
on: {
input: ([value], updateForm) => {
@@ -88,6 +104,11 @@ export const accountFieldsMeta = (vm) => {
},
privileged: {
label: vm.$t('Privileged'),
el: {
get disabled() {
return vm.isDisabled
}
},
hidden: () => {
return vm.addTemplate
}
@@ -100,6 +121,11 @@ export const accountFieldsMeta = (vm) => {
el: {
multiple: false,
clearable: true,
disabled: {
get disabled() {
return vm.isDisabled
}
},
ajax: {
url: `/api/v1/accounts/accounts/su-from-accounts/?account=${vm.account?.id || ''}&asset=${vm.asset?.id || ''}`,
transformOption: (item) => {
@@ -110,6 +136,11 @@ export const accountFieldsMeta = (vm) => {
},
su_from_username: {
label: vm.$t('UserSwitchFrom'),
el: {
get disabled() {
return vm.isDisabled
}
},
hidden: (formValue) => {
return vm.platform || vm.asset || vm.addTemplate
}
@@ -117,6 +148,11 @@ export const accountFieldsMeta = (vm) => {
password: {
label: vm.$t('Password'),
component: UpdateToken,
el: {
get disabled() {
return vm.isDisabled
}
},
hidden: (formValue) => {
return formValue.secret_type !== 'password' || vm.addTemplate
}
@@ -124,33 +160,63 @@ export const accountFieldsMeta = (vm) => {
ssh_key: {
label: vm.$t('PrivateKey'),
component: UploadSecret,
el: {
get disabled() {
return vm.isDisabled
}
},
hidden: (formValue) => formValue.secret_type !== 'ssh_key' || vm.addTemplate
},
passphrase: {
label: vm.$t('Passphrase'),
component: UpdateToken,
el: {
get disabled() {
return vm.isDisabled
}
},
hidden: (formValue) => formValue.secret_type !== 'ssh_key' || vm.addTemplate
},
token: {
label: vm.$t('Token'),
component: UploadSecret,
el: {
get disabled() {
return vm.isDisabled
}
},
hidden: (formValue) => formValue.secret_type !== 'token' || vm.addTemplate
},
access_key: {
id: 'access_key',
label: vm.$t('AccessKey'),
component: UploadSecret,
el: {
get disabled() {
return vm.isDisabled
}
},
hidden: (formValue) => formValue.secret_type !== 'access_key' || vm.addTemplate
},
api_key: {
id: 'api_key',
label: vm.$t('ApiKey'),
component: UploadSecret,
el: {
get disabled() {
return vm.isDisabled
}
},
hidden: (formValue) => formValue.secret_type !== 'api_key' || vm.addTemplate
},
secret_type: {
type: 'radio-group',
options: [],
el: {
get disabled() {
return vm.isDisabled
}
},
hidden: () => {
return vm.addTemplate
}
@@ -163,7 +229,8 @@ export const accountFieldsMeta = (vm) => {
!automation.ansible_enabled ||
!vm.$hasPerm('accounts.push_account') ||
(formValue.secret_type === 'ssh_key' && vm.iPlatform.type.value === 'windows') ||
vm.addTemplate
vm.addTemplate ||
!formValue.secret_reset
}
},
params: {
@@ -184,12 +251,27 @@ export const accountFieldsMeta = (vm) => {
}
},
is_active: {
label: vm.$t('IsActive')
label: vm.$t('IsActive'),
el: {
get disabled() {
return vm.isDisabled
}
}
},
comment: {
label: vm.$t('Comment'),
hidden: () => {
return vm.addTemplate
el: {
get disabled() {
return vm.isDisabled
}
}
},
secret_reset: {
label: vm.$t('SecretReset'),
el: {
get disabled() {
return vm.isDisabled
}
}
}
}

View File

@@ -45,6 +45,7 @@ export default {
data() {
return {
loading: true,
isDisabled: false,
usernameChanged: false,
submitBtnText: this.$t('Confirm'),
iPlatform: {
@@ -61,11 +62,12 @@ export default {
form: Object.assign({ 'on_invalid': 'error' }, this.account || {}),
encryptedFields: ['secret'],
fields: [
[this.$t('AccountTemplate'), ['template']],
[this.$t('Basic'), ['assets', 'name', 'username', 'privileged', 'su_from', 'su_from_username']],
[this.$t('Basic'), ['name', 'username', 'privileged', 'su_from', 'su_from_username', 'template']],
[this.$t('Assets'), ['assets']],
[this.$t('Secret'), [
'secret_type', 'password', 'ssh_key', 'token',
'access_key', 'passphrase', 'api_key'
'access_key', 'passphrase', 'api_key',
'secret_reset'
]],
[this.$t('Other'), ['push_now', 'params', 'on_invalid', 'is_active', 'comment']]
],
@@ -73,6 +75,16 @@ export default {
hasSaveContinue: false
}
},
watch: {
'$route.query': {
handler(nv, ov) {
if (nv && (nv.flag === 'move' || nv.flag === 'copy')) {
this.isDisabled = true
}
},
immediate: true
}
},
async mounted() {
try {
await this.getPlatform()
@@ -157,16 +169,16 @@ export default {
<style lang='scss' scoped>
.account-add {
::v-deep .el-form-item {
margin-bottom: 5px;
//margin-bottom: 5px;
.help-block {
margin-bottom: 5px;
//margin-bottom: 5px;
}
}
::v-deep .form-group-header {
.hr-line-dashed {
margin: 5px 0;
//margin: 5px 0;
}
h3 {

View File

@@ -1,36 +1,38 @@
<template>
<Dialog
v-if="iVisible"
:close-on-click-modal="false"
:destroy-on-close="true"
:show-cancel="false"
:show-confirm="false"
<template v-if="iVisible">
<Drawer
:title="title"
:visible.sync="iVisible"
v-bind="$attrs"
width="900px"
v-on="$listeners"
:visible="iVisible"
class="drawer"
@close-drawer="handleCloseDrawer"
>
<AccountCreateUpdateForm
v-if="!loading"
ref="form"
:account="account"
:add-template="addTemplate"
:asset="asset"
@add="addAccount"
@edit="editAccount"
/>
</Dialog>
<Page :title="'null'">
<IBox class="content">
<AccountCreateUpdateForm
v-if="!loading"
ref="form"
:account="account"
:add-template="addTemplate"
:asset="asset"
@add="addAccount"
@edit="editAccount"
/>
</IBox>
</Page>
</Drawer>
</template>
<script>
import Dialog from '@/components/Dialog/index.vue'
import Drawer from '@/components/Drawer/index.vue'
import AccountCreateUpdateForm from '@/components/Apps/AccountCreateUpdateForm/index.vue'
import IBox from '@/components/IBox/index.vue'
import Page from '@/layout/components/Page/index.vue'
export default {
name: 'CreateAccountDialog',
components: {
Dialog,
IBox,
Drawer,
Page,
AccountCreateUpdateForm
},
props: {
@@ -111,11 +113,22 @@ export default {
},
editAccount(form) {
const data = { ...form }
this.$axios.patch(`/api/v1/accounts/accounts/${this.account.id}/`, data).then(() => {
this.iVisible = false
this.$emit('add', true)
this.$message.success(this.$tc('UpdateSuccessMsg'))
}).catch(error => this.setFieldError(error))
const flag = this.$route.query.flag
switch (flag) {
case 'copy':
this.handleAccountOperation(this.account.id, 'copy-to-assets', data)
break
case 'move':
this.handleAccountOperation(this.account.id, 'move-to-assets', data)
break
default:
this.$axios.patch(`/api/v1/accounts/accounts/${this.account.id}/`, data).then(() => {
this.iVisible = false
this.$emit('add', true)
this.$message.success(this.$tc('UpdateSuccessMsg'))
}).catch(error => this.setFieldError(error))
}
},
handleResult(resp, error) {
let bulkCreate = !this.asset
@@ -168,7 +181,29 @@ export default {
refsAutoDataForm.setFieldError(current, err)
}
}
},
handleCloseDrawer() {
this.iVisible = false
Reflect.deleteProperty(this.$route.query, 'flag')
},
handleAccountOperation(id, path, data) {
this.$axios.post(`/api/v1/accounts/accounts/${id}/${path}/`, data).then((res) => {
this.iVisible = false
this.$emit('add', true)
this.handleResult(res, null)
}).catch(error => this.handleResult(null, error))
}
}
}
</script>
<style lang="scss" scoped>
.drawer {
::v-deep .el-drawer__body {
.el-form {
margin-right: 30px;
}
}
}
</style>

View File

@@ -1,6 +1,12 @@
<template>
<div>
<ListTable ref="ListTable" :header-actions="headerActions" :table-config="tableConfig" />
<DrawerListTable
ref="ListTable"
:detail-drawer="detailDrawer"
:header-actions="headerActions"
:quick-filters="quickFilters"
:table-config="tableConfig"
/>
<ViewSecret
v-if="showViewSecretDialog"
:account="account"
@@ -16,22 +22,13 @@
<AccountCreateUpdate
v-if="showAddDialog"
:account="account"
:add-template="addTemplate"
:asset="iAsset"
:title="accountCreateUpdateTitle"
:visible.sync="showAddDialog"
@add="addAccountSuccess"
@bulk-create-done="showBulkCreateResult($event)"
/>
<AccountCreateUpdate
v-if="showAddTemplateDialog"
:account="account"
:add-template="true"
:asset="iAsset"
:title="accountCreateByTemplateTitle"
:visible.sync="showAddTemplateDialog"
@add="addAccountSuccess"
@bulk-create-done="showBulkCreateResult($event)"
/>
<ResultDialog
v-if="showResultDialog"
:result="createAccountResults"
@@ -44,29 +41,36 @@
v-bind="updateSelectedDialogSetting"
@update="handleAccountBulkUpdate"
/>
<PasswordHistoryDialog
v-if="showPasswordHistoryDialog"
:account="currentAccountColumn"
:visible.sync="showPasswordHistoryDialog"
/>
</div>
</template>
<script>
import ListTable from '@/components/Table/ListTable/index.vue'
import { ActionsFormatter } from '@/components/Table/TableFormatters'
import { accountOtherActions, accountQuickFilters, connectivityMeta } from './const'
import { openTaskPage } from '@/utils/jms'
import { ActionsFormatter, PlatformFormatter, SecretViewerFormatter, AccountConnectFormatter } from '@/components/Table/TableFormatters'
import ViewSecret from './ViewSecret.vue'
import UpdateSecretInfo from './UpdateSecretInfo.vue'
import AccountCreateUpdate from './AccountCreateUpdate.vue'
import { connectivityMeta } from './const'
import { openTaskPage } from '@/utils/jms'
import ResultDialog from './BulkCreateResultDialog.vue'
import AccountCreateUpdate from './AccountCreateUpdate.vue'
import PasswordHistoryDialog from './PasswordHistoryDialog.vue'
import DrawerListTable from '@/components/Table/DrawerListTable/index.vue'
import AccountBulkUpdateDialog from '@/components/Apps/AccountListTable/AccountBulkUpdateDialog.vue'
export default {
name: 'AccountListTable',
components: {
AccountBulkUpdateDialog,
ResultDialog,
ListTable,
UpdateSecretInfo,
ViewSecret,
AccountCreateUpdate
ResultDialog,
DrawerListTable,
UpdateSecretInfo,
AccountCreateUpdate,
PasswordHistoryDialog,
AccountBulkUpdateDialog
},
props: {
url: {
@@ -89,7 +93,7 @@ export default {
},
hasClone: {
type: Boolean,
default: false
default: true
},
asset: {
type: Object,
@@ -119,7 +123,7 @@ export default {
columnsDefault: {
type: Array,
default: () => ([
'name', 'username', 'asset', 'date_updated'
'name', 'username', 'secret', 'asset', 'platform', 'connect'
])
},
headerExtraActions: {
@@ -129,22 +133,29 @@ export default {
extraQuery: {
type: Object,
default: () => ({})
},
showQuickFilters: {
type: Boolean,
default: true
}
},
data() {
const vm = this
return {
addTemplate: false,
currentAccountColumn: {},
showPasswordHistoryDialog: false,
showViewSecretDialog: false,
showUpdateSecretDialog: false,
showResultDialog: false,
showAddDialog: false,
showAddTemplateDialog: false,
detailDrawer: () => import('@/views/accounts/Account/AccountDetail/index.vue'),
createAccountResults: [],
accountCreateUpdateTitle: this.$t('AddAccount'),
accountCreateByTemplateTitle: this.$t('AddAccountByTemplate'),
iAsset: this.asset,
account: {},
secretUrl: '',
quickFilters: this.showQuickFilters ? accountQuickFilters(this) : [],
tableConfig: {
url: this.url,
permissions: {
@@ -153,6 +164,7 @@ export default {
},
extraQuery: this.extraQuery,
columnsExclude: ['spec_info'],
columnsAdd: ['secret', 'platform', 'connect'],
columnsShow: {
min: ['name', 'username', 'actions'],
default: this.columnsDefault
@@ -160,29 +172,50 @@ export default {
columnsMeta: {
name: {
width: '120px',
formatter: function(row) {
const to = {
name: 'AssetAccountDetail',
params: { id: row.id }
formatterArgs: {
can: () => vm.$hasPerm('accounts.view_account'),
getDrawerTitle({ row }) {
return `${row.username}@${row.asset.name}`
}
if (vm.$hasPerm('accounts.view_account')) {
return <router-link to={to}>{row.name}</router-link>
} else {
return <span>{row.name}</span>
}
},
secret: {
formatter: SecretViewerFormatter,
width: '130px',
formatterArgs: {
secretFrom: 'api',
hasDownload: false,
actionLeft: true
}
},
connect: {
label: this.$t('Connect'),
width: '80px',
formatter: AccountConnectFormatter,
formatterArgs: {
buttonIcon: 'fa fa-desktop',
titleText: '可选协议',
url: '/api/v1/assets/assets/{id}',
connectUrlTemplate: (row) => `/luna/pam_connect/${row.id}/${row.username}/${row.asset.id}/${row.asset.name}/`,
setMapItem: (id, protocol) => {
this.$store.commit('table/SET_PROTOCOL_MAP_ITEM', {
key: id,
value: protocol
})
}
}
},
platform: {
label: this.$t('Platform'),
width: '120px',
formatter: PlatformFormatter,
formatterArgs: {
platformAttr: 'asset.platform'
}
},
asset: {
formatter: function(row) {
const to = {
name: 'AssetDetail',
params: { id: row.asset.id }
}
if (vm.$hasPerm('assets.view_asset')) {
return <router-link to={to}>{row.asset.name}</router-link>
} else {
return <span>{row.asset.name}</span>
}
return row.asset.name
}
},
username: {
@@ -216,75 +249,11 @@ export default {
formatter: ActionsFormatter,
formatterArgs: {
hasUpdate: false, // can set function(row, value)
hasDelete: false, // can set function(row, value)
hasClone: this.hasClone,
hasDelete: true, // can set function(row, value)
hasClone: false,
canDelete: () => vm.$hasPerm('accounts.delete_account'),
moreActionsTitle: this.$t('More'),
extraActions: [
{
name: 'View',
title: this.$t('View'),
can: this.$hasPerm('accounts.view_accountsecret'),
type: 'primary',
callback: ({ row }) => {
// debugger
vm.secretUrl = `/api/v1/accounts/account-secrets/${row.id}/`
vm.account = row
vm.showViewSecretDialog = false
setTimeout(() => {
vm.showViewSecretDialog = true
})
}
},
{
name: 'Update',
title: this.$t('Edit'),
can: this.$hasPerm('accounts.change_account') && !this.$store.getters.currentOrgIsRoot,
callback: ({ row }) => {
const data = {
...this.asset,
...row.asset
}
vm.account = row
vm.iAsset = data
vm.showAddDialog = false
vm.accountCreateUpdateTitle = this.$t('UpdateAccount')
setTimeout(() => {
vm.showAddDialog = true
})
}
},
{
name: 'Test',
title: this.$t('Test'),
can: ({ row }) =>
!this.$store.getters.currentOrgIsRoot &&
this.$hasPerm('accounts.verify_account') &&
row.asset['auto_config'].ansible_enabled &&
row.asset['auto_config'].ping_enabled,
callback: ({ row }) => {
this.$axios.post(
`/api/v1/accounts/accounts/tasks/`,
{ action: 'verify', accounts: [row.id] }
).then(res => {
openTaskPage(res['task'])
})
}
},
{
name: 'ClearSecret',
title: this.$t('ClearSecret'),
can: this.$hasPerm('accounts.change_account'),
type: 'primary',
callback: ({ row }) => {
this.$axios.patch(
`/api/v1/accounts/accounts/clear-secret/`,
{ account_ids: [row.id] }
).then(() => {
this.$message.success(this.$tc('ClearSuccessMsg'))
})
}
}
]
extraActions: accountOtherActions(this)
}
},
...this.columnsMeta
@@ -320,6 +289,7 @@ export default {
setTimeout(() => {
vm.iAsset = this.asset
vm.account = {}
vm.addTemplate = false
vm.showAddDialog = true
})
}
@@ -336,7 +306,8 @@ export default {
setTimeout(() => {
vm.iAsset = this.asset
vm.account = {}
vm.showAddTemplateDialog = true
vm.showAddDialog = true
vm.addTemplate = true
})
}
},
@@ -347,11 +318,11 @@ export default {
name: 'TestSelected',
title: this.$t('TestSelected'),
type: 'primary',
icon: 'fa-link',
icon: 'verify',
can: ({ selectedRows }) => {
return selectedRows.length > 0 &&
['clickhouse', 'redis', 'website', 'chatgpt'].indexOf(selectedRows[0].asset.type.value) === -1 &&
!this.$store.getters.currentOrgIsRoot
['clickhouse', 'redis', 'website', 'chatgpt'].indexOf(selectedRows[0].asset.type.value) === -1 &&
!this.$store.getters.currentOrgIsRoot
},
callback: function({ selectedRows }) {
const ids = selectedRows.map(v => {
@@ -416,6 +387,15 @@ export default {
}
}
},
computed: {
accountCreateUpdateTitle() {
if (this.addTemplate) {
return this.$t('AddAccountByTemplate')
} else {
return this.$t('AddAccount')
}
}
},
watch: {
url(iNew) {
this.$set(this.tableConfig, 'url', iNew)
@@ -423,52 +403,34 @@ export default {
}
},
mounted() {
if (this.columns.length > 0) {
this.tableConfig.columns = this.columns
}
if (this.otherActions) {
const actionColumn = this.tableConfig.columns[this.tableConfig.columns.length - 1]
for (const item of this.otherActions) {
actionColumn.formatterArgs.extraActions.push(item)
}
}
if (this.hasDeleteAction) {
this.tableConfig.columnsMeta.actions.formatterArgs.extraActions.push(
{
name: 'Delete',
title: this.$t('Delete'),
can: this.$hasPerm('accounts.delete_account'),
type: 'primary',
callback: ({ row }) => {
const msg = this.$t('AccountDeleteConfirmMsg')
this.$confirm(msg, this.$tc('Info'), {
type: 'warning',
confirmButtonClass: 'el-button--danger',
beforeClose: async(action, instance, done) => {
if (action !== 'confirm') return done()
this.$axios.delete(`/api/v1/accounts/accounts/${row.id}/`).then(() => {
done()
this.$refs.ListTable.reloadTable()
this.$message.success(this.$tc('DeleteSuccessMsg'))
})
}
})
}
}
)
}
this.setActions()
},
activated() {
// 由于组件嵌套较深,有可能导致 Error in activated hook: "TypeError: Cannot read properties of undefined (reading 'getList')" 的问题
setTimeout(() => {
this.refresh()
}, 300)
if (this.tabDeactivated) {
setTimeout(() => this.refresh(), 300)
}
},
deactivated() {
this.tabDeactivated = true
},
methods: {
setActions() {
if (this.columns.length > 0) {
this.tableConfig.columns = this.columns
}
if (this.otherActions) {
const actionColumn = this.tableConfig.columns[this.tableConfig.columns.length - 1]
for (const item of this.otherActions) {
actionColumn.formatterArgs.extraActions.push(item)
}
}
},
onUpdateAuthDone(account) {
Object.assign(this.account, account)
},
addAccountSuccess() {
Reflect.deleteProperty(this.$route.query, 'flag')
this.$refs.ListTable.reloadTable()
},
async getAssetDetail() {
@@ -507,7 +469,7 @@ export default {
}
</script>
<style lang='scss' scoped>
<style lang="scss" scoped>
.cell a {
color: var(--color-info);
}

View File

@@ -4,7 +4,7 @@
<script>
import { GenericListTableDialog } from '@/layout/components'
import { ShowKeyCopyFormatter } from '@/components/Table/TableFormatters'
import { SecretViewerFormatter } from '@/components/Table/TableFormatters'
export default {
components: {
@@ -33,7 +33,7 @@ export default {
columnsMeta: {
secret: {
label: this.$t('Password'),
formatter: ShowKeyCopyFormatter,
formatter: SecretViewerFormatter,
formatterArgs: {
hasDownload: false,
name: this.account.name

View File

@@ -1,44 +1,36 @@
<template>
<Dialog
:destroy-on-close="true"
:show-buttons="false"
:title="$tc('UpdateAssetUserToken')"
:visible.sync="visible"
width="50"
@cancel="handleCancel()"
@confirm="handleConfirm()"
:visible.sync="iVisible"
width="800px"
v-on="$listeners"
>
<el-form label-position="right" label-width="90px">
<el-form-item :label="$tc('Name')">
<el-input v-model="account['asset_name']" readonly />
</el-form-item>
<el-form-item :label="$tc('Username')">
<el-input v-model="account['username']" readonly />
</el-form-item>
<el-form-item :label="$tc('Password')">
<UpdateToken v-model="authInfo.password" />
</el-form-item>
<el-form-item :label="$tc('SSHSecretKey')">
<UploadKey @input="getFile" />
</el-form-item>
<el-form-item :label="$tc('Passphrase')">
<UpdateToken v-model="authInfo.passphrase" />
</el-form-item>
</el-form>
<AutoDataForm
:fields="fields"
:fields-meta="fieldsMeta"
:form="init"
:has-reset="false"
:has-save-continue="false"
:url="''"
method="patch"
@submit="handleConfirm"
/>
</Dialog>
</template>
<script>
import Dialog from '@/components/Dialog/index.vue'
import { UpdateToken, UploadKey } from '@/components/Form/FormFields'
import { accountFieldsMeta } from '@/components/Apps/AccountCreateUpdateForm/const'
import { encryptPassword } from '@/utils/crypto'
import AutoDataForm from '@/components/Form/AutoDataForm/index.vue'
export default {
name: 'UpdateSecretInfo',
components: {
Dialog,
UploadKey,
UpdateToken
AutoDataForm,
Dialog
},
props: {
account: {
@@ -51,49 +43,59 @@ export default {
}
},
data() {
const accountMeta = accountFieldsMeta(this)
return {
secretInfo: {
password: '',
private_key: '',
passphrase: ''
fields: [
'name', 'secret_type', 'password', 'ssh_key', 'token',
'access_key', 'passphrase', 'api_key'
],
fieldsMeta: {
...accountMeta,
name: {
...accountMeta.name,
readonly: true
},
secret_type: {
hidden: () => true
}
},
init: {
...this.account
}
}
},
computed: {
iVisible: {
get() {
return this.visible
},
set(val) {
this.$emit('update:visible', val)
}
}
},
methods: {
handleConfirm() {
const data = {}
if (this.secretInfo.password !== '') {
data.password = encryptPassword(this.secretInfo.password)
}
if (this.secretInfo.private_key !== '') {
data.private_key = encryptPassword(this.secretInfo.private_key)
if (this.secretInfo.passphrase) data.passphrase = this.secretInfo.passphrase
handleConfirm(form) {
const secretType = this.account.secret_type.value
const data = {
secret: encryptPassword(form[secretType])
}
this.$axios.patch(
`/api/v1/accounts/accounts/${this.account.id}/`,
data,
{ disableFlashErrorMsg: true }
).then(res => {
this.authInfo = { password: '', private_key: '' }
this.$message.success(this.$tc('UpdateSuccessMsg'))
this.$emit('updateAuthDone', res)
this.$emit('update:visible', false)
this.iVisible = false
}).catch(err => {
const errMsg = Object.values(err.response.data).join(', ')
this.$message.error(this.$tc('UpdateErrorMsg') + ' ' + errMsg)
this.$emit('update:visible', true)
this.iVisible = false
})
},
handleCancel() {
this.$emit('update:visible', false)
},
getFile(file) {
this.secretInfo.private_key = file
}
}
}
</script>
<style scoped>
</style>

View File

@@ -18,7 +18,7 @@
<span>{{ account['username'] }}</span>
</el-form-item>
<el-form-item :label="secretTypeLabel">
<ShowKeyCopyFormatter
<SecretViewerFormatter
:cell-value="secretInfo.secret"
:col="{ formatterArgs: {
name: account['name'],
@@ -60,7 +60,7 @@
<script>
import Dialog from '@/components/Dialog/index.vue'
import PasswordHistoryDialog from './PasswordHistoryDialog.vue'
import { ShowKeyCopyFormatter } from '@/components/Table/TableFormatters'
import { SecretViewerFormatter } from '@/components/Table/TableFormatters'
import { encryptPassword } from '@/utils/crypto'
export default {
@@ -68,7 +68,7 @@ export default {
components: {
Dialog,
PasswordHistoryDialog,
ShowKeyCopyFormatter
SecretViewerFormatter
},
props: {
account: {

View File

@@ -1,4 +1,5 @@
import { ChoicesFormatter } from '@/components/Table/TableFormatters'
import { openTaskPage } from '@/utils/jms'
export const connectivityMeta = {
formatter: ChoicesFormatter,
@@ -22,3 +23,276 @@ export const connectivityMeta = {
},
width: '130px'
}
export const accountOtherActions = (vm) => [
{
name: 'View',
title: vm.$t('View'),
can: vm.$hasPerm('accounts.view_accountsecret'),
type: 'primary',
order: 1,
callback: ({ row }) => {
// debugger
vm.secretUrl = `/api/v1/accounts/account-secrets/${row.id}/`
vm.account = row
vm.showViewSecretDialog = false
setTimeout(() => {
vm.showViewSecretDialog = true
})
}
},
{
name: 'Update',
title: vm.$t('Edit'),
can: vm.$hasPerm('accounts.change_account') && !vm.$store.getters.currentOrgIsRoot,
callback: ({ row }) => {
const data = {
...vm.asset,
...row.asset
}
vm.account = row
vm.iAsset = data
vm.showAddDialog = false
vm.accountCreateUpdateTitle = vm.$t('UpdateAccount')
setTimeout(() => {
vm.showAddDialog = true
})
}
},
{
name: 'UpdateSecret',
title: vm.$t('EditSecret'),
can: vm.$hasPerm('accounts.change_account') && !vm.$store.getters.currentOrgIsRoot,
callback: ({ row }) => {
const data = {
...vm.asset,
...row.asset
}
vm.account = row
vm.iAsset = data
vm.showUpdateSecretDialog = false
vm.accountCreateUpdateTitle = vm.$t('UpdateAccount')
setTimeout(() => {
console.log('Update secret')
vm.showUpdateSecretDialog = true
})
}
},
{
name: 'Clone',
title: vm.$t('Duplicate'),
can: vm.$hasPerm('accounts.add_account') && !vm.$store.getters.currentOrgIsRoot,
callback: ({ row }) => {
const data = {
...vm.asset,
...row.asset
}
vm.account = {
...row,
name: `${row.name} - ${vm.$t('Duplicate').toLowerCase()}`
}
vm.iAsset = data
vm.showAddDialog = false
vm.accountCreateUpdateTitle = vm.$t('DuplicateAccount')
setTimeout(() => {
vm.showAddDialog = true
})
}
},
{
name: 'Test',
title: vm.$t('VerifySecret'),
divided: true,
can: ({ row }) =>
!vm.$store.getters.currentOrgIsRoot &&
vm.$hasPerm('accounts.verify_account') &&
row.asset['auto_config'].ansible_enabled &&
row.asset['auto_config'].ping_enabled,
callback: ({ row }) => {
vm.$axios.post(
`/api/v1/accounts/accounts/tasks/`,
{ action: 'verify', accounts: [row.id] }
).then(res => {
openTaskPage(res['task'])
})
}
},
{
name: 'ClearSecret',
title: vm.$t('ClearSecret'),
can: vm.$hasPerm('accounts.change_account'),
type: 'primary',
callback: ({ row }) => {
vm.$axios.patch(
`/api/v1/accounts/accounts/clear-secret/`,
{ account_ids: [row.id] }
).then(() => {
vm.$message.success(vm.$tc('ClearSuccessMsg'))
})
}
},
{
name: 'SecretHistory',
// 密文历史
title: vm.$t('HistoryPassword'),
can: () => vm.$hasPerm('accounts.view_accountsecret'),
type: 'primary',
callback: ({ row }) => {
vm.currentAccountColumn = row
vm.$nextTick(() => {
vm.showPasswordHistoryDialog = true
})
}
},
{
name: 'CopyToOther',
title: vm.$t('CopyToAsset'),
type: 'primary',
divided: true,
callback: ({ row }) => {
vm.accountCreateUpdateTitle = vm.$t('CopyToOther')
vm.$route.query.flag = 'copy'
vm.iAsset = vm.asset
vm.account = row
vm.showAddDialog = true
}
},
{
name: 'MoveToOther',
title: vm.$t('MoveToAsset'),
type: 'primary',
callback: ({ row }) => {
vm.accountCreateUpdateTitle = vm.$t('MoveToOther')
vm.$route.query.flag = 'move'
vm.iAsset = vm.asset
vm.account = row
vm.showAddDialog = true
}
}
]
export const accountQuickFilters = (vm) => [
{
label: vm.$t('Recent (7 days)'),
options: [
{
label: vm.$t('RecentlyDiscovered'),
filter: {
latest_discovery: '1'
}
},
{
label: vm.$t('RecentlyLoggedIn'),
filter: {
latest_accessed: '1'
}
},
{
label: vm.$t('RecentlyModified'),
filter: {
latest_updated: '1'
}
},
{
label: vm.$t('RecentlyChangedPassword'),
filter: {
latest_secret_changed: '1'
}
},
{
label: vm.$t('RecentPasswordChangeFailed'),
filter: {
latest_secret_changed_failed: '1'
}
}
]
},
{
label: vm.$t('RiskyAccount'),
options: [
{
label: vm.$t('LongTimeNoLogin'),
filter: {
risk: 'long_time_no_login'
}
},
{
label: vm.$t('UnmanagedAccount'),
filter: {
risk: 'new_found'
}
},
{
label: vm.$t('WeakPassword'),
filter: {
risk: 'weak_password'
}
},
{
label: vm.$t('EmptyPassword'),
filter: {
has_secret: 'false'
}
},
{
label: vm.$t('LongTimeNoChangeSecret'),
filter: {
long_time_no_change_secret: 'true'
}
},
{
label: vm.$t('LongTimeNoVerify'),
filter: {
long_time_no_verify: 'true'
}
}
]
},
{
label: vm.$t('AccountType'),
options: [
{
label: vm.$t('All'),
filter: {
category: ''
}
},
{
label: vm.$t('Host'),
filter: {
category: 'host'
}
},
{
label: vm.$t('Database'),
filter: {
category: 'database'
}
},
{
label: vm.$t('Cloud'),
filter: {
category: 'cloud'
}
},
{
label: vm.$t('Device'),
filter: {
category: 'device'
}
},
{
label: 'Web',
filter: {
category: 'website'
}
},
{
label: vm.$t('Other'),
filter: {
category: 'custom'
}
}
]
}
]

View File

@@ -22,6 +22,8 @@
:url="baseUrl"
class="tree-table"
v-bind="$attrs"
@loaded="handleTableLoaded"
v-on="$listeners"
/>
</Dialog>
</template>
@@ -64,6 +66,7 @@ export default {
data() {
const vm = this
return {
isLoaded: false,
dialogVisible: false,
rowSelected: _.cloneDeep(this.value) || [],
rowsAdd: [],
@@ -124,6 +127,9 @@ export default {
}
},
methods: {
handleTableLoaded() {
this.isLoaded = true
},
handleClose() {
this.$refs.ListPage.$refs.TreeList.componentKey += 1
},

View File

@@ -28,7 +28,6 @@
<script>
import Select2 from '@/components/Form/FormFields/Select2.vue'
import AssetSelectDialog from './dialog.vue'
import { b } from 'css-color-function/lib/adjusters'
export default {
componentName: 'AssetSelect',
@@ -38,6 +37,10 @@ export default {
type: String,
default: '/api/v1/assets/assets/'
},
defaultPageSize: {
type: Number,
default: 10
},
baseNodeUrl: {
type: String,
default: '/api/v1/assets/nodes/'
@@ -71,6 +74,7 @@ export default {
value: iValue,
multiple: true,
clearable: true,
defaultPageSize: this.defaultPageSize,
ajax: {
url: this.baseUrl,
transformOption: (item) => {
@@ -81,7 +85,6 @@ export default {
}
},
methods: {
b,
handleFocus() {
this.$refs.select2.selectRef.blur()
this.dialogVisible = true

View File

@@ -138,6 +138,10 @@ export default {
treeSetting.showDelete = this.$hasPerm('assets.delete_node')
},
methods: {
reloadTable() {
console.log('Reload table')
this.$refs.TreeList.reloadTable()
},
setTreeUrlQuery() {
let str = ''
for (const key in this.treeUrlQuery) {
@@ -157,30 +161,37 @@ export default {
$('#m_show_asset_only_current_node').css('color', '#606266')
}
},
getAssetsUrl(treeNode) {
let url = this.treeSetting?.url || this.url
const setParam = (param, value, delay) => {
setTimeout(() => {
url = setUrlParam(url, param, value)
})
}
if (treeNode.meta.type === 'node') {
const nodeId = treeNode.meta.data.id
url = setUrlParam(url, 'node_id', nodeId)
url = setUrlParam(url, 'asset_id', '')
setParam('node_id', nodeId)
setParam('asset_id', '')
} else if (treeNode.meta.type === 'asset') {
const assetId = treeNode.meta.data?.id || treeNode.id
url = setUrlParam(url, 'node_id', '')
url = setUrlParam(url, 'asset_id', assetId)
setParam('node_id', '')
setParam('asset_id', assetId)
} else if (treeNode.meta.type === 'category') {
url = setUrlParam(url, 'category', treeNode.meta.category)
setParam('category', treeNode.meta.category)
} else if (treeNode.meta.type === 'type') {
url = setUrlParam(url, 'category', treeNode.meta.category)
url = setUrlParam(url, 'type', treeNode.meta._type)
setParam('category', treeNode.meta.category)
setParam('type', treeNode.meta._type)
} else if (treeNode.meta.type === 'platform') {
url = setUrlParam(url, 'platform', treeNode.id)
setParam('platform', treeNode.id)
}
const query = this.setTreeUrlQuery()
url = query ? `${url}&${query}` : url
setTimeout(() => {
const query = this.setTreeUrlQuery()
url = query ? `${url}&${query}` : url
this.$set(this.tableConfig, 'url', url)
}, 300)
})
if (this.treeSetting.selectSyncToRoute !== false) {
setRouterQuery(this, url)

View File

@@ -3,7 +3,7 @@
</template>
<script>
import ListTable from '@/components/Table/ListTable/index.vue'
import { DrawerListTable as ListTable } from '@/components'
export default {
name: 'BlockedIPList',

View File

@@ -12,7 +12,7 @@
>
<el-form :model="secretInfo" class="password-form" label-position="right" label-width="100px">
<el-form-item :label="$tc('OldSecret')">
<ShowKeyCopyFormatter
<SecretViewerFormatter
:cell-value="secretInfo.old_secret"
:col="{ formatterArgs: {
name: 'old_secret'
@@ -20,7 +20,7 @@
/>
</el-form-item>
<el-form-item :label="$tc('NewSecret')">
<ShowKeyCopyFormatter
<SecretViewerFormatter
:cell-value="secretInfo.new_secret"
:col="{ formatterArgs: {
name: 'new_secret'
@@ -34,13 +34,13 @@
<script>
import Dialog from '@/components/Dialog/index.vue'
import { ShowKeyCopyFormatter } from '@/components/Table/TableFormatters'
import { SecretViewerFormatter } from '@/components/Table/TableFormatters'
export default {
name: 'RecordViewSecret',
components: {
Dialog,
ShowKeyCopyFormatter
SecretViewerFormatter
},
props: {
visible: {

View File

@@ -1,5 +1,6 @@
<template>
<AssetTreeTable
ref="AssetTreeTable"
:header-actions="headerActions"
:table-config="tableConfig"
:tree-setting="treeSetting"
@@ -47,9 +48,19 @@ export default {
return this.tableUrl.replace('/assets/', `/assets/${row.id}/accounts/`)
}
},
nameDisabled: {
type: Boolean,
default: true
name: {
type: Object,
default: () => ({
formatter: DetailFormatter,
formatterArgs: {
route: 'AssetDetail',
can: true
}
})
},
comment: {
type: Object,
default: () => ({})
}
},
data() {
@@ -76,15 +87,11 @@ export default {
columnsExclude: ['spec_info'],
columnsShow: {
min: ['name', 'address', 'accounts'],
default: ['name', 'address', 'platform', 'connectivity', 'view_account', 'actions']
default: ['name', 'address', 'platform', 'view_account', 'actions']
},
columnsMeta: {
name: {
formatter: DetailFormatter,
formatterArgs: {
route: 'AssetDetail',
can: !this.nameDisabled
}
...this.name
},
labels: {
formatterArgs: {
@@ -99,7 +106,8 @@ export default {
formatter: AccountInfoFormatter,
width: '100px'
},
connectivity: connectivityMeta
connectivity: connectivityMeta,
comment: { ...this.comment }
},
tableAttrs: {
rowClassName({ row }) {

View File

@@ -1,24 +1,25 @@
<template>
<el-row :gutter="24">
<el-col :md="20" :sm="22">
<ListTable v-bind="config" />
</el-col>
</el-row>
<TwoCol>
<ListTable v-bind="config" />
</TwoCol>
</template>
<script>
import ListTable from '@/components/Table/ListTable/index.vue'
import { DrawerListTable as ListTable } from '@/components'
import { toM2MJsonParams } from '@/utils/jms'
import TwoCol from '@/layout/components/Page/TwoColPage.vue'
export default {
name: 'AssetJsonTab',
components: {
TwoCol,
ListTable
},
props: {
object: {
type: Object,
default: () => {}
default: () => {
}
}
},
data() {
@@ -32,9 +33,11 @@ export default {
},
tableConfig: {
url: `/api/v1/assets/assets/?${key}=${value}`,
columns: ['name', 'address', 'platform',
'type', 'is_active'
],
columns: ['name', 'address', 'platform', 'type', 'is_active'],
columnsShow: {
min: ['name', 'address'],
default: ['name', 'address', 'platform']
},
columnsMeta: {
name: {
label: this.$t('Asset'),

View File

@@ -1,18 +1,18 @@
<template>
<el-row :gutter="24">
<el-col :md="20" :sm="22">
<ListTable v-bind="config" />
</el-col>
</el-row>
<TwoCol>
<ListTable v-bind="config" />
</TwoCol>
</template>
<script>
import ListTable from '@/components/Table/ListTable/index.vue'
import { DrawerListTable as ListTable } from '@/components'
import { toM2MJsonParams } from '@/utils/jms'
import TwoCol from '@/layout/components/Page/TwoColPage.vue'
export default {
name: 'User',
components: {
TwoCol,
ListTable
},
props: {
@@ -34,13 +34,16 @@ export default {
tableConfig: {
url: `/api/v1/users/users/?${key}=${value}`,
columns: [
'name', 'username', 'groups', 'system_roles',
'name', 'username', 'email', 'groups', 'system_roles',
'org_roles', 'source', 'is_valid'
],
columnsShow: {
min: ['name', 'username'],
default: ['name', 'username', 'email']
},
columnsMeta: {
name: {
label: this.$t('Name'),
width: 85,
formatter: (row) => {
const to = {
name: 'UserDetail',

View File

@@ -1,30 +1,28 @@
<template>
<div>
<el-row :gutter="20">
<el-col :md="16" :sm="24">
<IBox :title="title" class="block" v-bind="$attrs">
<el-timeline>
<el-timeline-item
v-for="(activity, index) in activities"
:key="index"
:size="activity.size"
:timestamp="activity.timestamp"
:type="activity.type"
placement="bottom"
<TwoCol>
<IBox :title="title" class="block" v-bind="$attrs">
<el-timeline>
<el-timeline-item
v-for="(activity, index) in activities"
:key="index"
:size="activity.size"
:timestamp="activity.timestamp"
:type="activity.type"
placement="bottom"
>
{{ activity.content }}
<el-link
v-if="activity['detail_url']"
type="primary"
@click.native="onClick(activity)"
>
{{ activity.content }}
<el-link
v-if="activity['detail_url']"
type="primary"
@click.native="onClick(activity)"
>
{{ $tc('Detail') }}
</el-link>
</el-timeline-item>
</el-timeline>
</IBox>
</el-col>
</el-row>
{{ $tc('Detail') }}
</el-link>
</el-timeline-item>
</el-timeline>
</IBox>
</TwoCol>
<DiffDetail ref="DetailDialog" :title="$tc('OperateLog')" />
</div>
</template>
@@ -34,10 +32,12 @@ import IBox from '@/components/IBox/index.vue'
import DiffDetail from '@/components/Dialog/DiffDetail.vue'
import { openTaskPage } from '@/utils/jms'
import { toSafeLocalDateStr } from '@/utils/time'
import TwoCol from '@/layout/components/Page/TwoColPage.vue'
export default {
name: 'ResourceActivity',
components: {
TwoCol,
IBox,
DiffDetail
},

View File

@@ -1,7 +1,12 @@
<template>
<IBox v-if="loading" style="width: 100%; height: 200px" />
<div v-else>
<DetailCard v-if="hasObject && items.length > 0" :items="validItems" :loading="loading" v-bind="$attrs" />
<DetailCard
v-if="hasObject && items.length > 0"
:items="validItems"
:loading="loading"
v-bind="$attrs"
/>
</div>
</template>
@@ -32,7 +37,7 @@ export default {
type: Array,
default: null
},
showUndefine: {
showUndefined: {
type: Boolean,
default: true
},
@@ -168,7 +173,7 @@ export default {
const data = await this.$store.dispatch('common/getUrlMeta', { url: this.url })
let remoteMeta = data.actions['GET'] || {}
if (this.nested) {
remoteMeta = remoteMeta[this.nested]?.children || {}
remoteMeta = remoteMeta[this.nested]?.children || remoteMeta || {}
}
let fields = this.fields
fields = fields || Object.keys(remoteMeta)
@@ -220,7 +225,7 @@ export default {
value = this.parseValue(value, fieldMeta.type)
if (value === undefined) {
if (this.showUndefine) {
if (this.showUndefined) {
value = '-'
} else {
continue

View File

@@ -1,19 +1,20 @@
<template>
<IBox :fa="fa" :title="title">
<el-form :label-width="labelWidth" class="content" label-position="left">
<span v-for="item in items" :key="item.key">
<el-form-item v-if="item.has !== false" :class="item.class" :label="item.key">
<span slot="label"> {{ formateLabel(item.key) }}</span>
<span
:is="item.component"
v-if="item.component"
v-bind="{...item}"
/>
<ItemValue v-else :value="item.value" class="item-value" v-bind="item" />
</el-form-item>
</span>
<el-form :label-width="labelWidth" class="content detail-card" label-position="left">
<template v-for="item in items">
<div v-if="item.has !== false" :key="item.key" :class="item.class " :label="item.key" class="el-form-item">
<span slot="label" class="el-form-item__label"> {{ formateLabel(item.key) }}</span>
<span class="item-value el-form-item__content">
<template
:is="item.component"
v-if="item.component"
v-bind="{...item}"
/>
<ItemValue v-else :value="item.value" v-bind="item" />
</span>
</div>
</template>
</el-form>
<slot />
</IBox>
</template>
@@ -45,7 +46,7 @@ export default {
},
labelWidth: {
type: String,
default: '25%'
default: '120px'
}
},
data() {
@@ -71,55 +72,76 @@ export default {
padding: 20px 40px;
}
.el-form-item {
border-bottom: 1px dashed #EBEEF5;
padding: 1px 0;
margin-bottom: 0;
&:last-child {
border-bottom: none;
}
&.array-item {
border-bottom: none;
::v-deep .el-form-item__content {
border-bottom: 1px dashed #EBEEF5
}
::v-deep .el-form-item__label:last-child {
border: 1px dashed #EBEEF5;
}
}
::v-deep .el-form-item__label {
padding-right: 8%;
overflow: hidden;
color: var(--color-icon-primary);
span {
display: inline-block;
line-height: 1.5;
}
}
::v-deep .el-form-item__content {
color: var(--color-text-primary);
font-size: 13px;
line-height: 40px;
}
::v-deep .el-tag--mini {
margin-right: 3px;
}
}
.item-value span {
word-break: break-word;
}
.content {
font-size: 13px;
line-height: 2.5;
line-height: 2;
::v-deep .el-form-item {
border-bottom: 1px dashed #F4F4F4;
padding: 1px 0;
margin-bottom: 0;
display: flex;
align-items: center;
//text-align: end;
line-height: 32px;
min-height: 32px;
&:last-child {
//border-bottom: none;
}
&.array-item {
border-bottom: none;
::v-deep .el-form-item__content {
border-bottom: 1px dashed #EBEEF5
}
::v-deep .el-form-item__label:last-child {
border: 1px dashed #EBEEF5;
}
}
.el-form-item__label {
//padding-right: 8%;
overflow: hidden;
color: var(--color-icon-primary);
font-size: 12px;
line-height: 1.5;
font-weight: 400;
width: 33%;
min-width: 120px;
padding: 5px 0;
span {
display: inline-block;
//line-height: 1.1;
}
}
.el-form-item__content {
color: var(--color-text-primary);
font-size: 13px;
line-height: 1.5;
width: calc(100% - 120px);
padding: 5px 0;
}
::v-deep .el-tag--mini {
margin-right: 3px;
}
}
.item-value {
::v-deep span {
//display: -webkit-box;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-word;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
vertical-align: middle;
}
}
}
</style>

View File

@@ -340,13 +340,13 @@ b, strong {
}
tr td {
line-height: 1.42857;
padding: 8px;
line-height: 1.4;
padding: 8px 0;
vertical-align: top;
}
tr.item td {
border-top: 1px solid #e7eaec;
border-top: 1px dashed #EBEEF5;
}
.box-margin {

View File

@@ -1,21 +1,22 @@
<template>
<el-card shadow="never">
<div slot="header" class="summary-header">
<span class="header-title">{{ title }}</span>
<div>
<div class="summary-header">
<el-tooltip :content="title" placement="top" :open-delay="500">
<span class="title">{{ title }}</span>
</el-tooltip>
</div>
<slot>
<h1 class="no-margins">
<span v-if="body.disabled" class="disabled-link">{{ body.count }}</span>
<router-link v-else :to="body.route">
<span>{{ body.count }}</span>
</router-link>
</h1>
<small>{{ body.comment }}</small>
<h3 class="no-margins ">
<span v-async="iCount" class="num" @click="handleClick">
-
</span>
</h3>
</slot>
</el-card>
</div>
</template>
<script>
export default {
name: 'SummaryCard',
props: {
@@ -23,56 +24,90 @@ export default {
type: String,
default: ''
},
rightSideLabel: {
type: Object,
default: () => ({})
},
body: {
type: Object,
default: () => ({})
},
count: {
type: [Number, String, Promise],
default: 0
},
route: {
type: [String, Object],
default: ''
},
callback: {
type: Function,
default: () => {
}
},
disabled: {
type: Boolean,
default: false
}
},
data() {
return {}
},
computed: {
iCount() {
const count = this.body.count || this.count
return count
},
iRoute() {
return this.body.route || this.route
},
iDisabled() {
return this.body.disabled === undefined ? this.disabled : this.body.disabled
}
},
methods: {
handleClick() {
if (this.iDisabled) {
return
}
if (this.iRoute) {
this.$router.push(this.iRoute)
return
}
this.callback.bind(this)()
this.$emit('click')
}
}
}
</script>
<style lang="scss" scoped>
.pull-right {
float: right !important;
}
.summary-header {
//color: var(--color-icon-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
.header-title {
font-size: 14px;
margin: 0 0 7px;
.title {
font-style: normal;
font-weight: 600;
font-size: 12px;
text-transform: uppercase;
line-height: 1.2;
}
}
.right-side ::v-deep .el-tag {
font-weight: 600;
padding: 3px 8px;
text-shadow: none;
line-height: 1;
}
.no-margins {
margin: 0 !important;
h1 {
font-size: 30px;
font-weight: 100;
}
.num {
font-style: normal;
font-weight: 500;
font-size: 24px;
line-height: 40px;
color: var(--color-text-primary);
cursor: pointer;
.el-card__body {
background-color: #ffffff;
color: inherit;
padding: 15px 20px 20px 20px !important;
border-color: #e7eaec;
border-image: none;
border-style: solid solid none;
border-width: 1px 0;
}
.no-margins {
margin: 0 !important;
}
.disabled-link {
color: #428bca;
&:hover {
color: var(--color-primary);
}
}
}
</style>

View File

@@ -5,15 +5,28 @@
v-if="action.dropdown"
v-show="action.dropdown.length > 0"
:key="action.name"
:class="[action.name, {grouped: action.grouped }]"
:size="action.size"
:split-button="!!action.split"
:type="action.type"
class="action-item"
placement="bottom-start"
trigger="click"
@click="handleClick(action)"
@command="handleDropdownCallback"
>
<el-button :size="size" class="more-action" v-bind="cleanButtonAction(action)">
<span v-if="action.icon && !action.icon.startsWith('el-')" class="pre-icon">
<i v-if="action.icon.startsWith('fa')" :class="'fa fa-fw ' + action.icon" />
<svg-icon v-else :icon-class="action.icon" />
<span v-if="action.split">
{{ action.title }}
</span>
<el-button
v-else
:class="action.name"
:size="size"
class="more-action"
v-bind="{...cleanButtonAction(action), icon: ''}"
>
<span class="pre-icon">
<Icon v-if="action.icon" :icon="action.icon" />
</span>
<span v-if="action.title">
{{ action.title }}<i class="el-icon-arrow-down el-icon--right" />
@@ -29,7 +42,13 @@
>
{{ option.group }}
</div>
<el-tooltip :key="option.name" :content="option.tip" :disabled="!option.tip" :open-delay="500" placement="top">
<el-tooltip
:key="option.name"
:content="option.tip"
:disabled="!option.tip"
:open-delay="500"
placement="top"
>
<el-dropdown-item
:key="option.name"
:command="[option, action]"
@@ -37,9 +56,8 @@
class="dropdown-item"
v-bind="{...option, icon: ''}"
>
<span v-if="option.icon" class="pre-icon">
<i v-if="option.icon.startsWith('fa')" :class="'fa fa-fw ' + option.icon" />
<svg-icon v-else :icon-class="option.icon" />
<span v-if="actionsHasIcon(action.dropdown)" class="pre-icon">
<Icon v-if="option.icon" :icon="option.icon" />
</span>
{{ option.title }}
</el-dropdown-item>
@@ -51,16 +69,16 @@
<el-button
v-else
:key="action.name"
:class="[action.name, {grouped: action.grouped }]"
:size="size"
class="action-item"
v-bind="{...cleanButtonAction(action), icon: action.icon && action.icon.startsWith('el-') ? action.icon : ''}"
v-bind="{...cleanButtonAction(action), icon: ''}"
@click="handleClick(action)"
>
<el-tooltip :content="action.tip" :disabled="!action.tip" :open-delay="500" placement="top">
<el-tooltip :content="action.tip" :disabled="!action.tip" placement="top">
<span>
<span v-if="action.icon && !action.icon.startsWith('el-')" style="vertical-align: initial">
<i v-if="action.icon.startsWith('fa')" :class="'fa ' + action.icon" />
<svg-icon v-else :icon-class="action.icon" />
<span v-if="action.icon" style="vertical-align: initial">
<Icon :icon="action.icon" />
</span>
{{ action.title }}
</span>
@@ -72,9 +90,13 @@
<script>
import { toSentenceCase } from '@/utils/common'
import Icon from '@/components/Widgets/Icon/index.vue'
export default {
name: 'DataActions',
components: {
Icon
},
props: {
grouped: {
type: Boolean,
@@ -99,6 +121,9 @@ export default {
}
},
methods: {
actionsHasIcon(actions) {
return actions.some(action => action.icon)
},
hasIcon(action, type = '') {
const icon = action.icon
if (!icon) {
@@ -110,6 +135,7 @@ export default {
return true
},
handleDropdownCallback(command) {
console.log('Handle drop down lick')
const [option, dropdown] = command
if (option.disabled) {
return
@@ -156,6 +182,7 @@ export default {
delete action['callback']
delete action['name']
delete action['can']
delete action['split']
return action
},
cleanActions(actions) {
@@ -185,6 +212,10 @@ export default {
}
delete action['can']
if (!action.size) {
action.size = 'small'
}
if (action.dropdown) {
action.dropdown = this.cleanActions(action.dropdown)
}
@@ -209,6 +240,10 @@ $color-drop-menu-border: #e4e7ed;
.action-item {
margin-left: 5px;
&.grouped {
margin-left: 0;
}
&:first-child {
margin-left: 0;
}
@@ -252,7 +287,10 @@ $color-drop-menu-border: #e4e7ed;
display: flex;
align-items: center;
padding: 2px 6px;
color: $btn-text-color;
&:not(.is-plain) {
color: $btn-text-color;
}
* {
vertical-align: baseline !important;
@@ -280,7 +318,6 @@ $color-drop-menu-border: #e4e7ed;
// 下拉 options
.el-dropdown-menu {
::v-deep .more-batch-processing {
text-align: center;
&:hover {
background-color: transparent !important;
@@ -303,6 +340,7 @@ $color-drop-menu-border: #e4e7ed;
.dropdown-item {
color: var(--color-text-primary);
line-height: 34px;
.pre-icon {
width: 17px;
@@ -324,6 +362,8 @@ $color-drop-menu-border: #e4e7ed;
}
.el-dropdown-menu__item {
padding: 0 20px;
&.is-disabled {
color: var(--color-disabled);
cursor: not-allowed;

View File

@@ -1,6 +1,7 @@
<template>
<Dialog
v-if="detailVisible"
:modal="false"
:show-cancel="false"
:show-confirm="false"
:title="title"

View File

@@ -0,0 +1,71 @@
<template>
<Dialog
:show-cancel="false"
:visible="iVisible"
class="processing-dialog"
height="300"
title="Processing"
width="300"
@confirm="iVisible=false"
>
<div id="load">
<div class="spinner" />
</div>
</Dialog>
</template>
<script>
import Dialog from './index.vue'
export default {
name: 'ProcessingDialog',
components: { Dialog },
props: {
visible: {
type: Boolean,
default: false
}
},
data() {
return {}
},
computed: {
iVisible: {
get() {
return this.visible
},
set(val) {
this.$emit('update:visible', val)
}
}
}
}
</script>
<style lang="scss" scoped>
.processing-dialog {
::v-deep .el-dialog__body {
overflow: hidden;
}
}
.spinner {
width: 100px;
height: 100px;
border: 5px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
border-top-color: var(--color-primary);
animation: spin 1s infinite linear;
}
#load {
display: flex;
justify-content: center;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>

View File

@@ -0,0 +1,69 @@
<template>
<Dialog
:visible="iVisible"
height="300"
title="Processing"
width="300"
class="processing-dialog"
>
<div id="load">
<div class="spinner" />
</div>
</Dialog>
</template>
<script>
import Dialog from './index.vue'
export default {
name: 'RemoteProcessingDialog',
components: { Dialog },
props: {
visible: {
type: Boolean,
default: false
}
},
data() {
return {}
},
computed: {
iVisible: {
get() {
return this.visible
},
set(val) {
this.$emit('update:visible', val)
}
}
}
}
</script>
<style lang="scss" scoped>
.processing-dialog {
::v-deep .el-dialog__body {
overflow: hidden;
}
}
.spinner {
width: 100px;
height: 100px;
border: 5px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
border-top-color: var(--color-primary);
animation: spin 1s infinite linear;
}
#load {
display: flex;
justify-content: center;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>

View File

@@ -0,0 +1,96 @@
<template>
<Dialog
:show-cancel="false"
:title="title"
:visible.sync="visible"
:close-on-click-modal="false"
width="700px"
@close="onClose"
@confirm="visible = false"
>
<el-alert type="warning" :closable="false">
{{ warningText }}
<div class="secret">
<div class="row">
<span class="col">ID:</span>
<span class="value">{{ keyInfo.id }}</span>
<i class="el-icon-copy-document copy-icon" @click="handleCopy(keyInfo.id)" />
</div>
<div class="row">
<span class="col">Secret:</span>
<span class="value">{{ keyInfo.secret }}</span>
<i class="el-icon-copy-document copy-icon" @click="handleCopy(keyInfo.secret)" />
</div>
</div>
</el-alert>
</Dialog>
</template>
<script>
import i18n from '@/i18n/i18n'
import { copy } from '@/utils/common'
import Dialog from '@/components/Dialog/index'
export default {
name: 'Secret',
components: {
Dialog
},
props: {
title: {
type: String,
default: () => i18n.t('CreateAccessKey')
},
warningText: {
type: String,
default: () => i18n.t('ApiKeyWarning')
}
},
data() {
return {
keyInfo: { id: '', secret: '' },
visible: false
}
},
methods: {
show(data) {
this.keyInfo = data
this.visible = true
},
onClose() {
this.$emit('close')
},
handleCopy(value) {
copy(value)
}
}
}
</script>
<style lang='scss' scoped>
.secret {
color: #2b2f3a;
margin-top: 20px;
}
.row {
margin-bottom: 10px;
}
.col {
width: 100px;
text-align: left;
display: inline-block;
}
.copy-icon {
margin-left: 5px;
cursor: pointer;
transition: color 0.2s;
}
.value {
font-weight: 600;
}
</style>

View File

@@ -10,13 +10,16 @@
v-bind="$attrs"
v-on="$listeners"
>
<slot />
<div v-loading="disabledStatus">
<slot />
</div>
<div v-if="showButtons" slot="footer" class="dialog-footer">
<slot name="footer">
<el-button v-if="showCancel && showButtons" size="small" @click="onCancel">{{ cancelTitle }}</el-button>
<el-button
v-if="showConfirm && showButtons"
:loading="loadingStatus"
:disabled="disabledStatus"
size="small"
type="primary"
@click="onConfirm"
@@ -69,7 +72,7 @@ export default {
type: Boolean,
default: true
},
loadingStatus: {
disabledStatus: {
type: Boolean,
default: false
},
@@ -98,58 +101,68 @@ export default {
</script>
<style lang="scss" scoped>
.dialog ::v-deep .el-dialog {
border-radius: 0.3em;
max-width: min(100vw, 1500px);
.dialog ::v-deep .el-dialog {
border-radius: 0.3em;
max-width: min(100vw, 1500px);
//.el-form, .form-buttons {
// margin-left: 20px;
//}
.form-group-header {
margin-left: 20px;
}
.el-form--label-top {
.form-group-header {
margin-left: 20px;
}
.el-icon-circle-check {
display: none;
}
&__header {
box-sizing: border-box;
padding: 15px 22px;
border-bottom: 1px solid #dee2e6;
font-weight: 400;
}
&__body {
padding: 20px 30px;
&:has(.el-table) {
background: #f3f3f4;
}
}
&__footer {
border-top: 1px solid #dee2e6;
padding: 16px 25px;
justify-content: flex-end;
margin-left: 0;
}
}
@media (max-width: 900px) {
.dialog ::v-deep .el-dialog {
max-width: calc(100% - 30px);
.el-icon-circle-check {
display: none;
}
&__header {
box-sizing: border-box;
padding: 15px 22px;
border-bottom: 1px solid #dee2e6;
font-weight: 400;
}
&__body {
padding: 20px 30px;
&:has(.el-table) {
background: #f3f3f4;
}
}
.dialog-footer ::v-deep button.el-button {
font-size: 13px;
padding: 8px 12px;
&__footer {
border-top: 1px solid #dee2e6;
padding: 16px 25px;
justify-content: flex-end;
}
}
.dialog-fade-enter-active, .dialog-fade-leave-active {
transition: opacity 1s ease;
@media (max-width: 900px) {
.dialog ::v-deep .el-dialog {
max-width: calc(100% - 30px);
}
}
.dialog-fade-enter, .dialog-fade-leave-to /* .dialog-fade-leave-active 在 <2.1.8 中以及被重复声明 */
{
opacity: 0;
}
.dialog-footer ::v-deep button.el-button {
font-size: 13px;
padding: 8px 12px;
}
.dialog-fade-enter-active, .dialog-fade-leave-active {
transition: opacity 1s ease;
}
.dialog-fade-enter, .dialog-fade-leave-to /* .dialog-fade-leave-active 在 <2.1.8 中以及被重复声明 */
{
opacity: 0;
}
</style>

View File

@@ -0,0 +1,289 @@
<template>
<el-drawer
ref="drawer"
v-el-drawer-drag-width
:append-to-body="true"
:before-close="handleClose"
:class="['drawer', { 'drawer__no-footer': !hasFooter }]"
:modal="modal"
:size="size"
:title="title"
:visible.sync="iVisible"
custom-class="drawer"
destroy-on-close
direction="rtl"
v-on="$listeners"
>
<div class="drawer__content">
<slot name="default">
<component
:is="component"
v-if="component"
ref="dynamicComponent"
v-bind="componentProps"
v-on="componentListeners"
/>
</slot>
</div>
<div v-if="hasFooter" ref="drawerFooter" class="drawer__footer" />
</el-drawer>
</template>
<script>
export default {
props: {
title: {
type: String,
default: ''
},
size: {
type: String,
default: '768px'
},
component: {
type: [String, Function, Object],
default: ''
},
componentProps: {
type: Object,
default: () => ({})
},
componentListeners: {
type: Object,
default: () => ({})
},
visible: {
type: Boolean,
default: false
},
modal: {
type: Boolean,
default: true
},
hasFooter: {
type: Boolean,
default: false
}
},
data() {
return {
loading: false,
formLabelWidth: '80px'
}
},
computed: {
iVisible: {
get() {
return this.visible
},
set(val) {
this.$emit('update:visible', val)
}
}
},
mounted() {
},
methods: {
handleClose(done) {
this.$emit('close-drawer')
done()
}
}
}
</script>
<style lang='scss' scoped>
.drawer__no-footer {
::v-deep {
.drawer {
.page {
height: calc(100vh - 55px);
}
}
}
}
.drawer {
::v-deep {
min-width: 565px;
.el-card__body {
padding-top: 10px;
padding-bottom: 20px;
}
.page-submenu {
.el-tabs__header {
padding: 0 15px;
}
.el-tabs__item.is-top {
padding: 0 10px;
}
}
.form-buttons {
margin-left: 13px;
}
.el-form {
margin-right: 1px;
padding-right: 15px;
height: 100%;
&.detail-card {
padding-right: 0;
}
// Detail 中
&.content {
margin-right: 0;
}
.form-buttons {
//position: absolute;
// bottom: 13px;
// margin-left: 20%;
// margin-top: 0;
}
// Form 中的子 form
.el-form {
margin-left: 0;
margin-top: 0;
margin-bottom: 0;
}
.el-form-item {
.el-form-item__label {
padding-right: 20px;
}
.el-radio {
line-height: 25px;
margin-right: 13px;
.el-radio__label {
padding-left: 5px;
}
}
}
&.el-form--label-top {
.el-radio-group {
.el-radio {
display: block;
padding-bottom: 3px;
}
}
.el-form-item {
padding-left: 12px;
.el-form-item__label {
padding: 0 20px 0 0;
line-height: 30px;
}
.sub-form {
margin-left: -1px;
.form-fields {
max-height: unset;
}
}
}
&.form-fields {
//overflow: auto;
//max-height: calc(100vh - 180px);
}
.el-checkbox-group {
.el-checkbox {
display: block;
padding-bottom: 3px;
}
}
.el-form-item__content:has(.el-checkbox):not(:has(.el-checkbox-group)) {
display: inline-block; /* 更改为 inline-block */
//width: unset; /* 这个设置上去后,平台详情中, Automations 会有问题 */
vertical-align: bottom;
}
.el-form-item__content {
form {
.el-form-item {
padding-left: 0;
}
}
}
}
.form-group-header {
margin-left: 20px;
}
}
.el-drawer__header {
border-bottom: 1px solid #EBEEF5;
margin-bottom: 0;
padding: 15px 20px;
font-size: 16px;
font-weight: 500;
color: var(--color-text-primary);
}
.sql.container {
display: none;
}
.page {
overflow-y: auto;
height: calc(100vh - 110px);
&.tab-page {
.page-content {
padding-right: 0;
padding-left: 0;
}
}
.page-content {
height: unset;
padding-right: 10px;
padding-left: 20px;
& > div {
margin-bottom: 1px;
}
}
.ibox {
margin-bottom: 10px;
border: none;
}
}
.drawer__content, .tab-page-content {
background: #f3f3f3;
}
.drawer__footer {
border-top: solid 1px #f3f3f3;
}
//.el-drawer__header {
// margin-bottom: 20px;
//
// span {
// font-size: 16px;
// font-weight: 800;
// color: var(--color-text-primary);
// }
//}
}
}
</style>

View File

@@ -85,6 +85,7 @@ export default {
// 如果不想等,证明是 value 自己变化导致的, 需要重新渲染
if (valJson !== this.formJson) {
this.iValue = val
this.$log.debug('Sub form value changed, rerender form: ', this.formJson, valJson)
this.loading = true
setTimeout(() => {
this.loading = false
@@ -95,11 +96,12 @@ export default {
}
},
methods: {
outputValue: _.debounce(function(val) {
this.$emit('input', val)
}),
updateValue(val) {
this.iValue = val
setTimeout(() => {
this.$emit('input', val)
}, 100)
this.outputValue(val)
},
objectToString(obj) {
let data = ''

View File

@@ -7,18 +7,18 @@
v-bind="$attrs"
v-on="$listeners"
>
<span
<template
v-for="(group, i) in groups"
:key="'group-'+group.name"
:slot="'id:'+group.name"
>
<FormGroupHeader
v-if="!groupHidden(group, i)"
:key="'group-' + group.name"
:group="group"
:index="i"
:line="i !== 0 && !groupHidden(groups[i - 1], i - 1)"
/>
</span>
</template>
</DataForm>
</template>
@@ -163,5 +163,6 @@ export default {
return true
}
}
}
</script>

View File

@@ -1,7 +1,6 @@
import Vue from 'vue'
import ObjectSelect2 from '@/components/Form/FormFields/NestedObjectSelect2.vue'
import NestedField from '@/components/Form/AutoDataForm/components/NestedField.vue'
import Switcher from '@/components/Form/FormFields/Switcher.vue'
import rules from '@/components/Form/DataForm/rules'
import BasicTree from '@/components/Form/FormFields/BasicTree.vue'
import JsonEditor from '@/components/Form/FormFields/JsonEditor.vue'
@@ -65,8 +64,9 @@ export class FormFieldGenerator {
}
break
case 'boolean':
type = ''
field.component = Switcher
type = 'checkbox'
// field.component = Switcher
// field.type = 'checkbox'
break
case 'list':
type = 'input'
@@ -197,6 +197,24 @@ export class FormFieldGenerator {
return field
}
setChoicesTips(field, fieldMeta, fieldRemoteMeta) {
// 设置 checkbox 的 tips
if (['checkbox-group', 'radio-group'].indexOf(field.type) !== -1) {
field.options.map(option => {
if (!option.tip && field.tips) {
option.tip = field.tips[option.value]
}
if (!option.tip) {
const match = option.label.match(/^(.+?)\s*\((.*?)\)$/)
if (match) {
option.label = match[1]
option.tip = match[2]
}
}
})
}
}
afterGenerateField(field) {
field.label = toSentenceCase(field.label)
@@ -204,15 +222,7 @@ export class FormFieldGenerator {
field.el.placeholder = field.placeholder
}
// 设置 checkbox 的 tips
if (field.tips && ['checkbox-group', 'radio-group'].indexOf(field.type) !== -1) {
field.options.map(option => {
if (!option.tip && field.tips[option.value]) {
option.tip = field.tips[option.value]
}
})
}
this.setChoicesTips(field)
return field
}

View File

@@ -8,18 +8,20 @@
v-bind="data.attrs"
>
<template v-if="data.label" #label>
<span>{{ data.label }}</span>
<el-tooltip
v-if="data.helpTip"
:open-delay="500"
:tabindex="-1"
effect="dark"
placement="right"
popper-class="help-tips"
>
<div slot="content" v-sanitize="data.helpTip" /> <!-- Noncompliant -->
<i class="fa fa-question-circle-o help-tip-icon" />
</el-tooltip>
<span :title="data.label">
{{ data.label }}
<el-tooltip
v-if="data.helpTip"
:open-delay="500"
:tabindex="-1"
effect="dark"
placement="right"
popper-class="help-tips"
>
<div slot="content" v-sanitize="data.helpTip" /> <!-- Noncompliant -->
<i class="fa fa-question-circle-o help-tip-icon" />
</el-tooltip>
</span>
</template>
<template v-if="readonly && hasReadonlyContent">
<div
@@ -71,6 +73,7 @@
<el-tooltip v-if="opt.tip" :content="opt.tip" :open-delay="500" placement="top">
<i class="el-icon-warning-outline" />
</el-tooltip>
<span v-if="data.helpText">{{ data.helpText }}</span>
</el-checkbox>
<!-- WARNING: radio label 属性来表示 value 的含义 -->
<!-- FYI: radio value 属性可以在没有 radio-group 时用来关联到同一个 v-model -->
@@ -87,7 +90,7 @@
</el-radio>
</template>
</custom-component>
<div v-if="data.helpText" class="help-block">
<div v-if="data.helpText" :class="data.type" class="help-block">
<el-alert
v-if="data.helpText.startsWith('!')"
:closable="false"
@@ -99,6 +102,9 @@
</el-alert>
<span v-else v-sanitize="data.helpText" />
</div>
<div v-if="data.helpTextFormatter" class="help-block">
<RenderHelpTextSafe :render-content="data.helpTextFormatter" />
</div>
</el-form-item>
</template>
<script>
@@ -121,6 +127,18 @@ function validator(data) {
export default {
components: {
RenderHelpTextSafe: {
functional: true,
props: {
renderContent: {
type: Function,
required: true
}
},
render(h, { props }) {
return props.renderContent()
}
},
/**
* 🐂🍺只需要有组件选项对象,就可以立刻包装成函数式组件在 template 中使用
* FYI: https://cn.vuejs.org/v2/guide/render-function.html#%E5%87%BD%E6%95%B0%E5%BC%8F%E7%BB%84%E4%BB%B6
@@ -315,6 +333,10 @@ export default {
::v-deep .el-alert__icon {
font-size: 16px
}
&.checkbox {
//display: inline;
}
}
.help-tip-icon {

View File

@@ -1,65 +1,71 @@
<template>
<ElFormRender
:id="id"
ref="form"
:class="mobile ? 'mobile' : 'desktop'"
:content="fields"
:form="basicForm"
:label-position="labelPosition"
label-width="25%"
v-bind="$attrs"
v-on="$listeners"
>
<!-- slot 透传 -->
<slot
v-for="item in fields"
:slot="`id:${item.id}`"
:name="`id:${item.id}`"
/>
<slot
v-for="item in fields"
:slot="`$id:${item.id}`"
:name="`$id:${item.id}`"
/>
<div>
<ElFormRender
:id="id"
ref="form"
:class="[mobile ? 'mobile' : 'desktop']"
:content="fields"
:form="basicForm"
:label-position="iLabelPosition"
class="form-fields"
label-width="25%"
v-bind="$attrs"
v-on="$listeners"
>
<!-- slot 透传 -->
<slot
v-for="item in fields"
:slot="`id:${item.id}`"
:name="`id:${item.id}`"
/>
<slot
v-for="item in fields"
:slot="`$id:${item.id}`"
:name="`$id:${item.id}`"
/>
<el-form-item v-if="hasButtons" class="form-buttons">
<el-button
v-if="defaultButton"
:disabled="!canSubmit"
:loading="isSubmitting"
:size="submitBtnSize"
type="primary"
@click="submitForm('form')"
>
{{ iSubmitBtnText }}
</el-button>
<el-button
v-if="defaultButton && hasSaveContinue"
size="small"
@click="submitForm('form', true)"
>
{{ $t("SaveAndAddAnother") }}
</el-button>
<el-button
v-if="defaultButton && hasReset"
size="small"
@click="resetForm('form')"
>
{{ $t("Reset") }}
</el-button>
<el-button
v-for="button in moreButtons"
v-show="!button.hidden"
:key="button.title"
:loading="button.loading"
size="small"
v-bind="button"
@click="handleClick(button)"
>
{{ button.title }}
</el-button>
</el-form-item>
</ElFormRender>
<div v-if="hasButtons" class="form-buttons">
<el-button
v-if="defaultButton"
:disabled="!canSubmit"
:loading="isSubmitting"
:size="submitBtnSize"
type="primary"
@click="submitForm('form')"
>
{{ iSubmitBtnText }}
</el-button>
<el-button
v-if="defaultButton && hasSaveContinue"
size="small"
@click="submitForm('form', true)"
>
{{ $t("SaveAndAddAnother") }}
</el-button>
<el-button
v-if="defaultButton && hasReset"
size="small"
@click="resetForm('form')"
>
{{ $t("Reset") }}
</el-button>
<el-button
v-for="button in moreButtons"
v-show="!button.hidden"
:key="button.title"
:loading="button.loading"
size="small"
v-bind="button"
@click="handleClick(button)"
>
{{ button.title }}
</el-button>
</div>
</ElFormRender>
</div>
</template>
<script>
@@ -118,6 +124,10 @@ export default {
isSubmitting: {
type: Boolean,
default: false
},
labelPosition: {
type: String,
default: ''
}
},
data() {
@@ -134,7 +144,17 @@ export default {
mobile() {
return this.$store.state.app.device === 'mobile'
},
labelPosition() {
drawer() {
return this.$store.state.common.inDrawer
},
iLabelPosition() {
if (this.labelPosition) {
return this.labelPosition
}
// if (this.drawer) {
// return 'left'
// }
// return this.drawer || this.mobile ? 'top' : 'right'
return this.mobile ? 'top' : 'right'
}
},
@@ -208,21 +228,32 @@ export default {
color: var(--color-text-primary);
}
&.label-top {
::v-deep .el-form-item {
.el-form-item__content {
width: 100%;
}
}
}
::v-deep .el-form-item {
margin-bottom: 10px;
.item-params {
margin-top: 0;
}
.el-form-item__label {
padding: 0 30px 0 0;
line-height: 32px;
line-height: 30px;
color: var(--color-text-primary);
i {
color: var(--color-icon-primary);
}
span {
max-width: calc(100% - 25px);
white-space: nowrap; /* 禁止换行 */
text-overflow: ellipsis;
overflow: hidden;
}
}
.el-form-item__content {
@@ -298,8 +329,9 @@ export default {
}
}
::v-deep .el-form-item.form-buttons {
margin-top: 20px;
::v-deep .form-buttons {
margin-top: 30px;
margin-left: 25%;
}
}

View File

@@ -115,6 +115,73 @@ export default {
color: #999;
}
.el-tree > .el-tree-node:after {
border-top: none;
}
//节点有间隙,隐藏掉展开按钮就好了,如果觉得空隙没事可以删掉
.el-tree-node__expand-icon.is-leaf {
display: none;
}
.el-tree > .el-tree-node:before {
border-left: none;
display: none;
}
.el-tree > .el-tree-node:after {
border-top: none;
display: none;
}
.el-tree-node__children {
padding-left: 13px;
.el-tree-node {
position: relative;
padding-left: 13px;
&:before {
content: "";
left: -4px;
position: absolute;
right: auto;
border-width: 1px;
}
&:first-child::before {
display: none;
}
&:last-child:before {
height: 38px;
}
&:before {
border-left: 1px dashed #dcdcdc;
bottom: 0;
height: 100%;
top: -26px;
width: 1px;
}
&:after {
content: "";
left: -4px;
position: absolute;
right: auto;
border-width: 1px;
}
&:after {
border-top: 1px dashed #dcdcdc;
height: 20px;
top: 12px;
width: 24px;
}
}
}
.el-tree-node__content:hover {
background-color: transparent;
}
@@ -129,7 +196,7 @@ export default {
}
.el-tree-node__children {
margin-left: -25px;
//margin-left: -25px;
}
}
}

View File

@@ -8,99 +8,118 @@
:label="item.name"
:prop="item.name"
>
<template v-if="item.type === 'button' && !item.isVisible">
<el-button
:type="item.el && item.el.type"
class="start-stop-btn"
size="mini"
@click="item.callback()"
>
<i :class="item.icon" />{{ item.name }}
</el-button>
<el-tooltip :disabled="!item.tip" :content="item.tip">
<el-button
:type="item.el && item.el.type"
class="start-stop-btn"
size="mini"
@click="item.callback()"
>
<i :class="item.icon" />
{{ item.name }}
</el-button>
</el-tooltip>
</template>
<template v-if="item.type === 'input' && item.el && item.el.autoComplete">
<el-autocomplete
v-model="formModel[item.name]"
:fetch-suggestions="item.el.query"
:placeholder="item.placeholder"
class="inline-input"
size="mini"
@change="handleInputChange(item)"
@select="handleInputChange(item)"
/>
<el-tooltip :disabled="!item.tip" :content="item.tip">
<el-autocomplete
v-model="formModel[item.name]"
:fetch-suggestions="item.el.query"
:placeholder="item.placeholder"
class="inline-input"
size="mini"
clearable
@change="handleInputChange(item)"
@select="handleInputChange(item)"
/>
</el-tooltip>
</template>
<template v-else-if="item.type === 'input'">
<el-input
v-model="formModel[item.name]"
:class="!isFold ? 'special-style' : ''"
:placeholder="item.placeholder"
class="inline-input"
size="mini"
@change="item.callback(formModel[item.name])"
/>
<el-tooltip :disabled="!item.tip" :content="item.tip">
<el-input
v-model="formModel[item.name]"
:class="!isFold ? 'special-style' : ''"
:placeholder="item.placeholder"
class="inline-input"
size="mini"
@change="item.callback(formModel[item.name])"
/>
</el-tooltip>
</template>
<template v-if="item.type === 'select' && item.el && item.el.create">
<span class="filter-label">{{ item.name }}:</span>
<el-select
v-if="item.type === 'select' && item.el && item.el.create"
:key="index"
v-model="formModel[item.name]"
:allow-create="item.el.create || false"
:filterable="item.el.create || false"
:multiple="item.el.multiple"
:placeholder="item.name"
class="autoWidth-select"
default-first-option
size="mini"
@change="item.callback(item.value)"
>
<template slot="prefix">{{ item.label + ':' + item.value }}</template>
<el-option
v-for="(option, id) in item.options"
:key="id"
:label="option.label"
:title="option.value"
:value="option.value"
/>
</el-select>
<el-tooltip :disabled="!item.tip" :content="item.tip">
<span class="filter-label">{{ item.name }}:</span>
<el-select
v-if="item.type === 'select' && item.el && item.el.create"
:key="index"
v-model="formModel[item.name]"
:allow-create="item.el.create || false"
:filterable="item.el.create || false"
:multiple="item.el.multiple"
:placeholder="item.name"
class="autoWidth-select"
default-first-option
size="mini"
@change="item.callback(item.value)"
>
<template slot="prefix">{{ item.label + ':' + item.value }}</template>
<el-option
v-for="(option, id) in item.options"
:key="id"
:label="option.label"
:title="option.value"
:value="option.value"
/>
</el-select>
</el-tooltip>
</template>
<template v-if="item.type === 'select' && (!item.el || !item.el.create)">
<el-dropdown
class="select-dropdown"
trigger="click"
@command="(command) => {
item.value = command
item.callback(command)
}"
>
<el-button size="mini" type="primary">
<div class="text-content">
<span class="content">
{{ getLabel(item.value, item.options) }}
<i class="el-icon-arrow-down el-icon--right" />
</span>
</div>
</el-button>
<el-dropdown-menu v-slot="dropdown">
<el-dropdown-item
v-for="(option, i) in item.options"
:key="i"
:command="option.value"
>
{{ option.label }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<el-tooltip :disabled="!item.tip" :content="item.tip">
<el-dropdown
class="select-dropdown"
trigger="click"
@command="(command) => {
item.value = command
item.callback(command)
}"
>
<el-button size="mini" type="primary">
<div class="text-content">
<span class="content">
{{ getLabel(item.value, item.options) }}
<i class="el-icon-arrow-down el-icon--right" />
</span>
</div>
</el-button>
<el-dropdown-menu v-slot="dropdown">
<el-dropdown-item
v-for="(option, i) in item.options"
:key="i"
:command="option.value"
>
{{ option.label }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</el-tooltip>
</template>
<template v-if="item.type === 'switch'">
<el-switch
v-model="formModel[item.name]"
:active-text="item.name"
:disabled="item.disabled"
@change="item.callback(formModel[item.name])"
/>
<el-tooltip :disabled="!item.tip" :content="item.tip">
<el-switch
v-model="formModel[item.name]"
:active-text="item.name"
:disabled="item.disabled"
@change="item.callback(formModel[item.name])"
/>
</el-tooltip>
</template>
</el-form-item>
<div
@@ -134,7 +153,13 @@
</div>
</div>
</el-form>
<codemirror ref="myCm" v-model="iValue" :options="iOptions" class="editor" />
<codemirror
ref="myCm"
v-model="iValue"
:options="iOptions"
class="editor"
:style="iActions.length > 0 ? { marginLeft: '30px' } : {}"
/>
</div>
</template>
@@ -208,7 +233,7 @@ export default {
Object.values(actionsObj).forEach(action => {
if (action.name === this.$t('RunAs') && action.type === 'input') {
rules[action.name] = [{ required: true, message: '请输入运行用户', trigger: 'blur' }]
rules[action.name] = [{ required: true, message: this.$t('RequiredRunas'), trigger: 'blur' }]
}
})
@@ -373,7 +398,6 @@ $input-border-color: #C0C4CC;
}
.editor {
margin-left: 30px;
border: 1px solid var(--color-border);
overflow: hidden;
}

View File

@@ -5,11 +5,11 @@
:default-time="['00:00:01', '23:59:59']"
:end-placeholder="$tc('DateEnd')"
:picker-options="pickerOptions"
:range-separator="$tc('To')"
:start-placeholder="$tc('DateStart')"
:type="type"
class="datepicker"
range-separator="-"
size="small"
type="datetimerange"
v-bind="$attrs"
@change="handleDateChange"
v-on="$listeners"
@@ -28,6 +28,15 @@ export default {
dateEnd: {
type: [Number, String, Date],
default: null
},
type: {
type: String,
default: 'daterange'
// default: 'datetimerange'
},
toMinMax: {
type: Boolean,
default: true
}
},
data() {
@@ -35,6 +44,10 @@ export default {
const endValue = this.dateEnd || this.$route.query['date_end']
const dateStart = new Date(startValue)
const dateTo = new Date(endValue)
if (this.toMinMax) {
dateStart.setHours(0, 0, 0, 0)
dateTo.setHours(23, 59, 59, 999)
}
return {
value: [dateStart, dateTo],
pickerOptions: {
@@ -74,9 +87,13 @@ export default {
}
},
onShortcutClick(picker, day) {
const end = new Date()
const start = new Date()
let start = new Date()
let end = new Date()
start.setTime(start.getTime() - 3600 * 1000 * 24 * day)
if (this.toMinMax) {
start = new Date(start.setHours(0, 0, 0, 0))
end = new Date(end.setHours(23, 59, 59, 999))
}
picker.$emit('pick', [start, end])
}
}
@@ -85,8 +102,11 @@ export default {
<style lang='scss' scoped>
.datepicker {
&.el-date-editor--daterange.el-input__inner {
width: 243px;
}
margin-left: 10px;
width: 233px;
border: 1px solid #dcdee2;
border-radius: 2px;
height: 28px;

View File

@@ -13,7 +13,7 @@
<script>
import Dialog from '@/components/Dialog/index.vue'
import ListTable from '@/components/Table/ListTable/index.vue'
import { DrawerListTable as ListTable } from '@/components'
export default {
name: 'AttrMatchResultDialog',

View File

@@ -5,7 +5,9 @@
v-bind="iAttrs"
@input="handleInput"
v-on="$listeners"
/>
>
hello
</Password>
</template>
<script>

View File

@@ -54,22 +54,22 @@ export default {
{
id: 'uppercase',
label: this.$t('Uppercase'),
type: 'switch'
type: 'checkbox'
},
{
id: 'lowercase',
label: this.$t('Lowercase'),
type: 'switch'
type: 'checkbox'
},
{
id: 'digit',
label: this.$t('Digit'),
type: 'switch'
type: 'checkbox'
},
{
id: 'symbol',
label: this.$t('SpecialSymbol'),
type: 'switch'
type: 'checkbox'
},
{
id: 'exclude_symbols',

View File

@@ -1,21 +1,20 @@
<template>
<div>
<el-input v-model="rawValue.phone" required :placeholder="$tc('InputPhone')" @input="OnInputChange">
<el-input v-model="rawValue.phone" :placeholder="$tc('InputPhone')" required @input="onInputChange">
<el-select
slot="prepend"
:placeholder="$tc('Select')"
:value="rawValue.code"
style="width: 75px;"
@change="OnChange"
style="width: 105px;"
@change="onChange"
>
<el-option
v-for="country in countries"
:key="country.value"
:key="country.name"
:label="country.value"
:value="country.value"
style="width: 200px;"
>
<span style="float: left">{{ country.name }}</span>
<span class="country-name">{{ country.name }}</span>
<span style="float: right; font-size: 13px">{{ country.value }}</span>
</el-option>
</el-select>
@@ -24,19 +23,19 @@
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'PhoneInput',
props: {
value: {
type: [Object, String],
default: () => ({ 'code': '+86', 'phone': '' })
default: null
}
},
data() {
return {
rawValue: {}
rawValue: {},
countries: [{ 'name': 'China', 'value': '+86' }]
}
},
computed: {
@@ -45,26 +44,38 @@ export default {
return ''
}
return `${this.rawValue.code}${this.rawValue.phone}`
},
countries: {
get() {
return this.publicSettings.COUNTRY_CALLING_CODES
}
},
...mapGetters(['publicSettings'])
}
},
mounted() {
this.rawValue = this.value || { code: '+86', phone: '' }
const defaults = { code: localStorage.getItem('prePhoneCode') || '+86', phone: '' }
this.rawValue = this.value || defaults
this.$axios.get('/api/v1/common/countries/').then(res => {
this.countries = res.map(item => {
return { name: `${item.flag} ${item.name}`, value: item.phone_code }
})
})
this.$emit('input', this.fullPhone)
},
methods: {
OnChange(countryCode) {
onChange(countryCode) {
this.rawValue.code = countryCode
this.OnInputChange()
this.onInputChange()
localStorage.setItem('prePhoneCode', countryCode)
},
OnInputChange() {
onInputChange() {
this.$emit('input', this.fullPhone)
}
}
}
</script>
<style scoped>
.country-name {
display: inline-block;
width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-right: 5px;
}
</style>

View File

@@ -144,9 +144,6 @@ export default {
watch: {
choices: {
handler(value, oldValue) {
if (value?.length === oldValue?.length) {
return
}
this.loading = true
setTimeout(() => {
this.setDefaultItems(value)
@@ -345,6 +342,10 @@ export default {
.protocol-item {
display: flex;
margin: 5px 0;
&:first-of-type {
margin-top: 0;
}
}
.input-button {

View File

@@ -125,16 +125,19 @@ export default {
allowCreate: {
type: Boolean,
default: false
},
defaultPageSize: {
type: Number,
default: 10
}
},
data() {
const vm = this
const defaultPageSize = 10
const defaultParams = {
search: '',
page: 1,
hasMore: true,
pageSize: defaultPageSize
pageSize: vm.defaultPageSize
}
// 设置axios全局报错提示不显示
const validateStatus = (status) => {
@@ -194,7 +197,6 @@ export default {
}
},
iAjax() {
const defaultPageSize = 10
const defaultMakeParams = (params) => {
const page = params.page || 1
const offset = (page - 1) * params.pageSize
@@ -237,7 +239,7 @@ export default {
}
const defaultAjax = {
url: '',
pageSize: defaultPageSize,
pageSize: this.defaultPageSize,
makeParams: defaultMakeParams,
transformOption: defaultTransformOption,
processResults: defaultProcessResults,

View File

@@ -14,10 +14,12 @@
:close-on-click-modal="false"
:title="label"
:visible.sync="showTransfer"
:disabled-status="!isLoaded"
class="the-dialog"
width="730px"
@cancel="handleTransCancel"
@confirm="handleTransConfirm"
v-on="$listeners"
>
<krryPaging v-if="selectInitialized" ref="pageTransfer" class="transfer" v-bind="pagingTransfer" />
</Dialog>
@@ -77,13 +79,16 @@ export default {
if (keyword) {
params['search'] = keyword
}
this.isLoaded = false
const data = await this.$axios.get(url, { params })
this.isLoaded = true
return data['results'].map(item => {
const n = transformOption(item)
return { id: n.value, label: n.label }
})
}
return {
isLoaded: false,
showTransfer: false,
selectInitialized: false,
select2: {
@@ -167,7 +172,3 @@ export default {
}
}
</script>
<style scoped>
</style>

View File

@@ -1,21 +1,27 @@
<template>
<div>
<div class="update-token">
<el-button v-show="!isShow" icon="el-icon-edit" type="text" @click="isShow=true">
{{ text }}
</el-button>
<el-input
v-show="isShow"
v-model.trim="curValue"
:disabled="disabled"
:placeholder="placeholder"
:type="type"
autocomplete="new-password"
class="password-input"
show-password
@change="onChange"
/>
<el-button :disabled="disabled" size="small" type="text" @click="randomPassword">
<i class="fa fa-refresh" />
</el-button>
</div>
</template>
<script>
import { randomString } from '@/utils/string'
export default {
props: {
value: {
@@ -39,6 +45,10 @@ export default {
placeholder: {
type: String,
default: () => ''
},
disabled: {
type: Boolean,
default: false
}
},
data() {
@@ -55,7 +65,24 @@ export default {
methods: {
onChange(e) {
this.$emit('input', this.curValue)
},
randomPassword() {
this.curValue = randomString(24, true)
this.$emit('input', this.curValue)
}
}
}
</script>
<style lang='scss' scoped>
.password-input {
width: calc(100% - 50px);
}
.update-token {
i {
color: var(--color-text-secondary);
font-size: 14px;
}
}
</style>

View File

@@ -8,7 +8,7 @@
<div v-if="tip !== ''" class="help-block">{{ tip }}</div>
<input v-model="value" hidden type="text" v-on="$listeners">
<div>
<img :class="showBG ? 'show-bg' : ''" :src="preview" v-bind="$attrs">
<img v-if="preview" :class="showBG ? 'show-bg' : ''" :src="preview" v-bind="$attrs" alt="">
</div>
</div>
</template>

View File

@@ -1,7 +1,7 @@
<template>
<div class="c-weektime">
<div class="c-schedue" />
<div :class="{'c-schedue': true, 'c-schedue-notransi': mode}" :style="styleValue" />
<div :class="{'c-schedue': true, 'c-schedue-notransi': mode}" />
<table :class="{'c-min-table': colspan < 2}" class="c-weektime-table">
<thead class="c-weektime-head">
<tr>
@@ -14,7 +14,7 @@
</tr>
</thead>
<tbody class="c-weektime-body" @mouseleave="containerLeave()">
<tr v-for="t in weektimeData" :key="t.row">
<tr v-for="t in weekTimeData" :key="t.row">
<td>{{ t.value }}</td>
<td
v-for="n in t.child"
@@ -45,6 +45,7 @@
const createArr = len => {
return Array.from(Array(len)).map((ret, id) => id)
}
function splicing(list) {
let same
let i = -1
@@ -67,6 +68,7 @@ function splicing(list) {
arr.shift()
return arr.join('')
}
export default {
name: 'WeekCronSelect',
props: {
@@ -77,7 +79,7 @@ export default {
colspan: {
type: Number,
default() {
return 2
return 1
}
}
},
@@ -100,7 +102,7 @@ export default {
this.$t('Saturday'),
this.$t('Sunday')
],
weektimeData: [],
weekTimeData: [],
timeRange: [] // 格式化之后数据
}
},
@@ -142,10 +144,11 @@ export default {
return {
value: ret,
row: index,
child: children(ret, index, 48)
child: children(ret, index, 24 * this.colspan)
}
})
this.weektimeData = isData
console.log('isData', isData)
this.weekTimeData = isData
},
// 反解析传递过来的默认值
nextValue() {
@@ -169,7 +172,7 @@ export default {
const startVal = this.countIndex(start)
const endVal = this.countIndex(end)
for (let i = startVal; i < (endVal === 0 ? 48 : endVal); i++) {
const curWeek = this.weektimeData[idNum]
const curWeek = this.weekTimeData[idNum]
curWeek.child[i].check = true
}
},
@@ -209,8 +212,9 @@ export default {
const nowDate = new Date(timeStamp).getTime()
const targetStamp = new Date(nowDate + offsetGMT * 60 * 1000 + timezone * 60 * 60 * 1000).getTime()
const beginStamp = targetStamp + col * 1800000 // col * 30 * 60 * 1000
const endStamp = beginStamp + 1800000
// (2 / this.colspan) 原来是一个单元格 30分钟现在是一个单元格 30 * 2 / this.colspan 分钟
const beginStamp = targetStamp + col * 1800000 * (2 / this.colspan) // col * 30 * 60 * 1000
const endStamp = beginStamp + 1800000 * (2 / this.colspan)
const begin = this.formatDate(new Date(beginStamp), 'hh:mm')
const end = this.formatDate(new Date(endStamp), 'hh:mm')
@@ -218,7 +222,7 @@ export default {
},
// 清空时间段
clearWeektime() {
this.weektimeData.forEach(item => {
this.weekTimeData.forEach(item => {
item.child.forEach(t => {
this.$set(t, 'check', false)
})
@@ -228,7 +232,7 @@ export default {
},
// 全选
selectAll() {
this.weektimeData.forEach(item => {
this.weekTimeData.forEach(item => {
item.child.forEach(t => {
this.$set(t, 'check', true)
})
@@ -241,12 +245,15 @@ export default {
this.mode = 0
},
setTimeRange() {
this.timeRange = this.weektimeData.map(item => {
this.timeRange = this.weekTimeData.map(item => {
console.log('item', item)
console.log('Value', splicing(item.child))
return {
id: item.row === 6 ? 0 : item.row + 1,
value: splicing(item.child)
}
})
console.log('Time range: ', this.timeRange)
this.$emit('change', this.timeRange)
},
cellEnter(item) {
@@ -308,7 +315,7 @@ export default {
selectWeek(row, col, check) {
const [minRow, maxRow] = row
const [minCol, maxCol] = col
this.weektimeData.forEach(item => {
this.weekTimeData.forEach(item => {
item.child.forEach(t => {
if (t.row >= minRow && t.row <= maxRow && t.col >= minCol && t.col <= maxCol) {
this.$set(t, 'check', check)
@@ -321,11 +328,12 @@ export default {
</script>
<style lang="scss" scoped>
.c-weektime {
min-width: 640px;
//min-width: 440px;
position: relative;
display: inline-block;
padding-right: 20px;
}
.c-schedue {
background: #598fe6;
position: absolute;
@@ -334,55 +342,70 @@ export default {
opacity: .6;
pointer-events: none;
}
.c-schedue-notransi {
transition: width .12s ease, height .12s ease, top .12s ease, left .12s ease;
}
.c-weektime-table {
border-collapse: collapse;
th {
vertical-align: inherit;
font-weight: bold;
}
tr {
height: 30px;
}
tr, td, th {
user-select: none;
border: 1px solid #dee4f5;
text-align: center;
min-width: 12px;
min-width: 10px;
line-height: 1.6em;
transition: background .16s ease;
}
.c-weektime-head {
font-size: 12px;
.week-td {
width: 72px;
}
}
.c-weektime-body {
font-size: 12px;
td {
&.weektime-atom-item {
user-select: unset;
background-color: #f5f5f5;
width: 18px;
}
&.ui-selected {
background-color: #598fe6;
}
}
}
.c-weektime-preview {
line-height: 2.4em;
padding: 0 10px;
font-size: 13px;
font-size: 11px;
.c-weektime-con {
line-height: 42px;
user-select: none;
}
.c-weektime-time {
text-align: left;
line-height: 2.4em;
p {
max-width: 625px;
line-height: 1.4em;
@@ -392,11 +415,13 @@ export default {
}
}
}
.c-min-table {
tr, td, th {
min-width: 24px;
min-width: 17px;
}
}
.g-clearfix {
&:after, &:before {
clear: both;
@@ -404,16 +429,20 @@ export default {
display: table;
}
}
.g-pull-left {
float: left;
}
.g-pull-right {
float: right;
color: #409eff!important;
color: #409eff !important;
}
.g-pull-margin {
margin-right: 12px;
}
.g-tip-text {
color: #999;
}

View File

@@ -1,7 +1,13 @@
<template>
<div class="form-group-header">
<div ref="formGroup" class="form-group-header">
<div v-if="line" class="hr-line-dashed" />
<h3>{{ group['title'] }} </h3>
<h3 @click="toggle">{{ group['title'] }} </h3>
<span class="compass" @click="toggle">
<i :class="iconClass" />
</span>
<div v-if="!isVisible" class="ellipsis" @click="toggle">
<i class="el-icon-more-outline" />
</div>
</div>
</template>
@@ -20,16 +26,62 @@ export default {
type: Object,
default: () => ({})
}
},
data() {
return {
isVisible: true
}
},
computed: {
iconClass() {
return this.isVisible ? 'el-icon-arrow-down' : 'el-icon-arrow-up'
}
},
methods: {
toggle() {
this.isVisible = !this.isVisible
this.toggleSiblingVisibility()
},
toggleSiblingVisibility() {
// 当前 form-group-header 的 DOM 元素
const formGroupHeader = this.$refs.formGroup
if (!formGroupHeader) return
// 找到当前 form-group-header 的下一个兄弟节点
let sibling = formGroupHeader.nextElementSibling
// 循环隐藏或显示直到找到下一个 form-group-header
while (sibling && sibling.classList.contains('el-form-item')) {
sibling.style.display = this.isVisible ? '' : 'none'
sibling = sibling.nextElementSibling
}
}
}
}
</script>
<style lang="less" scoped>
.hr-line-dashed {
border-top: 1px dashed #e7eaec;
color: #ffffff;
background-color: #ffffff;
height: 1px;
margin: 20px 0;
border-top: 1px dashed #e7eaec;
color: #ffffff;
background-color: #ffffff;
height: 1px;
margin: 20px 0;
}
h3 {
display: inline-block;
cursor: pointer;
}
.compass {
display: inline-block;
float: right;
cursor: pointer;
}
.ellipsis {
text-align: center;
cursor: pointer;
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<el-card :class="'ibox ' + type" shadow="never" v-bind="$attrs">
<el-card :class="'ibox ' + type" :shadow="shadow" v-bind="$attrs">
<template #header>
<slot name="header">
<div v-if="title" slot="header" class="clearfix ibox-title">
@@ -26,6 +26,10 @@ export default {
type: {
type: String,
default: 'default'
},
shadow: {
type: String,
default: 'never'
}
},
computed: {

View File

@@ -39,32 +39,32 @@ export default {
</script>
<style scoped>
.quick-actions ::v-deep table {
width: 100%;
}
.quick-actions ::v-deep table {
width: 100%;
}
.quick-actions ::v-deep tr > td {
line-height: 1.43;
padding: 8px;
vertical-align: top;
font-size: 13px;
width: 50%;
}
.quick-actions ::v-deep tr > td {
line-height: 1.43;
padding: 8px 0;
vertical-align: top;
font-size: 13px;
width: 50%;
}
.quick-actions ::v-deep tr > td > span:last-child {
float: right;
}
.quick-actions ::v-deep tr > td > span:last-child {
float: right;
}
.quick-actions ::v-deep button {
padding: 4px 5px;
font-size: 13px;
width: 65px;
.quick-actions ::v-deep button {
padding: 4px 5px;
font-size: 13px;
min-width: 65px;
span {
overflow: hidden;
white-space: nowrap; /* 控制文本不换行 */
text-overflow: ellipsis;
display: block;
}
span {
overflow: hidden;
white-space: nowrap; /* 控制文本不换行 */
text-overflow: ellipsis;
display: block;
}
}
</style>

View File

@@ -90,8 +90,14 @@ export default {
}
}
},
beforeDestroy() {
this.$eventBus.$off('showColumnSettingPopover', this.showColumnSettingPopoverHandler)
},
mounted() {
this.$eventBus.$on('showColumnSettingPopover', ({ url }) => {
this.$eventBus.$on('showColumnSettingPopover', this.showColumnSettingPopoverHandler)
},
methods: {
showColumnSettingPopoverHandler({ url }) {
if (url === this.url) {
this.checkAll = false
this.showColumnSettingPopover = true
@@ -105,9 +111,7 @@ export default {
this.checkAll = false
this.isIndeterminate = true
}
})
},
methods: {
},
handleColumnConfirm() {
this.showColumnSettingPopover = false
this.$emit('columnsUpdate', { columns: this.iCurrentColumns, url: this.url })

View File

@@ -187,7 +187,7 @@ export default {
break
case 'datetime':
col.formatter = DateFormatter
col.width = '175px'
col.width = '155px'
break
case 'object_related_field':
col.formatter = ObjectRelatedFormatter
@@ -348,6 +348,8 @@ export default {
let configColumns = config.columns || allColumnNames
const columnsExclude = config.columnsExclude || []
const columnsAdd = config.columnsAdd || []
configColumns = configColumns.concat(columnsAdd)
configColumns = configColumns.filter(item => !columnsExclude.includes(item))
// 解决后端 API 返回字段中包含 actions 的问题;

View File

@@ -0,0 +1,178 @@
<template>
<div class="account-panel">
<el-row :gutter="20">
<el-col :span="21">
<div class="title">
<span>{{ object.name }}</span>
</div>
</el-col>
<el-col v-if="iActions.length !== 0" :span="3" @click.native="handleClick($event)">
<el-dropdown>
<el-link :underline="false" type="primary">
<i class="el-icon-more el-icon--right" style="color: var(--color-text-primary)" />
</el-link>
<el-dropdown-menu default="dropdown">
<el-dropdown-item
v-for="action in iActions"
:key="action.name"
:disabled="action.disabled"
@click.native="action.callback(object)"
>
<i v-if="action.icon" :class="action.icon" /> {{ action.name }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</el-col>
</el-row>
<el-row :gutter="20" class="panel-content">
<el-col :span="6" class="panel-image">
<el-image :src="imageUrl" fit="contain" />
</el-col>
<el-col :span="18" class="panel-info">
<InfoPanel
v-for="(obj, index) in getInfos(object)"
:key="index"
:content="obj.content"
:title="obj.title"
/>
</el-col>
</el-row>
</div>
</template>
<script>
import InfoPanel from './InfoPanel'
export default {
name: 'CardPanel',
components: {
InfoPanel
},
props: {
tableConfig: {
type: Object,
default: () => ({})
},
object: {
type: Object,
required: true
},
actions: {
type: Array,
default: () => []
},
infos: {
type: Array,
default: () => []
},
getImage: {
type: Function,
default: (obj) => ''
},
getInfos: {
type: Function,
default: (obj) => []
},
handleUpdate: {
type: Function,
default: () => {
}
}
},
data() {
return {
defaultActions: [
{
id: 'update',
name: this.$tc('Update'),
icon: 'el-icon-edit',
callback: this.handleUpdate,
disabled: this.isDisabled('change')
},
{
id: 'delete',
name: this.$tc('Delete'),
icon: 'el-icon-delete',
callback: this.handleDelete,
disabled: this.isDisabled('delete')
}
]
}
},
computed: {
imageUrl() {
return this.getImage(this.object)
},
iActions() {
const mergedActions = new Map()
this.defaultActions.forEach(a => {
mergedActions.set(a.id, { ...a })
})
this.actions.forEach(a => {
mergedActions.set(a.id, { ...a })
})
return Array.from(mergedActions.values())
}
},
methods: {
isDisabled(action) {
const app = this.tableConfig.permissions?.app
const resource = this.tableConfig.permissions?.resource
return !this.$hasPerm(`${app}.${action}_${resource}`)
},
handleClick(event) {
event.stopPropagation()
},
handleDelete() {
const url = this.tableConfig.url
this.$confirm(this.$tc('DeleteConfirmMessage'), this.$tc('Delete'), {
confirmButtonText: this.$tc('Confirm'),
cancelButtonText: this.$tc('Cancel'),
type: 'warning'
}).then(() => {
this.$axios.delete(`${url}${this.object.id}/`).then(() => {
this.$message({
type: 'success',
message: this.$tc('DeleteSuccess')
})
this.$emit('refresh')
})
})
}
}
}
</script>
<style lang="scss" scoped>
.account-panel {
display: flex;
flex-direction: column;
//height: 100%;
cursor: pointer;
.title {
text-align: left;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 1.1em;
color: #555555;
}
.panel-content {
display: flex;
height: 100px;
padding: 10px 0;
.panel-image {
margin: auto 5px;
}
}
.el-divider--horizontal {
margin: 5px 0;
}
}
</style>

View File

@@ -1,9 +1,7 @@
<template>
<div class="panel-item">
<span class="item-label">{{ title }} </span>
<el-link :underline="false" class="item-value">
<span class="content">{{ content }}</span>
</el-link>
<span :title="content" class="text-info">{{ content || '' }}</span>
</div>
</template>
@@ -38,14 +36,22 @@ export default {
.panel-item {
text-align: left;
padding: 5px 0;
padding: 3px 0;
line-height: 20px;
@include textOverflow;
.item-label {
text-align: left;
display: inline-block;
width: 100px;
width: 35%;
}
.item-label::after {
content: ':';
margin-left: 1px;
}
.text-info {
@include textOverflow;
}
}

View File

@@ -0,0 +1,44 @@
<template>
<CardTable
ref="table"
:columns="3"
:table-config="tableConfig"
v-bind="$attrs"
>
<template v-slot:default="slotProps">
<CardPanel :object="slotProps.item" :table-config="tableConfig" v-bind="subComponentProps" />
</template>
</CardTable>
</template>
<script type="text/jsx">
import CardTable from '@/components/Table/CardTable/index.vue'
import CardPanel from './CardPanel.vue'
export default {
name: 'SmallCard',
components: {
CardPanel,
CardTable
},
props: {
tableConfig: {
type: Object,
default: () => ({})
},
subComponentProps: {
type: Object,
default: () => ({})
}
},
data() {
return {
}
},
methods: {
reloadTable() {
this.$refs.table.reloadTable()
}
}
}
</script>

View File

@@ -10,7 +10,7 @@
<IBox v-if="totalData.length === 0">
<el-empty :description="$t('NoData')" :image-size="200" class="no-data" style="padding: 20px" />
</IBox>
<el-col v-for="(d, index) in totalData" :key="index" :lg="8" :md="12" :sm="24" style="min-width: 335px;">
<el-col v-for="(d, index) in totalData" :key="index" :lg="8" :md="12" :sm="24" class="el-col">
<el-card
:body-style="{ 'text-align': 'center', 'padding': '15px' }"
:class="{'is-disabled': isDisabled(d)}"
@@ -19,8 +19,7 @@
@click.native="onView(d)"
>
<keep-alive>
<component :is="subComponent" v-if="subComponent" :object="d" @refresh="getList" />
<slot v-else :index="index" :item="d">
<slot :index="index" :item="d">
<span v-if="d.edition === 'enterprise'" class="enterprise">
{{ $t('Enterprise') }}
</span>
@@ -85,6 +84,10 @@ export default {
},
props: {
// 定义 table 的配置
columns: {
type: Number,
default: 3
},
tableConfig: {
type: Object,
default: () => ({})
@@ -100,6 +103,10 @@ export default {
subComponent: {
type: Object,
default: () => null
},
subComponentProps: {
type: Object,
default: () => ({})
}
},
data() {
@@ -371,6 +378,10 @@ export default {
border-top: 1px solid #e7eaec;
}
.el-col {
//min-width: 330px; 设置完后remote app 列表会有问题
}
.no-data {
display: flex;
flex-direction: column;

View File

@@ -105,14 +105,14 @@
<template #header>
<span :title="col.label">{{ col.label }}</span>
</template>
<template v-if="col.formatter && typeof col.formatter !== 'function'" v-slot:default="{row, column, index}">
<template v-if="col.formatter && typeof col.formatter !== 'function'" v-slot:default="{row, column, $index}">
<div
:is="col.formatter"
:key="row.id"
:cell-value="row[col.prop]"
:col="col"
:column="column"
:index="index"
:index="$index"
:reload="getList"
:row="row"
:table-data="data"

View File

@@ -153,6 +153,8 @@ export default {
this.toggleRowSelection(row, true)
}
}
this.$emit('loaded')
},
handleSizeChange(val) {
localStorage.setItem('paginationSize', val)

View File

@@ -0,0 +1,79 @@
<template>
<Drawer
:component="component"
:component-listeners="listener"
:size="drawerSize"
:title="title"
:visible.sync="iVisible"
append-to-body
class="form-drawer"
destroy-on-close
v-bind="props"
@close="closeDrawer"
v-on="$listeners"
/>
</template>
<script>
import Drawer from '@/components/Drawer/index.vue'
export default {
components: { Drawer },
props: {
visible: {
type: Boolean,
required: true
},
title: {
type: String,
default: ''
},
component: {
type: [String, Function],
required: true
},
props: {
type: Object,
default: () => ({})
},
action: {
type: String,
default: ''
}
},
data() {
return {
listener: {
...this.$listeners
}
}
},
computed: {
drawerSize() {
const drawerWidth = localStorage.getItem('drawerWidth')
if (drawerWidth && drawerWidth > 100 && drawerWidth < 2000) {
return drawerWidth + 'px'
}
const width = window.innerWidth
if (width >= 800) return '767px'
return '90%'
},
iVisible: {
get() {
return this.visible
},
set(val) {
this.$emit('update:visible', val)
}
}
},
methods: {
closeDrawer() {
this.iVisible = false
}
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,235 @@
<template>
<div>
<ListTable
ref="ListTable"
:header-actions="iHeaderActions"
:table-config="iTableConfig"
v-bind="$attrs"
/>
<PageDrawer
:action="action"
:class="[action]"
:component="drawerComponent"
:props="drawerProps"
:title="drawerTitle"
:visible.sync="drawerVisible"
class="page-drawer"
/>
</div>
</template>
<script>
import ListTable from '../ListTable'
import PageDrawer from './PageDrawer.vue'
import { setUrlParam, toLowerCaseExcludeAbbr, toSentenceCase } from '@/utils/common'
import { mapGetters } from 'vuex'
const drawerType = [String, Function]
export default {
name: 'GenericListPage',
components: {
ListTable, PageDrawer
},
props: {
detailDrawer: {
type: drawerType,
default: ''
},
createDrawer: {
type: drawerType,
default: ''
},
updateDrawer: {
type: drawerType,
default: ''
},
tableConfig: {
type: Object,
required: true
},
headerActions: {
type: Object,
required: true
},
drawerProps: {
type: Object,
default: () => ({})
},
reloadOrderQuery: {
type: String,
default: '-date_updated'
},
resource: {
type: String,
default: ''
},
getDrawerTitle: {
type: Function,
default: null
}
},
data() {
return {
title: '',
action: '',
visible: false,
drawerVisible: false,
drawerComponent: ''
}
},
computed: {
...mapGetters(['inDrawer']),
drawerTitle() {
return this.getDefaultTitle()
},
iHeaderActions() {
const actions = this.headerActions
if (!actions.onCreate) {
actions.onCreate = this.onCreate
}
return actions
},
iTableConfig() {
const config = {
...this.tableConfig
}
const actionMap = {
'columnsMeta.actions.formatterArgs.onUpdate': this.onUpdate,
'columnsMeta.actions.formatterArgs.onClone': this.onClone,
'columnsMeta.name.formatterArgs.drawer': true,
'columnsMeta.name.formatterArgs.drawerComponent': this.detailDrawer
}
for (const [key, value] of Object.entries(actionMap)) {
if (_.get(config, key)) {
continue
}
_.set(config, key, value)
}
const columnsMeta = config.columnsMeta
for (const value of Object.values(columnsMeta)) {
if (
value.formatter && value.formatter.name === 'AmountFormatter' &&
value.formatterArgs && !value.formatterArgs.drawer
) {
value.formatterArgs.drawer = this.detailDrawer
}
}
return config
}
},
watch: {
inDrawer(val) {
if (!this.drawerVisible) {
return
}
if (!val) {
this.drawerVisible = false
this.reloadTable()
}
}
},
methods: {
getDefaultTitle() {
let title = this.title
if (!title && this.resource) {
title = this.resource
}
if (!title) {
title = this.$route.meta?.title
title = title.replace('List', '').replace('列表', '')
title = _.trimEnd(title, 's')
}
if (!title) {
title = this.$t('NoTitle')
}
let action = this.action
if (action === 'clone') {
action = 'create'
}
title = toSentenceCase(action) + this.$t('WordSep') + toLowerCaseExcludeAbbr(title)
return title
},
getDefaultDrawer(action) {
const route = this.$route.name
const actionRouteName = route.replace('List', toSentenceCase(action))
return this.getRouteNameComponent(actionRouteName, action)
},
getRouteNameComponent(name, action) {
const route = { name: name }
if (action === 'detail' || action === 'update') {
route.params = { id: '1' }
}
const routes = this.$router.resolve(route)
if (!routes) {
return
}
const matched = routes.resolved.matched.filter(item => item.name === name && item.components)
if (matched.length === 0) {
return
}
if (matched[0] && matched[0].components?.default) {
const component = matched[0].components.default
return component
}
},
async showDrawer(action) {
this.action = action
if (action === 'create') {
this.drawerComponent = this.createDrawer
} else if (action === 'update') {
this.drawerComponent = this.updateDrawer || this.createDrawer
} else if (action === 'detail') {
this.drawerComponent = this.detailDrawer
} else if (action === 'clone') {
this.drawerComponent = this.createDrawer || this.getDefaultDrawer('create')
} else {
this.drawerComponent = this.createDrawer
}
if (!this.drawerComponent) {
this.drawerComponent = this.getDefaultDrawer(action)
}
if (this.getDrawerTitle) {
const actionMeta = await this.$store.getters['common/drawerActionMeta']
this.title = this.getDrawerTitle({ action, ...actionMeta })
}
this.drawerVisible = true
},
onCreate(meta) {
if (!meta) {
meta = {}
}
this.$store.dispatch('common/setDrawerActionMeta', {
action: 'create', ...meta
}).then(() => {
this.showDrawer('create')
})
},
reloadTable() {
if (this.reloadOrderQuery) {
this.iTableConfig.url = setUrlParam(this.iTableConfig.url, 'order', this.reloadOrderQuery)
}
this.$refs.ListTable.reloadTable()
},
onClone({ row, col }) {
this.$store.dispatch('common/setDrawerActionMeta', {
action: 'clone', row: row, col: col, id: row.id
}).then(() => {
this.showDrawer('clone')
})
},
onUpdate({ row, col }) {
this.$route.params.id = row.id
this.$store.dispatch('common/setDrawerActionMeta', {
action: 'update', row: row, col: col, id: row.id
}).then(() => {
this.showDrawer('update')
})
}
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@@ -49,6 +49,7 @@
import Dialog from '@/components/Dialog/index.vue'
import { createSourceIdCache } from '@/api/common'
import * as queryUtil from '@/components/Table/DataTable/compenents/el-data-table/utils/query'
import { download } from '@/utils/common'
export default {
name: 'ExportDialog',
@@ -170,14 +171,18 @@ export default {
]
}
},
beforeDestroy() {
this.$eventBus.$off('showExportDialog', this.showExportDialogHandler)
},
mounted() {
this.$eventBus.$on('showExportDialog', ({ selectedRows, url, name }) => {
this.$eventBus.$on('showExportDialog', this.showExportDialogHandler)
},
methods: {
showExportDialogHandler({ selectedRows, url, name }) {
if (url === this.url || url.indexOf(this.url) > -1) {
this.showExportDialog()
}
})
},
methods: {
},
showExportDialog() {
if (!this.mfaVerifyRequired) {
this.exportDialogShow = true
@@ -197,10 +202,7 @@ export default {
})
},
downloadCsv(url) {
const a = document.createElement('a')
a.href = url
a.click()
window.URL.revokeObjectURL(url)
download(url)
},
async defaultPerformExport(selectRows, exportOption, q, exportTypeOption) {
const url = (process.env.VUE_APP_ENV === 'production') ? (`${this.url}`) : (`${process.env.VUE_APP_BASE_API}${this.url}`)

View File

@@ -68,7 +68,7 @@
<script>
import Dialog from '@/components/Dialog/index.vue'
import ImportTable from '@/components/Table/ListTable/TableAction/ImportTable.vue'
import { getErrorResponseMsg } from '@/utils/common'
import { download, getErrorResponseMsg } from '@/utils/common'
import { createSourceIdCache } from '@/api/common'
export default {
@@ -142,14 +142,18 @@ export default {
this.showTable = false
}
},
beforeDestroy() {
this.$eventBus.$off('showImportDialog', this.showImportEventHandler)
},
mounted() {
this.$eventBus.$on('showImportDialog', ({ url }) => {
this.$eventBus.$on('showImportDialog', this.showImportEventHandler)
},
methods: {
showImportEventHandler({ url }) {
if (url === this.url) {
this.showImportDialog = true
}
})
},
methods: {
},
closeDialog() {
this.showImportDialog = false
this.$emit('importDialogClose')
@@ -226,10 +230,7 @@ export default {
this.$message.success(msg)
},
downloadCsv(url) {
const a = document.createElement('a')
a.href = url
a.click()
window.URL.revokeObjectURL(url)
download(url)
},
async handleImportConfirm() {
await this.$refs['importTable'].performUpload()

View File

@@ -44,8 +44,9 @@
import DataTable from '@/components/Table/DataTable/index.vue'
import { getUpdateObjURL } from '@/utils/common'
import { sleep } from '@/utils/time'
import { EditableInputFormatter, StatusFormatter } from '@/components/Table/TableFormatters'
import { EditableInputFormatter } from '@/components/Table/TableFormatters'
import { encryptPassword } from '@/utils/crypto'
import getStatusColumnMeta from '@/components/Table/ListTable/TableAction/const'
export default {
name: 'ImportTable',
@@ -223,38 +224,7 @@ export default {
},
methods: {
generateTableColumns(tableTitles, tableData) {
const vm = this
const columns = [{
prop: '@status',
label: vm.$t('Status'),
width: '80px',
align: 'center',
formatter: StatusFormatter,
formatterArgs: {
faChoices: {
ok: 'fa-check text-primary',
error: 'fa-times text-danger',
pending: 'fa-clock-o'
},
getChoicesKey(val) {
if (val === 'ok' || val === 'pending') {
return val
}
return 'error'
},
getTip(val) {
if (val === 'ok') {
return vm.$t('Success')
} else if (val === 'pending') {
return vm.$t('Pending')
} else if (val && val.name === 'error') {
return val.error
}
return ''
},
hasTips: true
}
}]
const columns = [{ ...getStatusColumnMeta.bind(this)().status }]
for (const item of tableTitles) {
const dataItemLens = tableData.map(d => {
if (!d) {

View File

@@ -71,7 +71,16 @@ export default {
this.listenViewPort()
},
mounted() {
this.$eventBus.$on('labelSearch', label => {
this.$eventBus.$on('labelSearch', this.labelSearchHandler)
},
beforeDestroy(label) {
this.$eventBus.$off('labelSearch', this.labelSearchHandler)
},
methods: {
handleCascaderFocus() {
this.setSearchFocus()
},
labelSearchHandler(label) {
if (!label) {
this.labelValue = []
this.showLabelSearch = true
@@ -82,14 +91,6 @@ export default {
setTimeout(() => {
this.showLabelSearch = true
}, 500)
})
},
destroyed() {
this.$eventBus.$off('labelSearch')
},
methods: {
handleCascaderFocus() {
this.setSearchFocus()
},
handleCascaderVisibleChange(visible) {
const input = this.$refs.labelCascader.$el

View File

@@ -8,14 +8,16 @@
</template>
<script>
import { cleanActions } from './utils'
import { createSourceIdCache } from '@/api/common'
import { getErrorResponseMsg } from '@/utils/common'
import i18n from '@/i18n/i18n'
import DataActions from '@/components/DataActions/index.vue'
import { createSourceIdCache } from '@/api/common'
import { cleanActions } from './utils'
import { getErrorResponseMsg } from '@/utils/common'
const defaultTrue = { type: [Boolean, Function, String], default: true }
const defaultFalse = { type: [Boolean, Function, String], default: false }
export default {
name: 'LeftSide',
components: {
@@ -31,6 +33,10 @@ export default {
return this.$route.name?.replace('List', 'Create')
}
},
beforeCreate: {
type: Function,
default: () => null
},
onCreate: {
type: Function,
default: null
@@ -75,6 +81,10 @@ export default {
default: () => ([])
},
moreActionsTitle: {
type: String,
default: ''
},
moreActionsType: {
type: String,
default: null
},
@@ -95,7 +105,7 @@ export default {
title: this.$t('DeleteSelected'),
name: 'actionDeleteSelected',
has: this.hasBulkDelete,
icon: 'fa fa-trash-o',
icon: 'trash',
can({ selectedRows }) {
return selectedRows.length > 0 && vm.canBulkDelete
},
@@ -128,7 +138,11 @@ export default {
has: this.hasCreate && !this.moreCreates,
can: this.canCreate,
icon: 'plus',
callback: this.onCreate || this.handleCreate
callback: () => {
this.beforeCreate()
const callback = this.onCreate || this.handleCreate
callback()
}
}
]
if (this.moreCreates) {
@@ -140,7 +154,11 @@ export default {
icon: 'plus',
can: this.canCreate,
dropdown: [],
callback: this.onCreate || this.handleCreate
callback: () => {
this.beforeCreate()
const callback = this.onCreate || this.handleCreate
callback()
}
}
const createCreateAction = Object.assign(defaultMoreCreate, this.moreCreates)
defaultActions.push(createCreateAction)
@@ -181,7 +199,8 @@ export default {
return {
name: 'moreActions',
title: this.moreActionsTitle || this.$t('MoreActions'),
dropdown: dropdown
dropdown: dropdown,
type: this.moreActionsType
}
},
hasSelectedRows() {
@@ -194,6 +213,7 @@ export default {
methods: {
handleCreate() {
let route
if (typeof this.createRoute === 'string') {
route = { name: this.createRoute }
route.name = this.createRoute
@@ -202,7 +222,9 @@ export default {
} else if (typeof this.createRoute === 'object') {
route = this.createRoute
}
this.$log.debug('handle create')
if (this.createInNewPage) {
const { href } = this.$router.resolve(route)
window.open(href, '_blank')
@@ -248,3 +270,6 @@ export default {
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,303 @@
<template>
<div v-show="isExpand">
<div v-if="filters || summary" :class="isExpand ? 'expand': 'shrink' " class="quick-filter">
<div v-show="isExpand" class="quick-filter-wrap">
<div v-if="filters" class="quick-filter-zone">
<div v-for="category in iFilters" :key="category.label" class="item-zone">
<div>
<h5>{{ category.label }}</h5>
<div class="filter-options">
<span
v-for="option in category.options"
:key="option.label"
:class="option.active ? 'active' : ''"
class="item"
@click="handleFilterClick(option)"
>
{{ option.label }}
<span v-if="option.hasCount">
(<span v-async="getCount(option)">-</span>)
</span>
<!-- <i class="el-icon-circle-check" />-->
</span>
</div>
</div>
</div>
</div>
<div v-if="summary" class="summary-zone">
<span v-for="item of iSummary" :key="item.title" class="summary-block">
<SummaryCard
:class="item.active ? 'active' : ''"
:count="getCount(item)"
:title="item.title"
@click="handleFilterClick(item)"
/>
</span>
</div>
</div>
<div class="expand-bar-wrap">
<div class="expand-bar" @click="toggle">
<i :class="isExpand ? 'expand': 'shrink' " class="fa fa-angle-double-up" />
<span v-show="!isExpand"> 展开过滤器 </span>
</div>
</div>
</div>
</div>
</template>
<script>
import SummaryCard from '@/components/Cards/SummaryCard/index.vue'
import { setUrlParam } from '@/utils/common'
export default {
name: 'QuickFilter',
components: { SummaryCard },
props: {
filters: {
type: Array,
default: () => []
},
summary: {
type: Array,
default: null
},
expand: {
type: Boolean,
default: true
},
tableUrl: {
type: String,
default: ''
}
},
data() {
return {
iFilters: this.cleanFilters(),
iSummary: this.cleanSummary(),
filtered: {},
activeFilters: []
}
},
computed: {
isExpand: {
set(val) {
this.$emit('update:expand', val)
},
get() {
return this.expand
}
}
},
methods: {
async getCount(item) {
if (item.count || item.count === 0) {
return item.count
}
if (!item.filter) {
return '-'
}
let url = this.tableUrl
for (const [k, v] of Object.entries({ ...item.filter, limit: 1 })) {
url = setUrlParam(url, k, v)
}
const res = await this.$axios.get(url, { raw: 1 })
item.count = res.data.count
return item.count
},
cleanSummary() {
if (!this.summary) {
return []
}
return this.summary.map(item => {
return {
category: 'summary',
label: item.title,
...item,
filter: item.filter || {},
active: false
}
})
},
cleanFilters() {
if (!this.filters) {
return []
}
return this.filters.map(category => {
return {
...category,
options: category.options.map(option => {
return {
category: category.label,
...option,
active: false,
filter: option.filter || {}
}
})
}
})
},
toggle() {
this.isExpand = !this.isExpand
},
handleFilterClick(option) {
if (!option.active) {
this.activeFilters = this.activeFilters.filter(item => {
const conflict = Object.keys(item.filter).some(key => {
return Object.keys(option.filter).includes(key)
})
if (conflict) {
item.active = false
}
return !conflict
})
this.activeFilters.push(option)
} else {
this.activeFilters = this.activeFilters.filter(item => {
return item.label !== option.label && item.category !== option.category
})
}
option.active = !option.active
this.filtered = this.activeFilters.reduce((acc, item) => {
return { ...acc, ...item.filter }
}, {})
this.$emit('filter', this.filtered)
}
}
}
</script>
<style lang='scss' scoped>
.quick-filter {
background: white;
padding: 10px 10px 10px 20px;
margin-bottom: 10px;
display: flex;
place-content: stretch flex-end;
justify-content: center;
align-content: stretch;
box-shadow: 0 1px 1px 0 rgba(54, 58, 80, .32);
&.shrink {
background: inherit;
padding: 0;
margin-bottom: 0;
box-shadow: none;
}
.quick-filter-wrap {
display: inline-block;
width: calc(100% - 70px);
.summary-zone {
padding-top: 10px;
display: flex;
justify-content: space-between;
}
.summary-block {
.active {
::v-deep .no-margins .num {
color: var(--color-primary);
&::after {
content: "\e720";
font-family: element-icons !important;
font-size: 13px;
line-height: 1;
}
}
}
}
.quick-filter-zone {
display: flex;
justify-content: flex-start;
flex-wrap: wrap; /* 允许 item-zone 换行 */
gap: 10px;
h5 {
font-weight: 600;
text-transform: uppercase;
font-size: 12px;
margin-bottom: .5rem;
line-height: 1.2;
display: inline-block;
}
.item-zone {
margin-right: 30px;
margin-bottom: 5px;
}
.item {
display: inline-block;
margin-right: 8px;
color: #303133;
font-size: 12px;
cursor: pointer;
&::after {
content: "";
margin-left: 4px;
margin-bottom: 2px;
vertical-align: middle;
width: 1px; /* 分割线宽度 */
height: 8px; /* 分割线高度 */
background-color: var(--color-icon-primary); /* 分割线颜色 */
display: inline-block;
}
&:last-child::after {
display: none;
}
i {
visibility: hidden;
margin-left: -3px;
}
&.active {
color: var(--color-primary);
i {
visibility: visible;
}
}
&:hover {
color: var(--color-primary);
}
}
ul {
list-style: none outside none;
margin-block-start: 0;
padding-left: 0;
}
}
}
}
.filter-options {
display: block;
}
.expand-bar-wrap {
margin: auto 0;
min-width: 60px;
.expand-bar {
float: right;
display: block;
cursor: pointer;
i {
padding: 5px;
&.shrink {
transform: rotate(180deg);
}
}
}
}
</style>

Some files were not shown because too many files have changed in this diff Show More