mirror of
https://github.com/jumpserver/lina.git
synced 2025-11-08 19:02:40 +00:00
Compare commits
861 Commits
v3.0
...
pr@dev@per
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e777ea67d | ||
|
|
a290e15b90 | ||
|
|
b47f396cd4 | ||
|
|
085554ef7f | ||
|
|
a62884564c | ||
|
|
a9c0d0677c | ||
|
|
5449d74d53 | ||
|
|
dc401f80b9 | ||
|
|
379cd2386a | ||
|
|
4e7bdb9c69 | ||
|
|
41449fb538 | ||
|
|
a75488217c | ||
|
|
8e32792696 | ||
|
|
b47caf0287 | ||
|
|
715ae856f0 | ||
|
|
19ae27e6c2 | ||
|
|
2132bacff5 | ||
|
|
e57c5a20d0 | ||
|
|
3d35f0aafe | ||
|
|
68ac03db9e | ||
|
|
7b5471a451 | ||
|
|
7f052ac85e | ||
|
|
64e75eb9ff | ||
|
|
8e75e5d5e3 | ||
|
|
1810e6833c | ||
|
|
8c7c012785 | ||
|
|
378d82518a | ||
|
|
728b04c8e3 | ||
|
|
b8611f095a | ||
|
|
f49a1184e2 | ||
|
|
77a4441018 | ||
|
|
18f1f0de79 | ||
|
|
d252c7dd08 | ||
|
|
e5547f8a4c | ||
|
|
63d6578991 | ||
|
|
da00ae84a8 | ||
|
|
74a905ee85 | ||
|
|
a03e985df3 | ||
|
|
2d6005a4e0 | ||
|
|
c4ca28d2d9 | ||
|
|
3934c17f52 | ||
|
|
51717f8583 | ||
|
|
67e99702c1 | ||
|
|
27ce5a3785 | ||
|
|
49122bb213 | ||
|
|
263c4d3f89 | ||
|
|
2ed203dc32 | ||
|
|
742a06ea2d | ||
|
|
1ad4e2c62e | ||
|
|
7e29a3e836 | ||
|
|
3a66e8323c | ||
|
|
4852e3d26a | ||
|
|
80509dc15f | ||
|
|
601bd4740c | ||
|
|
62be5885db | ||
|
|
62e49567cc | ||
|
|
f2eff11d66 | ||
|
|
ae61586f95 | ||
|
|
a08fbc3b77 | ||
|
|
53d130f1cf | ||
|
|
c05992ce50 | ||
|
|
52522b7095 | ||
|
|
af3f6c5900 | ||
|
|
7b15ca4955 | ||
|
|
031016330a | ||
|
|
e274640d2e | ||
|
|
bd3352424c | ||
|
|
f8ec2bce0c | ||
|
|
81f34f0154 | ||
|
|
759e205bfb | ||
|
|
fc5029e88a | ||
|
|
6fa8052878 | ||
|
|
0dc62712d4 | ||
|
|
23b08590cf | ||
|
|
8d8ab483e1 | ||
|
|
fb757686e3 | ||
|
|
6a8161dcaf | ||
|
|
ae2391f07f | ||
|
|
50a9ce35ad | ||
|
|
2d2a4be3a2 | ||
|
|
e055429ff2 | ||
|
|
8727bac560 | ||
|
|
fd018dc5ac | ||
|
|
4c3673aef2 | ||
|
|
4eb61373e0 | ||
|
|
8e8dd38e2e | ||
|
|
f8b7720e2c | ||
|
|
50af6fe017 | ||
|
|
8bc617c4a7 | ||
|
|
f75d69601b | ||
|
|
f54d819ec8 | ||
|
|
c50db4089c | ||
|
|
a6d7cc1215 | ||
|
|
f6a2fcbbea | ||
|
|
071822e665 | ||
|
|
063dc9f8e1 | ||
|
|
66e90f189c | ||
|
|
bdd1a86568 | ||
|
|
01d0c9a0c2 | ||
|
|
d42bd25371 | ||
|
|
2d07b10961 | ||
|
|
77fb9ef528 | ||
|
|
b5950b795b | ||
|
|
097817e02c | ||
|
|
cf7a77ce2e | ||
|
|
e72850bff8 | ||
|
|
4669cbfc83 | ||
|
|
cf8ad2a581 | ||
|
|
b776d0157b | ||
|
|
9aaee957af | ||
|
|
0bc681ea08 | ||
|
|
ea89ce1796 | ||
|
|
e8a37a9c5b | ||
|
|
1350573ee2 | ||
|
|
ba83bb14f3 | ||
|
|
7ae5adf49c | ||
|
|
85a1385b4b | ||
|
|
4da8eb12dc | ||
|
|
bf1be51c39 | ||
|
|
3bbe9eccc1 | ||
|
|
65e84b9b41 | ||
|
|
0619900fd7 | ||
|
|
9ecc759dac | ||
|
|
946787e876 | ||
|
|
54c30fcc0d | ||
|
|
83d730cf0f | ||
|
|
7f7173432d | ||
|
|
c1e5f1c1ce | ||
|
|
ca3a99a5cf | ||
|
|
95831c3ff7 | ||
|
|
b5abf3e6ad | ||
|
|
9e4f519fc5 | ||
|
|
b712ec4183 | ||
|
|
35936ad01e | ||
|
|
de5aed0f58 | ||
|
|
5cb8cec835 | ||
|
|
d66b2a8a87 | ||
|
|
d06637afd4 | ||
|
|
b27d88859f | ||
|
|
61580e096b | ||
|
|
da5d67ccbe | ||
|
|
85a88616b0 | ||
|
|
3d3dfcafd3 | ||
|
|
d51e025f04 | ||
|
|
e78d201bd5 | ||
|
|
e1836eb1c6 | ||
|
|
085447255b | ||
|
|
e3da28221e | ||
|
|
b276bbad34 | ||
|
|
f534b292ce | ||
|
|
a02e5363e0 | ||
|
|
a0dbb3d7b0 | ||
|
|
b6e955f4b7 | ||
|
|
902662681c | ||
|
|
0196d42ffc | ||
|
|
a0a7590769 | ||
|
|
b18c045a1b | ||
|
|
e0411010d1 | ||
|
|
757c48ebc8 | ||
|
|
38ea835df5 | ||
|
|
869b94d365 | ||
|
|
25a8f43345 | ||
|
|
7f2cb97bd7 | ||
|
|
580c005bc6 | ||
|
|
e22d46bb38 | ||
|
|
8d7dd81ebd | ||
|
|
d5aa439fee | ||
|
|
095492e5a3 | ||
|
|
a4e2cadedd | ||
|
|
29b35ff2a9 | ||
|
|
4b137f855e | ||
|
|
d836b46966 | ||
|
|
f9d7d68c77 | ||
|
|
cafc5879a0 | ||
|
|
2ec88c3591 | ||
|
|
c37212a5ce | ||
|
|
f9c334b003 | ||
|
|
d43d4137d7 | ||
|
|
614565ba9c | ||
|
|
575015616f | ||
|
|
26fac22b82 | ||
|
|
40b7c62099 | ||
|
|
017486e961 | ||
|
|
7bb4567345 | ||
|
|
76c78df007 | ||
|
|
ff6ac297b9 | ||
|
|
6b953506e4 | ||
|
|
7ec130f033 | ||
|
|
2f9e00de2e | ||
|
|
e479dbfcb5 | ||
|
|
1e5b6f970d | ||
|
|
e7222fb63c | ||
|
|
f9617a92a8 | ||
|
|
38e1b070cf | ||
|
|
f75d97305f | ||
|
|
7df0cc78b1 | ||
|
|
a8a90930b9 | ||
|
|
69dbf2f5da | ||
|
|
fc8339d659 | ||
|
|
eada8d319d | ||
|
|
11940428a0 | ||
|
|
af2544e24b | ||
|
|
4546952388 | ||
|
|
a0c849f29d | ||
|
|
fb9cd1614a | ||
|
|
c1543183a2 | ||
|
|
03dc3e993c | ||
|
|
8719ffee8e | ||
|
|
cc60f2c1f5 | ||
|
|
f042692dbf | ||
|
|
3d8dc619ad | ||
|
|
b223c73a47 | ||
|
|
53e313517d | ||
|
|
56bea8eaf4 | ||
|
|
992025e618 | ||
|
|
205d301578 | ||
|
|
4daccc9df3 | ||
|
|
ac802fff59 | ||
|
|
a6b0da60a3 | ||
|
|
45c860797a | ||
|
|
e636aa24c8 | ||
|
|
86b37eeeba | ||
|
|
61cf9e7f14 | ||
|
|
344e49e7ec | ||
|
|
3ebdacafd0 | ||
|
|
e1ae467823 | ||
|
|
65c1ab450b | ||
|
|
cbe3efdb18 | ||
|
|
e5eaa5bcfa | ||
|
|
df687c0c06 | ||
|
|
1cc00517f5 | ||
|
|
f3a6d6c02b | ||
|
|
7bf33a9011 | ||
|
|
7948a59b80 | ||
|
|
af9f715357 | ||
|
|
202f8a357c | ||
|
|
3e75c781b3 | ||
|
|
3c3ed27eb2 | ||
|
|
fd0e23e35f | ||
|
|
c09a2df142 | ||
|
|
36737a6f25 | ||
|
|
e4af9ccc1e | ||
|
|
3c256c6fdc | ||
|
|
1e36f59b23 | ||
|
|
aec6ee8376 | ||
|
|
526b049495 | ||
|
|
5bef5a59a9 | ||
|
|
1e371a4e32 | ||
|
|
0f6fd0ed70 | ||
|
|
fc1aefbb54 | ||
|
|
7561f1224d | ||
|
|
0932132add | ||
|
|
8492882633 | ||
|
|
11698255f6 | ||
|
|
53eee6c857 | ||
|
|
f2bc4d6f22 | ||
|
|
e50b16b13b | ||
|
|
232c30cadb | ||
|
|
3142da16ae | ||
|
|
741f8b847e | ||
|
|
b6d00a1784 | ||
|
|
2f122f7fbe | ||
|
|
c3c46b759e | ||
|
|
a1c4013a69 | ||
|
|
8aa9690a61 | ||
|
|
30c0100a0b | ||
|
|
134dd17f3f | ||
|
|
505642baec | ||
|
|
c08964ee34 | ||
|
|
9df443667c | ||
|
|
8fd8e9c1d4 | ||
|
|
aa3ab5e138 | ||
|
|
a7df8706e5 | ||
|
|
c200781322 | ||
|
|
75c7778a10 | ||
|
|
c30e919573 | ||
|
|
ffff648e6d | ||
|
|
981e676fa2 | ||
|
|
664855d4b0 | ||
|
|
c79637095a | ||
|
|
2a96abdd4a | ||
|
|
9541b97b23 | ||
|
|
52518a9ff3 | ||
|
|
24a1c11288 | ||
|
|
313aebaf50 | ||
|
|
5c6373e689 | ||
|
|
09fe3ea107 | ||
|
|
ccf081b608 | ||
|
|
80ce3293a1 | ||
|
|
4ed282ff2b | ||
|
|
dd8957cb69 | ||
|
|
36c687b854 | ||
|
|
e22ecb6fe8 | ||
|
|
b77440284f | ||
|
|
93d866328c | ||
|
|
42c7b278c5 | ||
|
|
14ba501f2b | ||
|
|
f072546083 | ||
|
|
8f9ebcdfa9 | ||
|
|
7b76917768 | ||
|
|
e4d4bc84b6 | ||
|
|
62cf19e70e | ||
|
|
2cb36f89f0 | ||
|
|
9946ad75ad | ||
|
|
abd8919225 | ||
|
|
6e5e760689 | ||
|
|
1235895973 | ||
|
|
59f9025e42 | ||
|
|
d06bda6d40 | ||
|
|
a8099089b2 | ||
|
|
fd0c14e1c0 | ||
|
|
9a04c81238 | ||
|
|
f1b3e5038f | ||
|
|
f6fe08607b | ||
|
|
6f2ca3e26a | ||
|
|
3ed9ac0d9b | ||
|
|
821ed14f40 | ||
|
|
6f0ee734e5 | ||
|
|
9a5b174eb1 | ||
|
|
c27e1c97f2 | ||
|
|
1b4e01d19e | ||
|
|
28c836b1f6 | ||
|
|
fc85eaf6b9 | ||
|
|
678d17a4e7 | ||
|
|
ddf9780f9c | ||
|
|
d47bd4acad | ||
|
|
09dabb5d3f | ||
|
|
9eea051884 | ||
|
|
f6a3eb1349 | ||
|
|
433f3a34cb | ||
|
|
aa790944f6 | ||
|
|
57167fc821 | ||
|
|
fb70719cba | ||
|
|
5988892840 | ||
|
|
af11e1bf0c | ||
|
|
09416b13d3 | ||
|
|
478745f534 | ||
|
|
3de2bf73ea | ||
|
|
3b877739a3 | ||
|
|
06899d6932 | ||
|
|
6f3d21bb77 | ||
|
|
b7da517ad7 | ||
|
|
598b6b12e8 | ||
|
|
400ceac737 | ||
|
|
34de6d8775 | ||
|
|
080542633a | ||
|
|
653f26137b | ||
|
|
c86f0cc08d | ||
|
|
541c6c5fe5 | ||
|
|
c36a210cd4 | ||
|
|
b9f26df5e6 | ||
|
|
dd0baa7b00 | ||
|
|
524105278b | ||
|
|
2656546e3b | ||
|
|
ecaf1ceace | ||
|
|
cf515a18de | ||
|
|
a0ab7d3f32 | ||
|
|
467c3f7288 | ||
|
|
f68eaee8c8 | ||
|
|
163f661386 | ||
|
|
e54bc076d6 | ||
|
|
1fbf04ae51 | ||
|
|
f002c4aa3d | ||
|
|
f67bd39067 | ||
|
|
5c60624b2d | ||
|
|
488684f293 | ||
|
|
5c511345bf | ||
|
|
be85a91ccd | ||
|
|
94a02a2e7e | ||
|
|
e60a33a2b1 | ||
|
|
8449dafd55 | ||
|
|
a787737290 | ||
|
|
fa517c8325 | ||
|
|
0748b6ce0c | ||
|
|
59ee3eff17 | ||
|
|
5cc17de1e0 | ||
|
|
42aacd9df5 | ||
|
|
bb27171b09 | ||
|
|
ccc163ef07 | ||
|
|
e63630fce7 | ||
|
|
17748c56c9 | ||
|
|
d880e5cb8c | ||
|
|
310fe8068a | ||
|
|
8670c3988d | ||
|
|
b5144625ee | ||
|
|
58b6d75ccf | ||
|
|
c9ec67cc50 | ||
|
|
e4d1533091 | ||
|
|
97440860d9 | ||
|
|
3dd31dc76e | ||
|
|
1822d72a9b | ||
|
|
3267839327 | ||
|
|
17d73f2b3c | ||
|
|
ec49241272 | ||
|
|
c2e10b8a34 | ||
|
|
29813e7ce1 | ||
|
|
919d95b56b | ||
|
|
5f95fc0422 | ||
|
|
49a4000086 | ||
|
|
9c7818313e | ||
|
|
705e236279 | ||
|
|
d4ecb33c78 | ||
|
|
67e45ad284 | ||
|
|
8cb72c3d82 | ||
|
|
b65a007d37 | ||
|
|
e487ee24af | ||
|
|
bfbc78e342 | ||
|
|
54879a8174 | ||
|
|
36634a2ce5 | ||
|
|
07920a4917 | ||
|
|
25932587a5 | ||
|
|
d98534d039 | ||
|
|
6cb5d6ac46 | ||
|
|
fab461c10d | ||
|
|
644980bc40 | ||
|
|
a56e3843bf | ||
|
|
5ca188c014 | ||
|
|
510cf48def | ||
|
|
e64fdab7fc | ||
|
|
aaec496642 | ||
|
|
80c05c9b65 | ||
|
|
c9159838f5 | ||
|
|
b8e005f4b1 | ||
|
|
6e73ded101 | ||
|
|
6926556fa3 | ||
|
|
24971c1112 | ||
|
|
763792f42e | ||
|
|
0ff28d2626 | ||
|
|
f607eeda42 | ||
|
|
c0145f4ec4 | ||
|
|
7c995c005b | ||
|
|
21f4f003bb | ||
|
|
fbbac6cea5 | ||
|
|
302400e350 | ||
|
|
b2120678dd | ||
|
|
03c7755703 | ||
|
|
21373574de | ||
|
|
342ee31c72 | ||
|
|
7aa8afec44 | ||
|
|
d6b54c9879 | ||
|
|
b1952a180f | ||
|
|
d3a7fb63b9 | ||
|
|
53cfdbd3a1 | ||
|
|
41cd77d75e | ||
|
|
15ac510c6f | ||
|
|
31e005eca7 | ||
|
|
842ea3cbf3 | ||
|
|
9f893af74b | ||
|
|
5274dc4e6b | ||
|
|
64441e4836 | ||
|
|
43997eeac1 | ||
|
|
1b95af54c1 | ||
|
|
e79f95a822 | ||
|
|
fb85e168e7 | ||
|
|
521cafa2af | ||
|
|
bb9f905dd4 | ||
|
|
3eda4381bb | ||
|
|
080acf57f7 | ||
|
|
9c137e6763 | ||
|
|
75f5363602 | ||
|
|
32f6d5dc4a | ||
|
|
21265aa983 | ||
|
|
e11441e5a7 | ||
|
|
e7624f47bf | ||
|
|
7276b19a92 | ||
|
|
32fa172197 | ||
|
|
aa80a07bfc | ||
|
|
fd97a6c4a2 | ||
|
|
9b355a2942 | ||
|
|
0431f7108a | ||
|
|
3500ab89f5 | ||
|
|
fcdcf29efa | ||
|
|
49d74271c8 | ||
|
|
9a7eca3cf8 | ||
|
|
dc686f2af7 | ||
|
|
172c6d69a1 | ||
|
|
3f2a846184 | ||
|
|
48ac797b59 | ||
|
|
92962bdc5a | ||
|
|
ca4889a19a | ||
|
|
e4631a5309 | ||
|
|
62eced61d2 | ||
|
|
7a8df3fb7b | ||
|
|
91ef3cfaab | ||
|
|
c92843e973 | ||
|
|
47372df4d2 | ||
|
|
143e913554 | ||
|
|
61b2b0fb23 | ||
|
|
54ac31920c | ||
|
|
4eaeff0a18 | ||
|
|
08c16aae72 | ||
|
|
bf5e218edc | ||
|
|
e8dbd99f74 | ||
|
|
9ab2acfa5b | ||
|
|
d3cbb48e05 | ||
|
|
170dde2ba5 | ||
|
|
021eb1fd5b | ||
|
|
1b2c85d86d | ||
|
|
2434e434b7 | ||
|
|
3a5aa7bf90 | ||
|
|
dbe4c232c1 | ||
|
|
3294495577 | ||
|
|
05e7e6dd06 | ||
|
|
936de3c9fb | ||
|
|
51dac8ca30 | ||
|
|
89b3ea51f5 | ||
|
|
bdfc608534 | ||
|
|
667514e90b | ||
|
|
83a116c094 | ||
|
|
211933349e | ||
|
|
ec51b243b1 | ||
|
|
80eb072c1b | ||
|
|
ce08fd57c6 | ||
|
|
40ccab1a19 | ||
|
|
8489d94643 | ||
|
|
72693af5fd | ||
|
|
1fdcf9ff75 | ||
|
|
c4ad25bfe3 | ||
|
|
d6a7d93398 | ||
|
|
43b2ad3104 | ||
|
|
2209eea890 | ||
|
|
1a56546d2e | ||
|
|
fa52226958 | ||
|
|
42ee6c0848 | ||
|
|
49f63d638c | ||
|
|
7e6d78d223 | ||
|
|
a99a5a6312 | ||
|
|
e80b162eca | ||
|
|
fabf60ac47 | ||
|
|
14b5eb5239 | ||
|
|
e365eaef8a | ||
|
|
d14cbce2d6 | ||
|
|
f29f88b78b | ||
|
|
40ccff9685 | ||
|
|
b85de1de33 | ||
|
|
640e758016 | ||
|
|
09bd49941f | ||
|
|
edc74c8dfb | ||
|
|
fcd7d17ef1 | ||
|
|
574b7b54c5 | ||
|
|
f486da0018 | ||
|
|
1598b4e4ad | ||
|
|
4556d3f4fe | ||
|
|
e521868cd2 | ||
|
|
d3d6a0e890 | ||
|
|
fd4223e107 | ||
|
|
152301b0f3 | ||
|
|
cb7a492b74 | ||
|
|
80dd8a23ec | ||
|
|
41729ebe53 | ||
|
|
da3e6aff76 | ||
|
|
7ff8114850 | ||
|
|
43eaaf3eba | ||
|
|
26d8154db8 | ||
|
|
673198af07 | ||
|
|
b14630c33a | ||
|
|
187a0824fc | ||
|
|
e180fbd2a5 | ||
|
|
90ebb0ff0b | ||
|
|
c922fa99fd | ||
|
|
5e3917d61c | ||
|
|
57702bcef3 | ||
|
|
a055b4fe45 | ||
|
|
7305bf772c | ||
|
|
3a4c579c6c | ||
|
|
4ce651e319 | ||
|
|
c3adfe7031 | ||
|
|
b7868e804d | ||
|
|
3ea8239c77 | ||
|
|
ddb7369994 | ||
|
|
97b0260921 | ||
|
|
213f896ab8 | ||
|
|
9ef79e677c | ||
|
|
8e03194a65 | ||
|
|
4c6f9d6490 | ||
|
|
95e14227f0 | ||
|
|
1bf5e9c3bc | ||
|
|
0b52dfd852 | ||
|
|
7cc3a7f9a1 | ||
|
|
74df2dc164 | ||
|
|
b40e6bb412 | ||
|
|
10c9a54896 | ||
|
|
1f4d211c32 | ||
|
|
b736a3aeb8 | ||
|
|
b3050e62b3 | ||
|
|
675fd1ba9f | ||
|
|
b1579b1fbd | ||
|
|
84d4aebdeb | ||
|
|
6b1c0590ce | ||
|
|
b7fd52fa6e | ||
|
|
904dbd6d5e | ||
|
|
c6c693d112 | ||
|
|
3f946f9ada | ||
|
|
35482abf7a | ||
|
|
8b77b3a6df | ||
|
|
8240f56ecd | ||
|
|
77dd600393 | ||
|
|
1149195a23 | ||
|
|
800162ce27 | ||
|
|
9b473a8729 | ||
|
|
c553a09cb4 | ||
|
|
2702257d71 | ||
|
|
d41162a89b | ||
|
|
60999a7533 | ||
|
|
09f20e8149 | ||
|
|
bb631556ba | ||
|
|
32f708a014 | ||
|
|
249cc67140 | ||
|
|
301d3a79b7 | ||
|
|
d5d86afed7 | ||
|
|
6e4ea15d82 | ||
|
|
79aa64c999 | ||
|
|
fa405f4cce | ||
|
|
3fa2cfd860 | ||
|
|
6dad655704 | ||
|
|
2a6406a8fb | ||
|
|
eca3685899 | ||
|
|
afc4bea076 | ||
|
|
ace2568028 | ||
|
|
0382bab537 | ||
|
|
5f535d8bae | ||
|
|
48fe080009 | ||
|
|
e4cab9c2a4 | ||
|
|
61a6b45051 | ||
|
|
03cda9a47a | ||
|
|
f00f679bd2 | ||
|
|
2340efedf5 | ||
|
|
3d0b722409 | ||
|
|
777d31e562 | ||
|
|
47d602302f | ||
|
|
87002430f4 | ||
|
|
1bb0bce869 | ||
|
|
9e74a49644 | ||
|
|
29d40f4d54 | ||
|
|
dd0314d235 | ||
|
|
fd3260faff | ||
|
|
c05f0275de | ||
|
|
ed13ab3fdd | ||
|
|
5938ddbb35 | ||
|
|
99672fc645 | ||
|
|
3a73cb900d | ||
|
|
87196e0cd1 | ||
|
|
7c171b3a76 | ||
|
|
3694a27752 | ||
|
|
b5b6b5227d | ||
|
|
5ac365a990 | ||
|
|
2361b16c30 | ||
|
|
023fb03490 | ||
|
|
4a0d104a96 | ||
|
|
f85351b163 | ||
|
|
140ad7cac4 | ||
|
|
d0b4b488b1 | ||
|
|
da69186239 | ||
|
|
2ea6058113 | ||
|
|
8913a5ea87 | ||
|
|
de5ff58d18 | ||
|
|
b658e37fcb | ||
|
|
99efbd153f | ||
|
|
09a0630c6b | ||
|
|
cc7dd80a8a | ||
|
|
a1906fd925 | ||
|
|
f48632d7f7 | ||
|
|
f3d31a3f0f | ||
|
|
7aaec2ea43 | ||
|
|
40b7dbc211 | ||
|
|
1cec999bfd | ||
|
|
b33f598742 | ||
|
|
19b2b55051 | ||
|
|
b6235ec7dd | ||
|
|
108a39472a | ||
|
|
63c97e18ad | ||
|
|
19f46b098a | ||
|
|
03ad5239e9 | ||
|
|
720a076648 | ||
|
|
80eef76604 | ||
|
|
feb9c65733 | ||
|
|
7597112520 | ||
|
|
d54044760d | ||
|
|
b369d2d22a | ||
|
|
2e3db496c0 | ||
|
|
97e2f23093 | ||
|
|
42b79c3e30 | ||
|
|
d2fe84a81e | ||
|
|
c3bb902e67 | ||
|
|
dde98e470b | ||
|
|
822fa9714c | ||
|
|
90659afc36 | ||
|
|
7310cbe636 | ||
|
|
0857007e15 | ||
|
|
41b00cb293 | ||
|
|
89466a0995 | ||
|
|
800315fb7c | ||
|
|
8b037e8bac | ||
|
|
085f7e68ef | ||
|
|
5c075a5a7d | ||
|
|
01bc2b838a | ||
|
|
0d495e979d | ||
|
|
6d66419fa6 | ||
|
|
96f629a1b7 | ||
|
|
35df28ea90 | ||
|
|
ca98a8ee2e | ||
|
|
dc09364348 | ||
|
|
423601d840 | ||
|
|
09ee34f0d3 | ||
|
|
98fef43ece | ||
|
|
41bb35f7c6 | ||
|
|
827339ad0e | ||
|
|
9dbdc559cd | ||
|
|
bce328a440 | ||
|
|
84e9cf1ac3 | ||
|
|
5fd5834c8d | ||
|
|
73a783bb97 | ||
|
|
1a5dd023e8 | ||
|
|
e4e011e183 | ||
|
|
4b594290e9 | ||
|
|
c71a0f2a75 | ||
|
|
fd6ab841bf | ||
|
|
2cf27f08b9 | ||
|
|
97f531bbec | ||
|
|
ac7d5e216e | ||
|
|
ee8b373e77 | ||
|
|
ecd1da5fc4 | ||
|
|
cdcf340a75 | ||
|
|
d8941c14b8 | ||
|
|
bb3ca059b0 | ||
|
|
d994a01a10 | ||
|
|
bb432dff18 | ||
|
|
9ce7311f6d | ||
|
|
a468e04bad | ||
|
|
a5dcc8be52 | ||
|
|
f905ed0065 | ||
|
|
0f80375242 | ||
|
|
ed8c9df4c3 | ||
|
|
7bc9f2f75b | ||
|
|
b0c635463a | ||
|
|
5ccf713ef3 | ||
|
|
16e92e925c | ||
|
|
a2ec7d3ccd | ||
|
|
ae3e072275 | ||
|
|
9e195dd1d9 | ||
|
|
8432299660 | ||
|
|
b9f41570bf | ||
|
|
5ead251ea7 | ||
|
|
fc10a77372 | ||
|
|
41ef8c6e97 | ||
|
|
ef0658de24 | ||
|
|
fedb068221 | ||
|
|
5a66c39313 | ||
|
|
ce6ca288b7 | ||
|
|
65541a7001 | ||
|
|
24a5c6be43 | ||
|
|
020c7ec9fa | ||
|
|
f12707d958 | ||
|
|
82c2e20fce | ||
|
|
11f0ee12cf | ||
|
|
ca64c0b219 | ||
|
|
e34c6fc843 | ||
|
|
51e139712d | ||
|
|
9daf001312 | ||
|
|
27353ee0c2 | ||
|
|
b6643d89c0 | ||
|
|
9503586b8a | ||
|
|
317735c69a | ||
|
|
5c26000f90 | ||
|
|
139a47b858 | ||
|
|
2f2e05101b | ||
|
|
36f7d9711d | ||
|
|
4e89c8d53f | ||
|
|
5902475778 | ||
|
|
abdba774cc | ||
|
|
c1d10322cb | ||
|
|
90fdf4982e | ||
|
|
f5e5d431c7 | ||
|
|
9d9fb51a7d | ||
|
|
997b5d8d19 | ||
|
|
ea19dd8e0a | ||
|
|
983663a3fa | ||
|
|
3809519331 | ||
|
|
fe7d20778d | ||
|
|
f6bf8b3193 | ||
|
|
5d5ee3cdaa | ||
|
|
ed4da2ab50 | ||
|
|
9c8ed912bb | ||
|
|
c58551adea | ||
|
|
f44618d915 | ||
|
|
aeb26c748a | ||
|
|
c75f0b1312 | ||
|
|
6138fe2e35 | ||
|
|
8d3f99392f | ||
|
|
2fd4792ed4 | ||
|
|
757a822e34 | ||
|
|
03ffe4a911 | ||
|
|
3b83ecd85d | ||
|
|
80145dc114 | ||
|
|
5612f432a5 | ||
|
|
023fd55a70 | ||
|
|
897f3881b2 | ||
|
|
730aa548d4 | ||
|
|
f1a3d775cc | ||
|
|
71d79ec6ef | ||
|
|
4118a21d1d | ||
|
|
be87eec4e5 | ||
|
|
230433de05 | ||
|
|
e3fdf1cc41 | ||
|
|
f1f4ba5dfa | ||
|
|
d9d5de5102 | ||
|
|
7bf74d8727 | ||
|
|
937bdcf5c7 | ||
|
|
0a86240f90 | ||
|
|
ea3c28791d | ||
|
|
1ff829182b | ||
|
|
23fea1bf99 | ||
|
|
496605c539 | ||
|
|
051784e52b | ||
|
|
dcd6fa977d | ||
|
|
411987fb69 | ||
|
|
ac88b70b1c | ||
|
|
36e053068e | ||
|
|
7944c4a414 | ||
|
|
325c2b45e4 | ||
|
|
477fd5164e | ||
|
|
acfdb04b09 | ||
|
|
50073f0512 | ||
|
|
6ab61215b5 | ||
|
|
b5d4bfd488 | ||
|
|
24297dbead | ||
|
|
ad0c17610e | ||
|
|
a54f6f3a89 | ||
|
|
3212260d8e | ||
|
|
8ec7cd3c3c | ||
|
|
32c09c896c | ||
|
|
71c1d77498 | ||
|
|
06fc05e6b3 | ||
|
|
6b491ef696 | ||
|
|
5e23ae7926 | ||
|
|
f62c6b980e | ||
|
|
d930e09065 | ||
|
|
10a6dae2cf | ||
|
|
c099a16a49 | ||
|
|
198e6a11d2 | ||
|
|
8c66e20961 | ||
|
|
0393128be5 | ||
|
|
69ff6be5f4 | ||
|
|
8685362344 | ||
|
|
7ea68e9a09 | ||
|
|
ebede00841 | ||
|
|
73ce298d6a | ||
|
|
69cd561dc0 | ||
|
|
7af1a55e2a | ||
|
|
c1272d9a9f | ||
|
|
f3e28f7cd4 | ||
|
|
412a7ceba1 | ||
|
|
73bf34c422 | ||
|
|
3d2b139214 | ||
|
|
658f0ff587 | ||
|
|
621893fdcc | ||
|
|
a6e94b4173 | ||
|
|
f9098c3203 | ||
|
|
b1c0218ad9 | ||
|
|
4058843b1a | ||
|
|
d144cd6809 |
35
.github/workflows/release-drafter.yml
vendored
35
.github/workflows/release-drafter.yml
vendored
@@ -31,16 +31,29 @@ jobs:
|
||||
config-name: release-config.yml
|
||||
version: ${{ steps.get_version.outputs.TAG }}
|
||||
tag: ${{ steps.get_version.outputs.TAG }}
|
||||
|
||||
build-and-release:
|
||||
needs: create-realese
|
||||
name: Build and Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build it and upload
|
||||
uses: jumpserver/action-build-upload-assets@node10
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16.20'
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
- name: Build web
|
||||
run: |
|
||||
sed -i "s@version-dev@${{ steps.get_version.outputs.TAG }}@g" src/layout/components/NavHeader/About.vue
|
||||
yarn build
|
||||
- name: Create Upload Assets
|
||||
run: |
|
||||
rm -rf build/*
|
||||
mv lina lina-${{ steps.get_version.outputs.VERSION }}
|
||||
tar -czf lina-${{ steps.get_version.outputs.VERSION }}.tar.gz lina-${{ steps.get_version.outputs.VERSION }}
|
||||
echo $(md5sum lina-${{ steps.get_version.outputs.VERSION }}.tar.gz | awk '{print $1}') > build/lina-${{ steps.get_version.outputs.VERSION }}.tar.gz.md5
|
||||
mv lina-${{ steps.get_version.outputs.VERSION }}.tar.gz build/
|
||||
- name: Release Upload Assets
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
draft: true
|
||||
files: |
|
||||
build/lina-${{ steps.get_version.outputs.TAG }}.tar.gz
|
||||
build/lina-${{ steps.get_version.outputs.TAG }}.tar.gz.md5
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.create-realese.outputs.upload_url }}
|
||||
|
||||
27
Dockerfile
27
Dockerfile
@@ -1,13 +1,28 @@
|
||||
FROM node:14.16 as stage-build
|
||||
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,id=lina \
|
||||
sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
|
||||
&& rm -f /etc/apt/apt.conf.d/docker-clean \
|
||||
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ${DEPENDENCIES} \
|
||||
&& echo "no" | dpkg-reconfigure dash \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ARG NPM_REGISTRY="https://registry.npmmirror.com"
|
||||
|
||||
WORKDIR /data
|
||||
|
||||
RUN set -ex \
|
||||
&& npm config set registry ${NPM_REGISTRY} \
|
||||
&& yarn config set registry ${NPM_REGISTRY}
|
||||
|
||||
WORKDIR /data
|
||||
|
||||
ADD package.json yarn.lock /data
|
||||
RUN --mount=type=cache,target=/usr/local/share/.cache/yarn,sharing=locked,id=lina \
|
||||
yarn install
|
||||
@@ -16,9 +31,9 @@ ARG VERSION
|
||||
ENV VERSION=$VERSION
|
||||
ADD . /data
|
||||
RUN --mount=type=cache,target=/usr/local/share/.cache/yarn,sharing=locked,id=lina \
|
||||
sed -i "s@<strong> version-dev </strong>@<strong> ${VERSION} </strong>@g" src/layout/components/NavHeader/About.vue \
|
||||
sed -i "s@version-dev@${VERSION}@g" src/layout/components/NavHeader/About.vue \
|
||||
&& yarn build
|
||||
|
||||
FROM nginx:alpine
|
||||
FROM nginx:1.24-bullseye
|
||||
COPY --from=stage-build /data/lina /opt/lina
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
@@ -5,11 +5,10 @@
|
||||
"author": "Pan <panfree23@gmail.com>",
|
||||
"license": "MIT",
|
||||
"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",
|
||||
"lint": "eslint --ext .js,.vue src",
|
||||
"fix": "eslint --ext .js,.vue --fix src",
|
||||
|
||||
@@ -496,3 +496,11 @@ td .el-button.el-button--mini {
|
||||
.el-alert.el-alert--error.is-light {
|
||||
border-color: var(--color-danger-light);
|
||||
}
|
||||
|
||||
#nprogress .bar {
|
||||
background: light-5!important;
|
||||
}
|
||||
|
||||
#nprogress .peg {
|
||||
box-shadow: 0 0 10px light-5, 0 0 5px light-5!important;
|
||||
}
|
||||
|
||||
11
src/App.vue
11
src/App.vue
@@ -1,12 +1,19 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<router-view />
|
||||
<router-view v-if="isRouterAlive" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'App'
|
||||
name: 'App',
|
||||
computed: {
|
||||
...mapState({
|
||||
isRouterAlive: state => state.common.isRouterAlive
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -44,3 +44,11 @@ export function renameFile(playbookId, node) {
|
||||
data: node
|
||||
})
|
||||
}
|
||||
|
||||
export function createJob(form) {
|
||||
return request({
|
||||
url: '/api/v1/ops/jobs/',
|
||||
method: 'post',
|
||||
data: form
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,24 +8,11 @@ export function terminateSession(data) {
|
||||
})
|
||||
}
|
||||
|
||||
export function getSessionDetail(id) {
|
||||
export function toggleLockSession(data) {
|
||||
return request({
|
||||
url: `/api/v1/terminal/sessions/${id}/`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getSessionCommands(id) {
|
||||
return request({
|
||||
url: `/api/v1/terminal/commands/?session_id=${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getTerminalDetail(id) {
|
||||
return request({
|
||||
url: `/api/v1/terminal/terminals/${id}/`,
|
||||
method: 'get'
|
||||
url: '/api/v1/terminal/tasks/toggle-lock-session/',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -25,12 +25,28 @@ export function importLicense(formData) {
|
||||
data: formData
|
||||
})
|
||||
}
|
||||
export function testLdapSetting(data) {
|
||||
return request({
|
||||
disableFlashErrorMsg: true,
|
||||
url: '/api/v1/settings/ldap/testing/config/',
|
||||
method: 'post',
|
||||
data: data
|
||||
export function testLdapSetting(data, refresh = true) {
|
||||
let url = '/api/v1/settings/ldap/testing/config/'
|
||||
if (refresh) {
|
||||
url = url + '?refresh=1'
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
request({
|
||||
disableFlashErrorMsg: true,
|
||||
url: url,
|
||||
method: 'post',
|
||||
data: data
|
||||
}).then(res => {
|
||||
if (res.status !== 'running') {
|
||||
resolve(res)
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
resolve(testLdapSetting(data, false))
|
||||
}, 1000)
|
||||
}
|
||||
}).catch(error => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -8,12 +8,13 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AutoDataForm from '@/components/AutoDataForm'
|
||||
import { UpdateToken } from '@/components/FormFields'
|
||||
import Select2 from '@/components/FormFields/Select2'
|
||||
import AssetSelect from '@/components/AssetSelect'
|
||||
import AutoDataForm from '@/components/Form/AutoDataForm/index.vue'
|
||||
import { UpdateToken, UploadSecret } from '@/components/Form/FormFields'
|
||||
import Select2 from '@/components/Form/FormFields/Select2.vue'
|
||||
import AssetSelect from '@/components/Apps/AssetSelect/index.vue'
|
||||
import { encryptPassword } from '@/utils/crypto'
|
||||
import { RequiredChange } from '@/components/DataForm/rules'
|
||||
import { Required, RequiredChange } from '@/components/Form/DataForm/rules'
|
||||
import AutomationParamsForm from '@/views/assets/Platform/AutomationParamsSetting.vue'
|
||||
|
||||
export default {
|
||||
name: 'AccountCreateForm',
|
||||
@@ -31,12 +32,16 @@ export default {
|
||||
},
|
||||
account: {
|
||||
type: Object,
|
||||
default: null
|
||||
default: () => ({})
|
||||
},
|
||||
// 默认组件密码加密
|
||||
encryptPassword: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
addTemplate: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -46,27 +51,30 @@ export default {
|
||||
defaultPrivilegedAccounts: ['root', 'administrator'],
|
||||
iPlatform: {
|
||||
automation: {},
|
||||
su_enabled: false,
|
||||
protocols: [
|
||||
{
|
||||
name: 'ssh',
|
||||
secret_types: ['password', 'ssh_key', 'token', 'api_key']
|
||||
secret_types: ['password', 'ssh_key', 'token', 'access_key', 'api_key']
|
||||
}
|
||||
]
|
||||
},
|
||||
url: '/api/v1/accounts/accounts/',
|
||||
form: this.account || {},
|
||||
form: Object.assign({ 'on_invalid': 'error' }, this.account || {}),
|
||||
encryptedFields: ['secret'],
|
||||
fields: [
|
||||
[this.$t('assets.Asset'), ['assets']],
|
||||
[this.$t('common.Basic'), ['name', 'username', ...this.controlShowField()]],
|
||||
[this.$t('accounts.AccountTemplate'), ['template']],
|
||||
[this.$t('common.Basic'), ['name', 'username', 'privileged', 'su_from', 'su_from_username']],
|
||||
[this.$t('assets.Secret'), [
|
||||
'secret_type', 'secret', 'ssh_key', 'token',
|
||||
'api_key', 'passphrase'
|
||||
'secret_type', 'password', 'ssh_key', 'token',
|
||||
'access_key', 'passphrase', 'api_key'
|
||||
]],
|
||||
[this.$t('common.Other'), ['push_now', 'is_active', 'comment']]
|
||||
[this.$t('common.Other'), ['push_now', 'params', 'on_invalid', 'is_active', 'comment']]
|
||||
],
|
||||
fieldsMeta: {
|
||||
assets: {
|
||||
rules: [Required],
|
||||
component: AssetSelect,
|
||||
label: this.$t('assets.Asset'),
|
||||
el: {
|
||||
@@ -76,6 +84,30 @@ export default {
|
||||
return this.platform || this.asset
|
||||
}
|
||||
},
|
||||
template: {
|
||||
component: Select2,
|
||||
rules: [Required],
|
||||
el: {
|
||||
multiple: false,
|
||||
ajax: {
|
||||
url: '/api/v1/accounts/account-templates/',
|
||||
transformOption: (item) => {
|
||||
return { label: item.name, value: item.id }
|
||||
}
|
||||
}
|
||||
},
|
||||
hidden: () => {
|
||||
return this.platform || this.asset || !this.addTemplate
|
||||
}
|
||||
},
|
||||
on_invalid: {
|
||||
rules: [Required],
|
||||
label: this.$t('accounts.AccountPolicy'),
|
||||
helpText: this.$t('accounts.BulkCreateStrategy'),
|
||||
hidden: () => {
|
||||
return this.platform || this.asset
|
||||
}
|
||||
},
|
||||
name: {
|
||||
rules: [RequiredChange],
|
||||
on: {
|
||||
@@ -90,6 +122,9 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
hidden: () => {
|
||||
return this.addTemplate
|
||||
}
|
||||
},
|
||||
username: {
|
||||
@@ -106,12 +141,20 @@ export default {
|
||||
updateForm({ privileged: true })
|
||||
}
|
||||
}
|
||||
},
|
||||
hidden: () => {
|
||||
return this.addTemplate
|
||||
}
|
||||
},
|
||||
privileged: {
|
||||
hidden: () => {
|
||||
return this.addTemplate
|
||||
}
|
||||
},
|
||||
su_from: {
|
||||
component: Select2,
|
||||
hidden: (formValue) => {
|
||||
return !this.asset?.id
|
||||
return !this.asset?.id || !this.iPlatform.su_enabled
|
||||
},
|
||||
el: {
|
||||
multiple: false,
|
||||
@@ -124,49 +167,80 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
secret: {
|
||||
su_from_username: {
|
||||
label: this.$t('assets.UserSwitchFrom'),
|
||||
hidden: (formValue) => {
|
||||
return this.platform || this.asset || this.addTemplate
|
||||
}
|
||||
},
|
||||
password: {
|
||||
label: this.$t('assets.Password'),
|
||||
component: UpdateToken,
|
||||
hidden: (formValue) => formValue.secret_type !== 'password'
|
||||
hidden: (formValue) => formValue.secret_type !== 'password' || this.addTemplate
|
||||
},
|
||||
ssh_key: {
|
||||
label: this.$t('assets.PrivateKey'),
|
||||
el: {
|
||||
type: 'textarea',
|
||||
rows: 4
|
||||
},
|
||||
hidden: (formValue) => formValue.secret_type !== 'ssh_key'
|
||||
component: UploadSecret,
|
||||
hidden: (formValue) => formValue.secret_type !== 'ssh_key' || this.addTemplate
|
||||
},
|
||||
passphrase: {
|
||||
label: this.$t('assets.Passphrase'),
|
||||
component: UpdateToken,
|
||||
hidden: (formValue) => formValue.secret_type !== 'ssh_key'
|
||||
hidden: (formValue) => formValue.secret_type !== 'ssh_key' || this.addTemplate
|
||||
},
|
||||
token: {
|
||||
label: this.$t('assets.Token'),
|
||||
el: {
|
||||
type: 'textarea',
|
||||
rows: 4
|
||||
},
|
||||
hidden: (formValue) => formValue.secret_type !== 'token'
|
||||
component: UploadSecret,
|
||||
hidden: (formValue) => formValue.secret_type !== 'token' || this.addTemplate
|
||||
},
|
||||
access_key: {
|
||||
id: 'access_key',
|
||||
label: this.$t('assets.AccessKey'),
|
||||
component: UploadSecret,
|
||||
hidden: (formValue) => formValue.secret_type !== 'access_key' || this.addTemplate
|
||||
},
|
||||
api_key: {
|
||||
id: 'api_key',
|
||||
label: this.$t('assets.AccessKey'),
|
||||
el: {
|
||||
type: 'textarea',
|
||||
rows: 4
|
||||
},
|
||||
hidden: (formValue) => formValue.secret_type !== 'api_key'
|
||||
label: this.$t('assets.ApiKey'),
|
||||
component: UploadSecret,
|
||||
hidden: (formValue) => formValue.secret_type !== 'api_key' || this.addTemplate
|
||||
},
|
||||
secret_type: {
|
||||
type: 'radio-group',
|
||||
options: []
|
||||
options: [],
|
||||
hidden: () => {
|
||||
return this.addTemplate
|
||||
}
|
||||
},
|
||||
push_now: {
|
||||
hidden: () => {
|
||||
helpText: this.$t('accounts.AccountPush.WindowsPushHelpText'),
|
||||
hidden: (formValue) => {
|
||||
const automation = this.iPlatform.automation || {}
|
||||
return !automation.push_account_enabled || !automation.ansible_enabled || !this.$hasPerm('accounts.push_account')
|
||||
return !automation.push_account_enabled ||
|
||||
!automation.ansible_enabled ||
|
||||
!this.$hasPerm('accounts.push_account') ||
|
||||
(formValue.secret_type === 'ssh_key' && this.iPlatform.type.value === 'windows') ||
|
||||
this.addTemplate
|
||||
}
|
||||
},
|
||||
params: {
|
||||
label: this.$t('assets.PushParams'),
|
||||
component: AutomationParamsForm,
|
||||
el: {
|
||||
method: this.asset?.auto_config?.push_account_method
|
||||
},
|
||||
hidden: (formValue) => {
|
||||
const automation = this.iPlatform.automation || {}
|
||||
return !formValue.push_now ||
|
||||
!automation.push_account_enabled ||
|
||||
!automation.ansible_enabled ||
|
||||
!this.$hasPerm('accounts.push_account') ||
|
||||
this.addTemplate
|
||||
}
|
||||
},
|
||||
comment: {
|
||||
hidden: () => {
|
||||
return this.addTemplate
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -208,6 +282,10 @@ export default {
|
||||
},
|
||||
{
|
||||
label: this.$t('assets.AccessKey'),
|
||||
value: 'access_key'
|
||||
},
|
||||
{
|
||||
label: this.$t('assets.ApiKey'),
|
||||
value: 'api_key'
|
||||
}
|
||||
]
|
||||
@@ -215,30 +293,21 @@ export default {
|
||||
this.iPlatform.protocols?.forEach(p => {
|
||||
secretTypes.push(...p['secret_types'])
|
||||
})
|
||||
if (!this.form.secret_type) {
|
||||
if (!this.form?.secret_type) {
|
||||
this.form.secret_type = secretTypes[0]
|
||||
}
|
||||
this.fieldsMeta.secret_type.options = choices.filter(item => {
|
||||
return secretTypes.indexOf(item.value) > -1
|
||||
})
|
||||
},
|
||||
controlShowField() {
|
||||
const privileged = ['privileged']
|
||||
let suFrom = ['su_from']
|
||||
const filterSuFrom = ['database', 'device', 'cloud', 'web']
|
||||
const asset = this?.asset || {}
|
||||
if (filterSuFrom.includes(asset?.category?.value)) {
|
||||
suFrom = []
|
||||
}
|
||||
return [...privileged, ...suFrom]
|
||||
},
|
||||
confirm(form) {
|
||||
const secretType = form.secret_type || ''
|
||||
if (secretType !== 'password') {
|
||||
form.secret = form[secretType]
|
||||
delete form[secretType]
|
||||
}
|
||||
const secretType = form.secret_type || 'password'
|
||||
form.secret = form[secretType]
|
||||
form.secret = this.encryptPassword ? encryptPassword(form.secret) : form.secret
|
||||
|
||||
// 如果不删除会明文显示
|
||||
delete form[secretType]
|
||||
|
||||
if (!form.secret) {
|
||||
delete form['secret']
|
||||
}
|
||||
@@ -1,41 +1,36 @@
|
||||
<template>
|
||||
<Dialog
|
||||
:title="title"
|
||||
:visible.sync="iVisible"
|
||||
:destroy-on-close="true"
|
||||
:show-cancel="false"
|
||||
:show-confirm="false"
|
||||
:close-on-click-modal="false"
|
||||
v-bind="$attrs"
|
||||
width="70%"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<GenericCreateUpdateDrawer v-bind="$attrs">
|
||||
<AccountCreateUpdateForm
|
||||
v-if="!loading"
|
||||
ref="form"
|
||||
:account="account"
|
||||
:add-template="addTemplate"
|
||||
:asset="asset"
|
||||
@add="addAccount"
|
||||
@edit="editAccount"
|
||||
/>
|
||||
</Dialog>
|
||||
</GenericCreateUpdateDrawer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog'
|
||||
import AccountCreateUpdateForm from '@/components/AccountCreateUpdateForm'
|
||||
import GenericCreateUpdateDrawer from '@/layout/components/GenericCreateUpdateDrawer/index.vue'
|
||||
import AccountCreateUpdateForm from '@/components/Apps/AccountCreateUpdateForm/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'CreateAccountDialog',
|
||||
components: {
|
||||
Dialog,
|
||||
AccountCreateUpdateForm
|
||||
AccountCreateUpdateForm,
|
||||
GenericCreateUpdateDrawer
|
||||
},
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
addTemplate: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
asset: {
|
||||
type: Object,
|
||||
default: null
|
||||
@@ -58,6 +53,9 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
protocols() {
|
||||
return this.asset ? this.asset.protocol : []
|
||||
},
|
||||
iVisible: {
|
||||
get() {
|
||||
return this.visible
|
||||
@@ -65,37 +63,40 @@ export default {
|
||||
set(val) {
|
||||
this.$emit('update:visible', val)
|
||||
}
|
||||
},
|
||||
protocols() {
|
||||
return this.asset ? this.asset.protocol : []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addAccount(form) {
|
||||
const formValue = Object.assign({}, form)
|
||||
let assets = []
|
||||
let data, url, iVisible
|
||||
if (this.asset) {
|
||||
assets = [this.asset.id]
|
||||
data = {
|
||||
asset: this.asset.id,
|
||||
...formValue
|
||||
}
|
||||
iVisible = false
|
||||
url = `/api/v1/accounts/accounts/`
|
||||
} else {
|
||||
assets = formValue.assets
|
||||
iVisible = true
|
||||
data = formValue
|
||||
url = `/api/v1/accounts/accounts/bulk/`
|
||||
if (data.assets.length === 0) {
|
||||
this.$message.error(this.$tc('assets.PleaseSelectAsset'))
|
||||
return
|
||||
}
|
||||
}
|
||||
delete formValue.assets
|
||||
if (assets.length === 0) {
|
||||
this.$message.error(this.$tc('assets.PleaseSelectAsset'))
|
||||
return
|
||||
}
|
||||
const data = []
|
||||
for (const asset of assets) {
|
||||
data.push({
|
||||
...formValue,
|
||||
asset
|
||||
})
|
||||
}
|
||||
this.$axios.post(`/api/v1/accounts/accounts/`, data).then(() => {
|
||||
this.iVisible = false
|
||||
this.$emit('add', true)
|
||||
this.$message.success(this.$tc('common.createSuccessMsg'))
|
||||
}).catch(error => this.setFieldError(error))
|
||||
this.$axios.post(url, data, {
|
||||
disableFlashErrorMsg: iVisible
|
||||
}).then((data) => {
|
||||
this.handleResult(data, null)
|
||||
this.iVisible = iVisible
|
||||
if (!iVisible) {
|
||||
this.$emit('add', true)
|
||||
}
|
||||
}).catch(error => {
|
||||
this.iVisible = true
|
||||
this.handleResult(null, error)
|
||||
})
|
||||
},
|
||||
editAccount(form) {
|
||||
const data = { ...form }
|
||||
@@ -105,6 +106,30 @@ export default {
|
||||
this.$message.success(this.$tc('common.updateSuccessMsg'))
|
||||
}).catch(error => this.setFieldError(error))
|
||||
},
|
||||
handleResult(resp, error) {
|
||||
let bulkCreate = !this.asset
|
||||
if (error && !Array.isArray(error?.response?.data)) {
|
||||
bulkCreate = false
|
||||
}
|
||||
if (resp && !Array.isArray(resp)) {
|
||||
bulkCreate = false
|
||||
}
|
||||
if (!bulkCreate) {
|
||||
if (!error) {
|
||||
this.$message.success(this.$tc('common.createSuccessMsg'))
|
||||
} else {
|
||||
this.setFieldError(error)
|
||||
}
|
||||
} else {
|
||||
let result
|
||||
if (error) {
|
||||
result = error.response.data
|
||||
} else {
|
||||
result = resp
|
||||
}
|
||||
this.$emit('bulk-create-done', result)
|
||||
}
|
||||
},
|
||||
setFieldError(error) {
|
||||
const response = error.response
|
||||
const data = response.data
|
||||
@@ -14,28 +14,36 @@
|
||||
@updateAuthDone="onUpdateAuthDone"
|
||||
/>
|
||||
<AccountCreateUpdate
|
||||
v-if="showAddDialog"
|
||||
:account="account"
|
||||
:url="url"
|
||||
:asset="iAsset"
|
||||
:account="account"
|
||||
:add-template="addTemplate"
|
||||
:title="accountCreateUpdateTitle"
|
||||
:visible.sync="showAddDialog"
|
||||
@add="addAccountSuccess"
|
||||
@bulk-create-done="showBulkCreateResult($event)"
|
||||
/>
|
||||
<ResultDialog
|
||||
v-if="showResultDialog"
|
||||
:result="createAccountResults"
|
||||
:visible.sync="showResultDialog"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListTable from '@/components/ListTable/index'
|
||||
import { ActionsFormatter } from '@/components/TableFormatters'
|
||||
import ViewSecret from './ViewSecret'
|
||||
import UpdateSecretInfo from './UpdateSecretInfo'
|
||||
import AccountCreateUpdate from './AccountCreateUpdate'
|
||||
import ListTable from '@/components/Table/ListTable/index.vue'
|
||||
import { ActionsFormatter } 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'
|
||||
|
||||
export default {
|
||||
name: 'AccountListTable',
|
||||
components: {
|
||||
ResultDialog,
|
||||
ListTable,
|
||||
UpdateSecretInfo,
|
||||
ViewSecret,
|
||||
@@ -80,11 +88,22 @@ export default {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
hasDeleteAction: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
columnsMeta: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
}
|
||||
},
|
||||
columnsDefault: {
|
||||
type: Array,
|
||||
default: () => ([
|
||||
'name', 'username', 'asset', 'privileged',
|
||||
'secret_type', 'is_active', 'date_updated'
|
||||
])
|
||||
},
|
||||
headerExtraActions: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
@@ -95,7 +114,11 @@ export default {
|
||||
return {
|
||||
showViewSecretDialog: false,
|
||||
showUpdateSecretDialog: false,
|
||||
showResultDialog: false,
|
||||
showAddDialog: false,
|
||||
showAddTemplateDialog: false,
|
||||
createAccountResults: [],
|
||||
addTemplate: false,
|
||||
accountCreateUpdateTitle: this.$t('assets.AddAccount'),
|
||||
iAsset: this.asset,
|
||||
account: {},
|
||||
@@ -106,11 +129,14 @@ export default {
|
||||
app: 'assets',
|
||||
resource: 'account'
|
||||
},
|
||||
extraQuery: {
|
||||
order: '-date_updated'
|
||||
},
|
||||
columnsExclude: ['spec_info'],
|
||||
columns: [
|
||||
'name', 'username', 'asset', 'privileged',
|
||||
'secret_type', 'source', 'actions'
|
||||
],
|
||||
columnsShow: {
|
||||
min: ['name', 'username', 'actions'],
|
||||
default: this.columnsDefault
|
||||
},
|
||||
columnsMeta: {
|
||||
name: {
|
||||
formatter: function(row) {
|
||||
@@ -189,14 +215,16 @@ export default {
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
title: this.$t('common.Delete'),
|
||||
can: this.$hasPerm('accounts.delete_account'),
|
||||
name: 'ClearSecret',
|
||||
title: this.$t('common.ClearSecret'),
|
||||
can: this.$hasPerm('accounts.change_account'),
|
||||
type: 'primary',
|
||||
callback: ({ row }) => {
|
||||
this.$axios.delete(`/api/v1/accounts/accounts/${row.id}/`).then(() => {
|
||||
this.$message.success(this.$tc('common.deleteSuccessMsg'))
|
||||
this.$refs.ListTable.reloadTable()
|
||||
this.$axios.patch(
|
||||
`/api/v1/accounts/accounts/clear-secret/`,
|
||||
{ account_ids: [row.id] }
|
||||
).then(() => {
|
||||
this.$message.success(this.$tc('common.ClearSuccessMsg'))
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -206,8 +234,8 @@ export default {
|
||||
can: ({ row }) =>
|
||||
!this.$store.getters.currentOrgIsRoot &&
|
||||
this.$hasPerm('accounts.change_account') &&
|
||||
row.asset['auto_info'].ansible_enabled &&
|
||||
row.asset['auto_info'].ping_enabled,
|
||||
row.asset['auto_config'].ansible_enabled &&
|
||||
row.asset['auto_config'].ping_enabled,
|
||||
callback: ({ row }) => {
|
||||
this.$axios.post(
|
||||
`/api/v1/accounts/accounts/tasks/`,
|
||||
@@ -221,7 +249,7 @@ export default {
|
||||
name: 'Update',
|
||||
title: this.$t('common.Update'),
|
||||
can: this.$hasPerm('accounts.change_account') && !this.$store.getters.currentOrgIsRoot,
|
||||
callback: ({ row }) => {
|
||||
callback: ({ row, col }) => {
|
||||
const data = {
|
||||
...this.asset,
|
||||
...row.asset
|
||||
@@ -229,9 +257,10 @@ export default {
|
||||
vm.account = row
|
||||
vm.iAsset = data
|
||||
vm.showAddDialog = false
|
||||
vm.addTemplate = false
|
||||
vm.accountCreateUpdateTitle = this.$t('assets.UpdateAccount')
|
||||
setTimeout(() => {
|
||||
vm.showAddDialog = true
|
||||
vm.$eventBus.$emit('showCreateUpdateDrawer', 'update', { url: this.url, row, col })
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -256,7 +285,8 @@ export default {
|
||||
},
|
||||
exportOptions: {
|
||||
url: this.exportUrl,
|
||||
mfaVerifyRequired: true
|
||||
mfaVerifyRequired: true,
|
||||
tips: this.$t('accounts.AccountExportTips')
|
||||
},
|
||||
importOptions: {
|
||||
canImportCreate: this.$hasPerm('accounts.add_account'),
|
||||
@@ -275,17 +305,52 @@ export default {
|
||||
setTimeout(() => {
|
||||
vm.iAsset = this.asset
|
||||
vm.account = {}
|
||||
vm.addTemplate = false
|
||||
vm.accountCreateUpdateTitle = this.$t('assets.AddAccount')
|
||||
vm.showAddDialog = true
|
||||
vm.$eventBus.$emit('showCreateUpdateDrawer', 'create', { url: vm.url })
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'add-template',
|
||||
title: this.$t('common.TemplateAdd'),
|
||||
has: !(this.platform || this.asset),
|
||||
can: () => {
|
||||
return vm.$hasPerm('accounts.add_account') && !this.$store.getters.currentOrgIsRoot
|
||||
},
|
||||
callback: async() => {
|
||||
await this.getAssetDetail()
|
||||
setTimeout(() => {
|
||||
vm.iAsset = this.asset
|
||||
vm.account = {}
|
||||
vm.addTemplate = true
|
||||
vm.accountCreateUpdateTitle = this.$t('assets.AddAccount')
|
||||
vm.$eventBus.$emit('showCreateUpdateDrawer', 'create', { url: vm.url })
|
||||
})
|
||||
}
|
||||
},
|
||||
...this.headerExtraActions
|
||||
// {
|
||||
// name: 'autocreate',
|
||||
// title: this.$t('accounts.AutoCreate'),
|
||||
// type: 'default'
|
||||
// }
|
||||
],
|
||||
extraMoreActions: [
|
||||
{
|
||||
name: 'ClearSecrets',
|
||||
title: this.$t('common.ClearSecret'),
|
||||
type: 'primary',
|
||||
fa: 'clean',
|
||||
can: ({ selectedRows }) => {
|
||||
return selectedRows.length > 0 && vm.$hasPerm('accounts.change_account')
|
||||
},
|
||||
callback: function({ selectedRows }) {
|
||||
const ids = selectedRows.map(v => { return v.id })
|
||||
this.$axios.patch(
|
||||
'/api/v1/accounts/accounts/clear-secret/',
|
||||
{ account_ids: ids }).then(() => {
|
||||
this.$message.success(this.$tc('common.ClearSuccessMsg'))
|
||||
}).catch(err => {
|
||||
this.$message.error(this.$tc('common.bulkClearErrorMsg' + ' ' + err))
|
||||
})
|
||||
}.bind(this)
|
||||
}
|
||||
],
|
||||
canBulkDelete: vm.$hasPerm('accounts.delete_account'),
|
||||
searchConfig: {
|
||||
@@ -312,6 +377,22 @@ export default {
|
||||
actionColumn.formatterArgs.extraActions.push(item)
|
||||
}
|
||||
}
|
||||
if (this.hasDeleteAction) {
|
||||
this.tableConfig.columnsMeta.actions.formatterArgs.extraActions.push(
|
||||
{
|
||||
name: 'Delete',
|
||||
title: this.$t('common.Delete'),
|
||||
can: this.$hasPerm('accounts.delete_account'),
|
||||
type: 'primary',
|
||||
callback: ({ row }) => {
|
||||
this.$axios.delete(`/api/v1/accounts/accounts/${row.id}/`).then(() => {
|
||||
this.$message.success(this.$tc('common.deleteSuccessMsg'))
|
||||
this.$refs.ListTable.reloadTable()
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onUpdateAuthDone(account) {
|
||||
@@ -328,6 +409,13 @@ export default {
|
||||
},
|
||||
refresh() {
|
||||
this.$refs.ListTable.reloadTable()
|
||||
},
|
||||
showBulkCreateResult(results) {
|
||||
this.showResultDialog = false
|
||||
this.createAccountResults = results
|
||||
setTimeout(() => {
|
||||
this.showResultDialog = true
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
120
src/components/Apps/AccountListTable/BulkCreateResultDialog.vue
Normal file
120
src/components/Apps/AccountListTable/BulkCreateResultDialog.vue
Normal file
@@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<Dialog
|
||||
:show-cancel="false"
|
||||
:title="title"
|
||||
v-bind="$attrs"
|
||||
@confirm="closeDialog"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<el-alert style="margin-bottom: 10px" type="success">
|
||||
<span v-for="item of summary" :key="item.key"><b>{{ item.label }}</b>: {{ item.value }} </span>
|
||||
</el-alert>
|
||||
<DataTable :config="config" />
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog/index.vue'
|
||||
import DataTable from '@/components/Table/DataTable/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'ResultDialog',
|
||||
components: {
|
||||
DataTable,
|
||||
Dialog
|
||||
},
|
||||
props: {
|
||||
result: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
const errorProp = this.$t('common.Error')
|
||||
const stateMap = {
|
||||
'created': this.$tc('common.Created'),
|
||||
'updated': this.$tc('common.Updated'),
|
||||
'skipped': this.$tc('common.Skipped')
|
||||
}
|
||||
const stateClsMap = {
|
||||
'created': 'color-primary',
|
||||
'updated': 'color-success',
|
||||
'skipped': 'color-default'
|
||||
}
|
||||
return {
|
||||
title: this.$t('accounts.AddAccountResult'),
|
||||
config: {
|
||||
columns: [
|
||||
{
|
||||
prop: 'asset',
|
||||
label: this.$t('assets.Asset')
|
||||
},
|
||||
{
|
||||
prop: 'state',
|
||||
label: this.$t('common.Status'),
|
||||
width: '200px',
|
||||
formatter: (row) => {
|
||||
if (row.error) {
|
||||
return <span class='color-error'>{ errorProp }: { row.error }</span>
|
||||
} else if (row.state) {
|
||||
const colorCls = stateClsMap[row.state]
|
||||
const state = stateMap[row.state]
|
||||
return <span class={ colorCls }>{ state }</span>
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
totalData: this.result
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
summary() {
|
||||
const labels = {
|
||||
total: this.$tc('common.Total'),
|
||||
created: this.$tc('common.Created'),
|
||||
updated: this.$tc('common.Updated'),
|
||||
skipped: this.$tc('common.Skipped'),
|
||||
error: this.$tc('common.Error')
|
||||
}
|
||||
const grouped = _.groupBy(this.result, 'state')
|
||||
const groupedLength = _.mapValues(grouped, 'length')
|
||||
groupedLength['total'] = this.result.length
|
||||
return _.map(groupedLength, (value, key) => {
|
||||
return {
|
||||
label: labels[key],
|
||||
value: value,
|
||||
key: key
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
closeDialog() {
|
||||
this.$emit('update:visible', false)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.color-error {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.color-primary {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.color-success {
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.color-default {
|
||||
}
|
||||
|
||||
::v-deep .el-data-table .el-table .el-table__row > td > div > span {
|
||||
white-space: inherit;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<script>
|
||||
import { GenericListTableDialog } from '@/layout/components'
|
||||
import { ShowKeyCopyFormatter } from '@/components/TableFormatters'
|
||||
import { ShowKeyCopyFormatter } from '@/components/Table/TableFormatters'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -27,8 +27,9 @@ export default {
|
||||
visible: false,
|
||||
width: '60%',
|
||||
tableConfig: {
|
||||
id: 'history_date',
|
||||
url: `/api/v1/accounts/account-secrets/${this.account.id}/histories/`,
|
||||
columns: ['secret', 'secret_type', 'history_date'],
|
||||
columns: ['secret', 'version', 'history_date'],
|
||||
columnsMeta: {
|
||||
secret: {
|
||||
label: this.$t('assets.Password'),
|
||||
@@ -29,8 +29,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog'
|
||||
import { UpdateToken, UploadKey } from '@/components/FormFields'
|
||||
import Dialog from '@/components/Dialog/index.vue'
|
||||
import { UpdateToken, UploadKey } from '@/components/Form/FormFields'
|
||||
import { encryptPassword } from '@/utils/crypto'
|
||||
|
||||
export default {
|
||||
@@ -1,10 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<UserConfirmDialog
|
||||
:url="url"
|
||||
@UserConfirmCancel="exit"
|
||||
@UserConfirmDone="getAuthInfo"
|
||||
/>
|
||||
<Dialog
|
||||
:destroy-on-close="true"
|
||||
:show-cancel="false"
|
||||
@@ -12,7 +7,7 @@
|
||||
:visible.sync="showSecret"
|
||||
:width="'50'"
|
||||
v-bind="$attrs"
|
||||
@confirm="showSecret = false"
|
||||
@confirm="accountConfirmHandle"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<el-form :model="secretInfo" class="password-form" label-position="right" label-width="100px">
|
||||
@@ -27,7 +22,9 @@
|
||||
:cell-value="secretInfo.secret"
|
||||
:col="{ formatterArgs: {
|
||||
name: account['name'],
|
||||
secretType: secretType || ''
|
||||
}}"
|
||||
@input="onShowKeyCopyFormatterChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="secretType === 'ssh_key'" :label="$tc('assets.sshKeyFingerprint')">
|
||||
@@ -61,17 +58,16 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog'
|
||||
import PasswordHistoryDialog from './PasswordHistoryDialog'
|
||||
import UserConfirmDialog from '@/components/UserConfirmDialog'
|
||||
import { ShowKeyCopyFormatter } from '@/components/TableFormatters'
|
||||
import Dialog from '@/components/Dialog/index.vue'
|
||||
import PasswordHistoryDialog from './PasswordHistoryDialog.vue'
|
||||
import { ShowKeyCopyFormatter } from '@/components/Table/TableFormatters'
|
||||
import { encryptPassword } from '@/utils/crypto'
|
||||
|
||||
export default {
|
||||
name: 'ShowSecretInfo',
|
||||
components: {
|
||||
Dialog,
|
||||
PasswordHistoryDialog,
|
||||
UserConfirmDialog,
|
||||
ShowKeyCopyFormatter
|
||||
},
|
||||
props: {
|
||||
@@ -100,10 +96,12 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
modifiedSecret: '',
|
||||
secretInfo: {},
|
||||
versions: '-',
|
||||
showSecret: false,
|
||||
sshKeyFingerprint: '',
|
||||
mfaDialogVisible: true,
|
||||
sshKeyFingerprint: '-',
|
||||
historyCount: 0,
|
||||
showPasswordHistoryDialog: false
|
||||
}
|
||||
@@ -121,14 +119,31 @@ export default {
|
||||
const url = `/api/v1/accounts/account-secrets/${this.account.id}/histories/?limit=1`
|
||||
this.$axios.get(url, { disableFlashErrorMsg: true }).then(resp => {
|
||||
this.versions = resp.count
|
||||
this.showSecretDialog()
|
||||
})
|
||||
} else {
|
||||
this.showSecretDialog()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getAuthInfo() {
|
||||
this.$axios.get(this.url, { disableFlashErrorMsg: true }).then(resp => {
|
||||
this.secretInfo = resp
|
||||
this.sshKeyFingerprint = resp?.spec_info
|
||||
accountConfirmHandle() {
|
||||
this.modifiedSecret && this.onChangeSecretSubmit()
|
||||
this.showSecret = false
|
||||
this.mfaDialogVisible = false
|
||||
},
|
||||
onChangeSecretSubmit() {
|
||||
const params = {
|
||||
name: this.secretInfo.name,
|
||||
secret: encryptPassword(this.modifiedSecret)
|
||||
}
|
||||
this.$axios.patch(`/api/v1/accounts/accounts/${this.account.id}/`, params).then(() => {
|
||||
this.$message.success(this.$tc('common.updateSuccessMsg'))
|
||||
})
|
||||
},
|
||||
showSecretDialog() {
|
||||
return this.$axios.get(this.url, { disableFlashErrorMsg: true }).then((res) => {
|
||||
this.secretInfo = res
|
||||
this.sshKeyFingerprint = res?.spec_info?.ssh_key_fingerprint || '-'
|
||||
this.showSecret = true
|
||||
})
|
||||
},
|
||||
@@ -137,6 +152,10 @@ export default {
|
||||
},
|
||||
showHistoryDialog() {
|
||||
this.showPasswordHistoryDialog = true
|
||||
},
|
||||
onShowKeyCopyFormatterChange(value) {
|
||||
if (value === this.secretInfo.secret) return
|
||||
this.modifiedSecret = value
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import i18n from '@/i18n/i18n'
|
||||
import { ChoicesFormatter } from '@/components/TableFormatters'
|
||||
import { ChoicesFormatter } from '@/components/Table/TableFormatters'
|
||||
|
||||
export const connectivityMeta = {
|
||||
label: i18n.t('assets.Connectivity'),
|
||||
@@ -15,7 +15,7 @@ export const connectivityMeta = {
|
||||
err: 'text-danger'
|
||||
},
|
||||
getText({ cellValue }) {
|
||||
if (cellValue?.value === '-') {
|
||||
if (cellValue?.value === '-' || cellValue?.value === 'unknown') {
|
||||
return '-'
|
||||
} else {
|
||||
return cellValue?.label
|
||||
@@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<IBox :fa="icon" :type="type" :title="title" v-bind="$attrs">
|
||||
<IBox :fa="icon" :title="title" :type="type" v-bind="$attrs">
|
||||
<table style="width: 100%">
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<AssetSelect ref="assetSelect" :disabled="disabled" :can-select="canSelect" />
|
||||
<AssetSelect ref="assetSelect" :can-select="canSelect" :disabled="disabled" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<el-button :type="type" size="small" :disabled="disabled" @click="addObjects">{{ $t('common.Add') }}</el-button>
|
||||
<el-button :disabled="disabled" :type="type" size="small" @click="addObjects">{{ $t('common.Add') }}</el-button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -16,8 +16,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import IBox from '@/components/IBox'
|
||||
import AssetSelect from '@/components/AssetSelect'
|
||||
import IBox from '@/components/IBox/index.vue'
|
||||
import AssetSelect from '@/components/Apps/AssetSelect/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'AssetRelationCard',
|
||||
@@ -5,27 +5,27 @@
|
||||
top="1vh"
|
||||
v-bind="$attrs"
|
||||
width="80vw"
|
||||
@close="handleClose"
|
||||
@cancel="handleCancel"
|
||||
@close="handleClose"
|
||||
@confirm="handleConfirm"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<AssetTreeTable
|
||||
ref="ListPage"
|
||||
v-bind="$attrs"
|
||||
:header-actions="headerActions"
|
||||
:table-config="tableConfig"
|
||||
:url="baseUrl"
|
||||
:node-url="baseNodeUrl"
|
||||
:table-config="tableConfig"
|
||||
:tree-url="`${baseNodeUrl}children/tree/`"
|
||||
:url="baseUrl"
|
||||
class="tree-table"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AssetTreeTable from '@/components/AssetTreeTable'
|
||||
import Dialog from '@/components/Dialog'
|
||||
import AssetTreeTable from '@/components/Apps/AssetTreeTable/index.vue'
|
||||
import Dialog from '@/components/Dialog/index.vue'
|
||||
|
||||
export default {
|
||||
componentName: 'AssetSelectDialog',
|
||||
@@ -86,7 +86,7 @@ export default {
|
||||
{
|
||||
prop: 'protocols',
|
||||
formatter: function(row) {
|
||||
const data = row.protocols.map(p => {
|
||||
const data = row.protocols?.map(p => {
|
||||
return <el-tag size='mini'>{p.name}/{p.port} </el-tag>
|
||||
})
|
||||
return <span> {data} </span>
|
||||
@@ -161,21 +161,15 @@ export default {
|
||||
|
||||
.tree-table {
|
||||
.search {
|
||||
.el-input__inner {
|
||||
background-color: #f3f3f3;
|
||||
}
|
||||
|
||||
.el-cascader {
|
||||
background-color: #f3f3f3;
|
||||
}
|
||||
}
|
||||
|
||||
.left {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.treebox {
|
||||
height: 70vh;
|
||||
}
|
||||
.right {
|
||||
height: calc(100vh - 200px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.mini {
|
||||
@@ -183,13 +177,13 @@ export default {
|
||||
}
|
||||
|
||||
.transition-box {
|
||||
padding: 5px;
|
||||
padding: 10px 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page ::v-deep .treebox {
|
||||
height: inherit !important;
|
||||
.page ::v-deep .treebox .ztree {
|
||||
|
||||
}
|
||||
|
||||
.asset-select-dialog ::v-deep .el-icon-circle-check {
|
||||
@@ -11,12 +11,12 @@
|
||||
<AssetSelectDialog
|
||||
v-if="dialogVisible"
|
||||
ref="dialog"
|
||||
:base-node-url="baseNodeUrl"
|
||||
:base-url="baseUrl"
|
||||
:tree-url-query="treeUrlQuery"
|
||||
:value="value"
|
||||
:visible.sync="dialogVisible"
|
||||
v-bind="$attrs"
|
||||
:tree-url-query="treeUrlQuery"
|
||||
:base-url="baseUrl"
|
||||
:base-node-url="baseNodeUrl"
|
||||
@cancel="handleCancel"
|
||||
@confirm="handleConfirm"
|
||||
v-on="$listeners"
|
||||
@@ -25,7 +25,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Select2 from '@/components/FormFields/Select2'
|
||||
import Select2 from '@/components/Form/FormFields/Select2.vue'
|
||||
import AssetSelectDialog from './dialog.vue'
|
||||
import { b } from 'css-color-function/lib/adjusters'
|
||||
|
||||
@@ -130,21 +130,11 @@ export default {
|
||||
padding: 0 0 0 3px;
|
||||
|
||||
.tree-table {
|
||||
.search {
|
||||
.el-input__inner {
|
||||
background-color: #f3f3f3;
|
||||
}
|
||||
|
||||
.el-cascader {
|
||||
background-color: #f3f3f3;
|
||||
}
|
||||
}
|
||||
|
||||
.left {
|
||||
padding: 5px;
|
||||
|
||||
.treebox {
|
||||
height: 70vh;
|
||||
.ztree {
|
||||
height: calc(100vh - 250px) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +143,7 @@ export default {
|
||||
}
|
||||
|
||||
.transition-box {
|
||||
padding: 5px;
|
||||
padding: 10px 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<TreeTable
|
||||
ref="TreeList"
|
||||
component="TabTree"
|
||||
:table-config="tableConfig"
|
||||
:active-menu.sync="treeTableConfig.activeMenu"
|
||||
:table-config="tableConfig"
|
||||
:tree-tab-config="treeTableConfig"
|
||||
component="TabTree"
|
||||
v-bind="$attrs"
|
||||
v-on="$listeners"
|
||||
>
|
||||
@@ -12,13 +12,13 @@
|
||||
<slot name="table" />
|
||||
</template>
|
||||
<div slot="rMenu" slot-scope="{data}">
|
||||
<slot name="rMenu" :data="data" />
|
||||
<slot :data="data" name="rMenu" />
|
||||
</div>
|
||||
</TreeTable>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TreeTable from '../TreeTable'
|
||||
import TreeTable from '../../Table/TreeTable/index.vue'
|
||||
import { setRouterQuery, setUrlParam } from '@/utils/common'
|
||||
import $ from '@/utils/jquery-vendor'
|
||||
|
||||
203
src/components/Apps/AutomationParams/index.vue
Normal file
203
src/components/Apps/AutomationParams/index.vue
Normal file
@@ -0,0 +1,203 @@
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<el-button
|
||||
:disabled="isDisabled"
|
||||
size="mini"
|
||||
type="primary"
|
||||
@click="onOpenDialog"
|
||||
>
|
||||
{{ $tc('common.Setting') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<Dialog
|
||||
v-if="visible"
|
||||
:destroy-on-close="true"
|
||||
:show-cancel="false"
|
||||
:show-confirm="false"
|
||||
:title="title"
|
||||
:visible.sync="visible"
|
||||
v-bind="$attrs"
|
||||
width="60%"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<AutoDataForm
|
||||
ref="autoDataForm"
|
||||
:form="form"
|
||||
class="data-form"
|
||||
v-bind="config"
|
||||
@submit="onSubmit"
|
||||
/>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Dialog from '../../Dialog'
|
||||
import AutoDataForm from '../../Form/AutoDataForm'
|
||||
|
||||
export default {
|
||||
componentName: 'AutomationParams',
|
||||
components: {
|
||||
Dialog,
|
||||
AutoDataForm
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: function() {
|
||||
return this.$t('assets.PushParams')
|
||||
}
|
||||
},
|
||||
assets: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
nodes: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
platforms: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
method: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
default: `/api/v1/assets/platform-automation-methods/`
|
||||
}
|
||||
},
|
||||
data() {
|
||||
const vm = this
|
||||
return {
|
||||
remoteMeta: {},
|
||||
visible: false,
|
||||
isDisabled: true,
|
||||
form: this.value,
|
||||
config: {
|
||||
url: this.url,
|
||||
hasSaveContinue: false,
|
||||
hasButtons: true,
|
||||
method: 'get',
|
||||
fields: [],
|
||||
fieldsMeta: {}
|
||||
},
|
||||
onFieldChangeHandler: _.debounce(vm.handleFieldChange, 1000)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
refForm() {
|
||||
return this.$refs.autoDataForm
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
nodes: {
|
||||
handler() {
|
||||
this.onFieldChangeHandler()
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
assets: {
|
||||
handler() {
|
||||
this.onFieldChangeHandler()
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
platforms: {
|
||||
handler(newVal) {
|
||||
this.onFieldChangeHandler()
|
||||
},
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.getUrlMeta()
|
||||
},
|
||||
methods: {
|
||||
async getUrlMeta() {
|
||||
const data = await this.$store.dispatch('common/getUrlMeta', { url: this.url })
|
||||
this.remoteMeta = data.actions[this.config.method.toUpperCase()] || {}
|
||||
},
|
||||
async getFilterPlatforms() {
|
||||
return await this.$axios.post(
|
||||
'/api/v1/assets/platforms/filter-nodes-assets/',
|
||||
{
|
||||
'node_ids': this.nodes,
|
||||
'asset_ids': this.assets,
|
||||
'platform_ids': this.platforms.map(i => i.id || i.pk || i)
|
||||
}
|
||||
)
|
||||
},
|
||||
async handleFieldChange() {
|
||||
const platforms = await this.getFilterPlatforms()
|
||||
let pushAccountMethods = platforms.map(i => i.automation[this.method])
|
||||
pushAccountMethods = _.uniq(pushAccountMethods)
|
||||
// 检测是否有可设置的推送方式
|
||||
const hasCanSettingPushMethods = _.intersection(pushAccountMethods, Object.keys(this.remoteMeta))
|
||||
this.setFormConfig(hasCanSettingPushMethods)
|
||||
this.isDisabled = hasCanSettingPushMethods.length <= 0
|
||||
},
|
||||
setFormConfig(methods) {
|
||||
const newForm = {}
|
||||
const fields = []
|
||||
const fieldsMeta = {}
|
||||
this.config.fields = []
|
||||
// Todo: 未来改成后端处理,生成 serializer, 这里就不用判断类型了
|
||||
const typeMapper = {
|
||||
'string': 'input',
|
||||
'boolean': 'switch'
|
||||
}
|
||||
|
||||
for (const method of methods) {
|
||||
const filterField = this.remoteMeta[method] || {}
|
||||
// 修改资产、节点时不点击设置按钮也需要获取form表单值暴露出去
|
||||
if (this.form.hasOwnProperty(method)) {
|
||||
newForm[method] = this.form[method]
|
||||
}
|
||||
fields.push([filterField.label, [method]])
|
||||
fieldsMeta[method] = {
|
||||
fields: [],
|
||||
fieldsMeta: {}
|
||||
}
|
||||
if (Object.keys(filterField?.children || {}).length > 0) {
|
||||
for (const [k, v] of Object.entries(filterField.children)) {
|
||||
const item = {
|
||||
...v,
|
||||
type: typeMapper[v.type] || 'input'
|
||||
}
|
||||
delete item.default
|
||||
fieldsMeta[method].fields.push(k)
|
||||
fieldsMeta[method].fieldsMeta[k] = item
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.form = newForm
|
||||
this.config.fields = fields
|
||||
this.config.fieldsMeta = fieldsMeta
|
||||
},
|
||||
onOpenDialog() {
|
||||
this.visible = true
|
||||
},
|
||||
onSubmit(form) {
|
||||
this.form = form
|
||||
this.$emit('input', form)
|
||||
setTimeout(() => {
|
||||
this.visible = false
|
||||
}, 100)
|
||||
this.$log.debug('Auto push form:', form)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
97
src/components/Apps/BlockedIPs/BlockedIPList.vue
Normal file
97
src/components/Apps/BlockedIPs/BlockedIPList.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<ListTable ref="ListTable" :table-config="tableConfig" :header-actions="headerActions" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListTable from '@/components/Table/ListTable/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'BlockedIPList',
|
||||
components: {
|
||||
ListTable
|
||||
},
|
||||
props: {
|
||||
object: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
const vm = this
|
||||
return {
|
||||
tableConfig: {
|
||||
url: '/api/v1/settings/security/block-ip/',
|
||||
columns: [
|
||||
'ip', 'actions'
|
||||
],
|
||||
columnsMeta: {
|
||||
ip: {
|
||||
label: this.$t('assets.ip')
|
||||
},
|
||||
actions: {
|
||||
formatterArgs: {
|
||||
hasDelete: false,
|
||||
hasUpdate: false,
|
||||
hasClone: false,
|
||||
extraActions: [
|
||||
{
|
||||
name: 'UnlockIP',
|
||||
title: this.$t('setting.Unblock'),
|
||||
can: this.$hasPerm('settings.change_security'),
|
||||
type: 'primary',
|
||||
callback: ({ row }) => {
|
||||
this.$axios.post(
|
||||
'/api/v1/settings/security/unlock-ip/',
|
||||
{ ips: [row.ip] }
|
||||
).then(() => {
|
||||
vm.$message.success(this.$tc('common.UnlockSuccessMsg'))
|
||||
vm.$refs.ListTable.reloadTable()
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
headerActions: {
|
||||
hasExport: false,
|
||||
hasImport: false,
|
||||
hasCreate: false,
|
||||
hasSearch: false,
|
||||
hasRefresh: true,
|
||||
hasBulkDelete: false,
|
||||
hasBulkUpdate: false,
|
||||
hasLeftActions: true,
|
||||
hasRightActions: true,
|
||||
extraMoreActions: [
|
||||
{
|
||||
name: 'UnlockSelected',
|
||||
title: this.$t('setting.BulkUnblock'),
|
||||
type: 'primary',
|
||||
can: ({ selectedRows }) => {
|
||||
return selectedRows.length > 0
|
||||
},
|
||||
callback: function({ selectedRows }) {
|
||||
vm.$axios.post(
|
||||
'/api/v1/settings/security/unlock-ip/',
|
||||
{
|
||||
ips: selectedRows.map(v => { return v.ip })
|
||||
}
|
||||
).then(res => {
|
||||
vm.$message.success(vm.$tc('common.UnlockSuccessMsg'))
|
||||
vm.$refs.ListTable.reloadTable()
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='less' scoped>
|
||||
|
||||
</style>
|
||||
75
src/components/Apps/BlockedIPs/index.vue
Normal file
75
src/components/Apps/BlockedIPs/index.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<el-button
|
||||
size="mini"
|
||||
type="primary"
|
||||
@click="onOpenDialog"
|
||||
>{{ $tc('common.View') }}</el-button>
|
||||
</div>
|
||||
<Dialog
|
||||
v-if="visible"
|
||||
:visible.sync="visible"
|
||||
:title="title"
|
||||
width="40%"
|
||||
:show-cancel="false"
|
||||
:show-confirm="false"
|
||||
:destroy-on-close="true"
|
||||
v-bind="$attrs"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<BlockedIPList />
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Dialog } from '@/components'
|
||||
import BlockedIPList from '@/components/Apps/BlockedIPs/BlockedIPList'
|
||||
|
||||
export default {
|
||||
componentName: 'BlockedIPs',
|
||||
components: {
|
||||
BlockedIPList,
|
||||
Dialog
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: function() {
|
||||
return this.$t('setting.BlockedIPS')
|
||||
}
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
default: `/api/v1/assets/platform-automation-methods/`
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
remoteMeta: {},
|
||||
visible: false,
|
||||
form: this.value,
|
||||
config: {
|
||||
url: this.url,
|
||||
hasSaveContinue: false,
|
||||
hasButtons: true,
|
||||
fields: [],
|
||||
fieldsMeta: {}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onOpenDialog() {
|
||||
this.visible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -33,7 +33,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog'
|
||||
import Dialog from '@/components/Dialog/index.vue'
|
||||
import { openTaskPage } from '@/utils/jms'
|
||||
|
||||
export default {
|
||||
@@ -3,8 +3,8 @@
|
||||
</template>
|
||||
|
||||
<script type="text/jsx">
|
||||
import TreeTable from '../TreeTable'
|
||||
import { DetailFormatter } from '@/components/TableFormatters'
|
||||
import TreeTable from '../../Table/TreeTable/index.vue'
|
||||
import { DetailFormatter } from '@/components/Table/TableFormatters'
|
||||
|
||||
export default {
|
||||
name: 'GrantedAssets',
|
||||
@@ -59,7 +59,8 @@ export default {
|
||||
hasTree: true,
|
||||
columnsExclude: ['spec_info'],
|
||||
columnShow: {
|
||||
min: ['name', 'address', 'accounts']
|
||||
min: ['name', 'address', 'accounts'],
|
||||
default: ['name', 'address', 'accounts', 'actions']
|
||||
},
|
||||
columnsMeta: {
|
||||
name: {
|
||||
71
src/components/Apps/ManyJsonTabs/AssetJsonTab.vue
Normal file
71
src/components/Apps/ManyJsonTabs/AssetJsonTab.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<el-row :gutter="24">
|
||||
<el-col :md="20" :sm="22">
|
||||
<ListTable v-bind="config" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListTable from '@/components/Table/ListTable/index.vue'
|
||||
import { toM2MJsonParams } from '@/utils/jms'
|
||||
|
||||
export default {
|
||||
name: 'AssetJsonTab',
|
||||
components: {
|
||||
ListTable
|
||||
},
|
||||
props: {
|
||||
object: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
const [key, value] = toM2MJsonParams(this.object.assets)
|
||||
return {
|
||||
config: {
|
||||
headerActions: {
|
||||
hasLeftActions: false,
|
||||
hasImport: false,
|
||||
hasExport: false
|
||||
},
|
||||
tableConfig: {
|
||||
url: `/api/v1/assets/assets/?${key}=${value}`,
|
||||
columns: ['name', 'address', 'platform',
|
||||
'type', 'is_active'
|
||||
],
|
||||
columnsMeta: {
|
||||
name: {
|
||||
label: this.$t('assets.Asset'),
|
||||
formatter: (row) => {
|
||||
const to = {
|
||||
name: 'AssetDetail',
|
||||
params: { id: row.id }
|
||||
}
|
||||
if (this.$hasPerm('assets.view_asset')) {
|
||||
return <router-link to={to} class='text-link'>{row.name}</router-link>
|
||||
} else {
|
||||
return <span>{row.name}</span>
|
||||
}
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
has: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iUrl() {
|
||||
return `/api/v1/users/users/`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
91
src/components/Apps/ManyJsonTabs/UserJsonTab.vue
Normal file
91
src/components/Apps/ManyJsonTabs/UserJsonTab.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<el-row :gutter="24">
|
||||
<el-col :md="20" :sm="22">
|
||||
<ListTable v-bind="config" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListTable from '@/components/Table/ListTable/index.vue'
|
||||
import { toM2MJsonParams } from '@/utils/jms'
|
||||
|
||||
export default {
|
||||
name: 'User',
|
||||
components: {
|
||||
ListTable
|
||||
},
|
||||
props: {
|
||||
object: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
const [key, value] = toM2MJsonParams(this.object.users)
|
||||
return {
|
||||
config: {
|
||||
headerActions: {
|
||||
hasLeftActions: false,
|
||||
hasImport: false,
|
||||
hasExport: false
|
||||
},
|
||||
tableConfig: {
|
||||
url: `/api/v1/users/users/?${key}=${value}`,
|
||||
columns: [
|
||||
'name', 'username', 'groups', 'system_roles',
|
||||
'org_roles', 'source', 'is_valid'
|
||||
],
|
||||
columnsMeta: {
|
||||
name: {
|
||||
label: this.$t('common.Name'),
|
||||
formatter: (row) => {
|
||||
const to = {
|
||||
name: 'UserDetail',
|
||||
params: { id: row.id }
|
||||
}
|
||||
if (this.$hasPerm('users.view_user')) {
|
||||
return <router-link to={to} class='text-link'>{row.name}</router-link>
|
||||
} else {
|
||||
return <span>{row.name}</span>
|
||||
}
|
||||
}
|
||||
},
|
||||
system_roles: {
|
||||
label: this.$t('users.SystemRoles'),
|
||||
formatter: (row) => {
|
||||
return row['system_roles'].map(item => item['display_name']).join(', ') || '-'
|
||||
},
|
||||
filters: [],
|
||||
columnKey: 'system_roles'
|
||||
},
|
||||
org_roles: {
|
||||
label: this.$t('users.OrgRoles'),
|
||||
formatter: (row) => {
|
||||
return row['org_roles'].map(item => item['display_name']).join(', ') || '-'
|
||||
},
|
||||
filters: [],
|
||||
columnKey: 'org_roles',
|
||||
has: () => {
|
||||
return this.$store.getters.hasValidLicense && !this.currentOrgIsRoot
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
has: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iUrl() {
|
||||
return `/api/v1/users/users/`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -13,7 +13,11 @@
|
||||
placement="bottom"
|
||||
>
|
||||
{{ activity.content }}
|
||||
<el-link v-if="activity.detail_url" type="primary" @click.native="onClick(activity.r_type, activity.detail_url)">
|
||||
<el-link
|
||||
v-if="activity['detail_url']"
|
||||
type="primary"
|
||||
@click.native="onClick(activity)"
|
||||
>
|
||||
{{ $tc('common.Detail') }}
|
||||
</el-link>
|
||||
</el-timeline-item>
|
||||
@@ -26,8 +30,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import IBox from '@/components/IBox'
|
||||
import DiffDetail from '@/components/Dialog/DiffDetail'
|
||||
import IBox from '@/components/IBox/index.vue'
|
||||
import DiffDetail from '@/components/Dialog/DiffDetail.vue'
|
||||
import { openTaskPage } from '@/utils/jms'
|
||||
|
||||
export default {
|
||||
@@ -66,7 +70,9 @@ export default {
|
||||
}
|
||||
})
|
||||
},
|
||||
onClick(type, taskUrl) {
|
||||
onClick(activity) {
|
||||
const type = activity['r_type']
|
||||
const taskUrl = activity['detail_url']
|
||||
if (type === 'O') {
|
||||
this.$axios.get(taskUrl).then(
|
||||
res => {
|
||||
224
src/components/Apps/UserConfirmDialog/index.vue
Normal file
224
src/components/Apps/UserConfirmDialog/index.vue
Normal file
@@ -0,0 +1,224 @@
|
||||
<template>
|
||||
<Dialog
|
||||
:close-on-click-modal="false"
|
||||
:destory-on-close="true"
|
||||
:show-cancel="false"
|
||||
:show-confirm="false"
|
||||
:title="title"
|
||||
:visible.sync="visible"
|
||||
class="dialog-content"
|
||||
v-bind="$attrs"
|
||||
width="600px"
|
||||
@confirm="visible = false"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<div v-if="confirmTypeRequired === 'relogin'">
|
||||
<el-row :gutter="24" style="margin: 0 auto;">
|
||||
<el-col :md="24" :sm="24">
|
||||
<el-alert
|
||||
:title="$tc('auth.ReLoginTitle')"
|
||||
center
|
||||
style="margin-bottom: 20px;"
|
||||
type="error"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="24" style="margin: 0 auto;">
|
||||
<el-col :md="24" :sm="24">
|
||||
<el-button class="confirm-btn" size="mini" type="primary" @click="logout">
|
||||
{{ this.$t('auth.ReLogin') }}
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-row :gutter="24" style="margin: 0 auto;">
|
||||
<el-col :md="24" :sm="24" :span="24" class="add">
|
||||
<el-select
|
||||
v-model="subTypeSelected"
|
||||
style="width: 100%; margin-bottom: 20px;"
|
||||
@change="handleSubTypeChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item of subTypeChoices"
|
||||
:key="item.name"
|
||||
:disabled="item.disabled"
|
||||
:label="item.display_name"
|
||||
:value="item.name"
|
||||
/>
|
||||
</el-select>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="24" style="margin: 0 auto;">
|
||||
<el-col :md="24" :sm="24" style="display: flex; margin-bottom: 20px;">
|
||||
<el-input v-model="secretValue" :placeholder="inputPlaceholder" :show-password="showPassword" />
|
||||
<span v-if="subTypeSelected === 'sms'" style="margin: -1px 0 0 20px;">
|
||||
<el-button
|
||||
:disabled="smsBtnDisabled"
|
||||
size="mini"
|
||||
style="line-height:20px; float: right;"
|
||||
type="primary"
|
||||
@click="sendSMSCode"
|
||||
>
|
||||
{{ smsBtnText }}
|
||||
</el-button>
|
||||
</span>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="24" style="margin: 10px auto;">
|
||||
<el-col :md="24" :sm="24">
|
||||
<el-button class="confirm-btn" size="mini" type="primary" @click="handleConfirm">
|
||||
{{ this.$t('common.Confirm') }}
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'UserConfirmDialog',
|
||||
components: {
|
||||
Dialog
|
||||
},
|
||||
props: {
|
||||
url: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
handler: {
|
||||
type: Function,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
title: this.$t('common.CurrentUserVerify'),
|
||||
smsWidth: 0,
|
||||
subTypeSelected: '',
|
||||
inputPlaceholder: '',
|
||||
smsBtnText: this.$t('common.SendVerificationCode'),
|
||||
smsBtnDisabled: false,
|
||||
confirmTypeRequired: '',
|
||||
subTypeChoices: [],
|
||||
secretValue: '',
|
||||
visible: false,
|
||||
callback: null,
|
||||
cancel: null,
|
||||
processing: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showPassword() {
|
||||
return this.confirmTypeRequired === 'password'
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// const onRecvCallback = _.debounce(this.performConfirm, 500)
|
||||
this.$eventBus.$on('showConfirmDialog', this.performConfirm)
|
||||
},
|
||||
methods: {
|
||||
handleSubTypeChange(val) {
|
||||
this.inputPlaceholder = this.subTypeChoices.filter(item => item.name === val)[0]?.placeholder
|
||||
this.smsWidth = val === 'sms' ? 6 : 0
|
||||
},
|
||||
performConfirm({ response, callback, cancel }) {
|
||||
if (this.processing || this.visible) {
|
||||
return
|
||||
}
|
||||
this.processing = true
|
||||
this.callback = callback
|
||||
this.cancel = cancel
|
||||
this.$log.debug('perform confirm action')
|
||||
const confirmType = response.data?.code
|
||||
const confirmUrl = '/api/v1/authentication/confirm/'
|
||||
this.$axios.get(confirmUrl, { params: { confirm_type: confirmType }}).then((data) => {
|
||||
this.confirmTypeRequired = data.confirm_type
|
||||
|
||||
if (this.confirmTypeRequired === 'relogin') {
|
||||
this.$axios.post(confirmUrl, { 'confirm_type': 'relogin', 'secret_key': 'x' }).then(() => {
|
||||
this.callback()
|
||||
this.visible = false
|
||||
}).catch(() => {
|
||||
this.title = this.$t('auth.NeedReLogin')
|
||||
this.visible = true
|
||||
})
|
||||
}
|
||||
this.subTypeChoices = data.content
|
||||
const defaultSubType = this.subTypeChoices.filter(item => !item.disabled)[0]
|
||||
this.subTypeSelected = defaultSubType.name
|
||||
this.inputPlaceholder = defaultSubType.placeholder
|
||||
this.visible = true
|
||||
}).catch((err) => {
|
||||
const data = err.response?.data
|
||||
const msg = data?.error || data?.detail || data?.msg || this.$t('common.GetConfirmTypeFailed')
|
||||
this.$message.error(msg)
|
||||
this.cancel(err)
|
||||
}).finally(() => {
|
||||
this.processing = false
|
||||
})
|
||||
},
|
||||
logout() {
|
||||
window.location.href = `${process.env.VUE_APP_LOGOUT_PATH}?next=${this.$route.fullPath}`
|
||||
},
|
||||
sendSMSCode() {
|
||||
this.$axios.post(`/api/v1/authentication/mfa/select/`, { type: 'sms' }).then(res => {
|
||||
this.$message.success(this.$tc('common.VerificationCodeSent'))
|
||||
let time = 60
|
||||
const interval = setInterval(() => {
|
||||
const originText = this.smsBtnText
|
||||
this.smsBtnText = this.$t('common.Pending') + `: ${time}`
|
||||
this.smsBtnDisabled = true
|
||||
time -= 1
|
||||
|
||||
if (time === 0) {
|
||||
this.smsBtnText = originText
|
||||
this.smsBtnDisabled = false
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, 1000)
|
||||
})
|
||||
},
|
||||
handleConfirm() {
|
||||
if (this.confirmTypeRequired === 'relogin') {
|
||||
return this.logout()
|
||||
}
|
||||
if (this.subTypeSelected === 'otp' && this.secretValue.length !== 6) {
|
||||
return this.$message.error(this.$tc('common.MFAErrorMsg'))
|
||||
}
|
||||
const data = {
|
||||
confirm_type: this.confirmTypeRequired,
|
||||
mfa_type: this.confirmTypeRequired === 'mfa' ? this.subTypeSelected : '',
|
||||
secret_key: this.secretValue
|
||||
}
|
||||
this.$axios.post(`/api/v1/authentication/confirm/`, data).then(res => {
|
||||
this.callback()
|
||||
this.visible = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dialog-content >>> .el-dialog__footer {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.dialog-content >>> .el-dialog {
|
||||
padding: 8px;
|
||||
|
||||
.el-dialog__body {
|
||||
padding-top: 30px;
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
width: 100%;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -17,6 +17,11 @@ export default {
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
formatterData: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
displayValue() {
|
||||
if ([null, undefined, ''].includes(this.value)) {
|
||||
@@ -65,7 +70,17 @@ export default {
|
||||
},
|
||||
render(h) {
|
||||
if (typeof this.formatter === 'function') {
|
||||
return this.formatter(this.item, this.value)
|
||||
const data = this.formatter(this.item, this.value)
|
||||
if (data instanceof Promise) {
|
||||
data.then(res => {
|
||||
this.formatterData = res
|
||||
})
|
||||
} else {
|
||||
this.formatterData = data
|
||||
}
|
||||
return (
|
||||
<span>{this.formatterData}</span>
|
||||
)
|
||||
}
|
||||
if (this.value instanceof Array) {
|
||||
const newArr = this.value || []
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<DetailCard v-if="!loading" :items="items" v-bind="$attrs" />
|
||||
<DetailCard v-if="!loading && hasObject && items.length > 0" :items="items" v-bind="$attrs" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DetailCard from './index'
|
||||
import { toSafeLocalDateStr } from '@/utils/common'
|
||||
import DetailCard from './index.vue'
|
||||
import { copy, toSafeLocalDateStr } from '@/utils/common'
|
||||
|
||||
export default {
|
||||
name: 'AutoDetailCard',
|
||||
@@ -33,6 +33,10 @@ export default {
|
||||
formatters: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
nested: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -41,19 +45,49 @@ export default {
|
||||
loading: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iObject() {
|
||||
if (this.nested) {
|
||||
return this.object[this.nested] || {}
|
||||
} else {
|
||||
return this.object
|
||||
}
|
||||
},
|
||||
hasObject() {
|
||||
return Object.keys(this.iObject).length > 0
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.optionAndGenFields()
|
||||
this.loading = false
|
||||
},
|
||||
methods: {
|
||||
defaultFormatter(fields) {
|
||||
const formatter = {}
|
||||
for (const name of fields) {
|
||||
formatter[name] = function(item, val) {
|
||||
if (val === '-') {
|
||||
return <span>{'-'}</span>
|
||||
}
|
||||
return (<span style={{ cursor: 'pointer' }} onClick={() => copy(val)}>
|
||||
{val}
|
||||
</span>)
|
||||
}
|
||||
}
|
||||
return formatter
|
||||
},
|
||||
async optionAndGenFields() {
|
||||
const data = await this.$store.dispatch('common/getUrlMeta', { url: this.url })
|
||||
const remoteMeta = data.actions['GET'] || {}
|
||||
let remoteMeta = data.actions['GET'] || {}
|
||||
if (this.nested) {
|
||||
remoteMeta = remoteMeta[this.nested]?.children || {}
|
||||
}
|
||||
let fields = this.fields
|
||||
fields = fields || Object.keys(remoteMeta)
|
||||
const defaultExcludes = ['org_id']
|
||||
const excludes = (this.excludes || []).concat(defaultExcludes)
|
||||
fields = fields.filter(item => !excludes.includes(item))
|
||||
const defaultFormatter = this.defaultFormatter(fields)
|
||||
for (const name of fields) {
|
||||
if (typeof name === 'object') {
|
||||
this.items.push(name)
|
||||
@@ -67,7 +101,7 @@ export default {
|
||||
continue
|
||||
}
|
||||
|
||||
let value = this.object[name]
|
||||
let value = this.iObject[name]
|
||||
const label = fieldMeta.label
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
@@ -107,9 +141,9 @@ export default {
|
||||
} else if (fieldMeta.type === 'labeled_choice') {
|
||||
value = value?.['label']
|
||||
} else if (fieldMeta.type === 'related_field' || fieldMeta.type === 'nested object') {
|
||||
value = value['name']
|
||||
value = value?.['name']
|
||||
} else if (fieldMeta.type === 'm2m_related_field') {
|
||||
value = value.map(item => item['name']).join(', ')
|
||||
value = value?.map(item => item['name']).join(', ')
|
||||
} else if (fieldMeta.type === 'boolean') {
|
||||
value = value ? this.$t('common.Yes') : this.$t('common.No')
|
||||
}
|
||||
@@ -125,7 +159,7 @@ export default {
|
||||
const item = {
|
||||
key: label,
|
||||
value: value,
|
||||
formatter: this.formatters[name]
|
||||
formatter: this.formatters[name] || defaultFormatter[name]
|
||||
}
|
||||
this.items.push(item)
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<IBox :title="title" :fa="fa">
|
||||
<IBox :fa="fa" :title="title">
|
||||
<el-form class="content" label-position="left" label-width="25%">
|
||||
<el-form-item v-for="item in items" :key="item.key" :label="item.key">
|
||||
<ItemValue class="item-value" :value="item.value" v-bind="item" />
|
||||
<ItemValue :value="item.value" class="item-value" v-bind="item" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<slot />
|
||||
@@ -10,8 +10,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import IBox from '../IBox'
|
||||
import ItemValue from './ItemValue'
|
||||
import IBox from '../../IBox/index.vue'
|
||||
import ItemValue from './ItemValue.vue'
|
||||
|
||||
export default {
|
||||
name: 'DetailCard',
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<IBox :type="type" :title="title" v-bind="$attrs">
|
||||
<table style="width: 100%;table-layout:fixed;" class="CardTable">
|
||||
<IBox :title="title" :type="type" v-bind="$attrs">
|
||||
<table class="CardTable" style="width: 100%;table-layout:fixed;">
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<Select2 ref="select2" v-model="select2.value" :disabled="iDisabled" v-bind="select2" />
|
||||
@@ -9,7 +9,7 @@
|
||||
<slot />
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<el-button :type="type" size="small" :loading="submitLoading" :disabled="iDisabled" @click="addObjects">
|
||||
<el-button :disabled="iDisabled" :loading="submitLoading" :type="type" size="small" @click="addObjects">
|
||||
{{ $t('common.Add') }}
|
||||
</el-button>
|
||||
</td>
|
||||
@@ -17,12 +17,12 @@
|
||||
<template v-if="showHasObjects">
|
||||
<tr v-for="obj of iHasObjects" :key="obj.value" class="item">
|
||||
<td style="width: 100%;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;">
|
||||
<el-tooltip style="margin: 4px;" effect="dark" :content="obj.label" placement="left">
|
||||
<el-tooltip :content="obj.label" effect="dark" placement="left" style="margin: 4px;">
|
||||
<b>{{ obj.label }}</b>
|
||||
</el-tooltip>
|
||||
</td>
|
||||
<td>
|
||||
<el-button size="mini" :disabled="iDisabled" type="danger" style="float: right" @click="removeObject(obj)">
|
||||
<el-button :disabled="iDisabled" size="mini" style="float: right" type="danger" @click="removeObject(obj)">
|
||||
<i class="fa fa-minus" />
|
||||
</el-button>
|
||||
</td>
|
||||
@@ -30,7 +30,7 @@
|
||||
</template>
|
||||
<tr v-if="params.hasMore && showHasMore" class="item">
|
||||
<td colspan="2">
|
||||
<el-button :type="type" :disabled="iDisabled" size="small" style="width: 100%" @click="loadMore">
|
||||
<el-button :disabled="iDisabled" :type="type" size="small" style="width: 100%" @click="loadMore">
|
||||
<i class="fa fa-arrow-down" />
|
||||
{{ $t('common.More') }}
|
||||
</el-button>
|
||||
@@ -41,10 +41,11 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Select2 from '../FormFields/Select2'
|
||||
import IBox from '../IBox'
|
||||
import Select2 from '@/components/Form/FormFields/Select2.vue'
|
||||
import IBox from '../../IBox/index.vue'
|
||||
import { createSourceIdCache } from '@/api/common'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'RelationCard',
|
||||
components: {
|
||||
@@ -117,6 +118,22 @@ export default {
|
||||
that.$message.success(that.$t('common.RemoveSuccessMsg'))
|
||||
}
|
||||
},
|
||||
onDeleteFail: {
|
||||
type: Function,
|
||||
default(error, that) {
|
||||
let msg = ''
|
||||
const data = error.response.data
|
||||
for (const item of Object.keys(data)) {
|
||||
const value = data[item]
|
||||
if (value instanceof Array) {
|
||||
msg = value.join(',')
|
||||
} else {
|
||||
msg = value
|
||||
}
|
||||
}
|
||||
that.$message.error(msg)
|
||||
}
|
||||
},
|
||||
performAdd: {
|
||||
type: Function,
|
||||
default: (objects, that) => {}
|
||||
@@ -248,7 +265,9 @@ export default {
|
||||
removeObject(obj) {
|
||||
this.performDelete(obj, this).then(
|
||||
() => this.onDeleteSuccess(obj, this)
|
||||
)
|
||||
).catch(error => {
|
||||
this.onDeleteFail(error, this)
|
||||
})
|
||||
},
|
||||
addObjects() {
|
||||
const objects = this.$refs.select2.$refs.select.selected.map(item => ({ label: item.label, value: item.value }))
|
||||
@@ -28,7 +28,10 @@
|
||||
:command="[option, action]"
|
||||
v-bind="option"
|
||||
>
|
||||
<i v-if="option.fa" :class="'fa ' + option.fa" />
|
||||
<span v-if="option.fa">
|
||||
<i v-if="option.fa.startsWith('fa-')" :class="'fa ' + option.fa" />
|
||||
<svg-icon v-else :icon-class="option.fa" style="font-size: 14px; margin-right: 2px; margin-left: -2px;" />
|
||||
</span>
|
||||
{{ option.title }}
|
||||
</el-dropdown-item>
|
||||
</template>
|
||||
@@ -158,7 +161,7 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style lang="scss" scoped>
|
||||
.layout {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -186,4 +189,22 @@ export default {
|
||||
.el-button-ungroup .action-item:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
::v-deep .more-batch-processing {
|
||||
&.el-dropdown-menu__item--divided {
|
||||
margin-top: 0;
|
||||
border-top: none;
|
||||
color: #909399;
|
||||
cursor: auto;
|
||||
font-size: 12px;
|
||||
line-height: 30px;
|
||||
border-bottom: 1px solid #E4E7ED;
|
||||
&:before {
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
&.el-dropdown-menu__item:not(.is-disabled):hover {
|
||||
color: #909399;
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -13,8 +13,7 @@
|
||||
<div v-else>
|
||||
<el-table
|
||||
:data="diff"
|
||||
height="500"
|
||||
style="width: 100%"
|
||||
class="diffTable"
|
||||
>
|
||||
<el-table-column
|
||||
:label="$tc('audits.ChangeField')"
|
||||
@@ -66,7 +65,7 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
diff: '',
|
||||
diff: [],
|
||||
detailVisible: false
|
||||
}
|
||||
},
|
||||
@@ -84,7 +83,7 @@ export default {
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style lang='scss' scoped>
|
||||
.el-tag {
|
||||
width: 100%;
|
||||
white-space: normal;
|
||||
@@ -94,4 +93,13 @@ export default {
|
||||
.el-table::before {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.diffTable {
|
||||
width: 100%;
|
||||
max-height: 80vh;
|
||||
|
||||
& >>> td {
|
||||
padding: 5px 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:append-to-body="true"
|
||||
:modal-append-to-body="true"
|
||||
:title="title"
|
||||
:top="top"
|
||||
:width="iWidth"
|
||||
class="dialog"
|
||||
:append-to-body="true"
|
||||
:modal-append-to-body="true"
|
||||
v-bind="$attrs"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<slot />
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<slot name="footer">
|
||||
<el-button v-if="showCancel" @click="onCancel">{{ cancelTitle }}</el-button>
|
||||
<el-button v-if="showConfirm" type="primary" :loading="loadingStatus" @click="onConfirm">
|
||||
<el-button v-if="showCancel && showButtons" @click="onCancel">{{ cancelTitle }}</el-button>
|
||||
<el-button v-if="showConfirm && showButtons" :loading="loadingStatus" type="primary" @click="onConfirm">
|
||||
{{ confirmTitle }}
|
||||
</el-button>
|
||||
</slot>
|
||||
@@ -29,16 +29,6 @@ export default {
|
||||
type: String,
|
||||
default: 'Title'
|
||||
},
|
||||
showCancel: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
cancelTitle: {
|
||||
type: String,
|
||||
default() {
|
||||
return this.$t('common.Cancel')
|
||||
}
|
||||
},
|
||||
top: {
|
||||
type: String,
|
||||
default: '3vh'
|
||||
@@ -51,16 +41,30 @@ export default {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
loadingStatus: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
confirmTitle: {
|
||||
type: String,
|
||||
default() {
|
||||
return this.$t('common.Confirm')
|
||||
}
|
||||
},
|
||||
showCancel: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
cancelTitle: {
|
||||
type: String,
|
||||
default() {
|
||||
return this.$t('common.Cancel')
|
||||
}
|
||||
},
|
||||
showButtons: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
loadingStatus: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
maxWidth: {
|
||||
type: String,
|
||||
default: '1200px'
|
||||
@@ -90,6 +94,10 @@ export default {
|
||||
border-radius: 0.3em;
|
||||
max-width: 1500px;
|
||||
|
||||
.el-icon-circle-check {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&__header {
|
||||
box-sizing: border-box;
|
||||
padding: 15px 22px;
|
||||
@@ -99,6 +107,10 @@ export default {
|
||||
|
||||
&__body {
|
||||
padding: 20px 30px;
|
||||
|
||||
&:has(.el-table) {
|
||||
background: #f3f3f4;
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
<template>
|
||||
<DataForm
|
||||
v-if="!loading"
|
||||
:disabled="disabled"
|
||||
:fields="iFields"
|
||||
:form="value"
|
||||
style="margin-left: -26%;margin-right: -6%"
|
||||
v-bind="kwargs"
|
||||
@change="updateValue($event)"
|
||||
@input="updateValue($event)"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DataForm from '@/components/DataForm'
|
||||
import DataForm from '@/components/Form/DataForm/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'NestedField',
|
||||
@@ -37,6 +40,8 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
formJson: JSON.stringify(this.value),
|
||||
kwargs: {
|
||||
hasReset: false,
|
||||
hasSaveContinue: false,
|
||||
@@ -65,7 +70,26 @@ export default {
|
||||
return fields
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value: {
|
||||
handler(val) {
|
||||
const valJson = JSON.stringify(val)
|
||||
// 如果不想等,证明是 value 自己变化导致的, 需要重新渲染
|
||||
if (valJson !== this.formJson) {
|
||||
this.loading = true
|
||||
setTimeout(() => {
|
||||
this.loading = false
|
||||
}, 10)
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateValue(val) {
|
||||
this.formJson = JSON.stringify(val)
|
||||
this.$emit('input', val)
|
||||
},
|
||||
objectToString(obj) {
|
||||
let data = ''
|
||||
// eslint-disable-next-line prefer-const
|
||||
@@ -23,9 +23,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DataForm from '../DataForm'
|
||||
import FormGroupHeader from '@/components/FormGroupHeader'
|
||||
import { FormFieldGenerator } from '@/components/AutoDataForm/utils'
|
||||
import DataForm from '../DataForm/index.vue'
|
||||
import FormGroupHeader from '@/components/Form/FormGroupHeader/index.vue'
|
||||
import { FormFieldGenerator } from '@/components/Form/AutoDataForm/utils'
|
||||
|
||||
export default {
|
||||
name: 'AutoDataForm',
|
||||
@@ -67,6 +67,9 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
dataForm() {
|
||||
return this.$refs.dataForm
|
||||
},
|
||||
iForm() {
|
||||
const iForm = {}
|
||||
Object.entries(this.form).forEach(([key, value]) => {
|
||||
@@ -1,12 +1,13 @@
|
||||
import Vue from 'vue'
|
||||
import Select2 from '@/components/FormFields/Select2'
|
||||
import ObjectSelect2 from '@/components/FormFields/NestedObjectSelect2'
|
||||
import NestedField from '@/components/AutoDataForm/components/NestedField'
|
||||
import Switcher from '@/components/FormFields/Switcher'
|
||||
import rules from '@/components/DataForm/rules'
|
||||
import BasicTree from '@/components/FormFields/BasicTree'
|
||||
import JsonEditor from '@/components/FormFields/JsonEditor'
|
||||
import Select2 from '@/components/Form/FormFields/Select2.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'
|
||||
import { assignIfNot } from '@/utils/common'
|
||||
import TagInput from '@/components/Form/FormFields/TagInput.vue'
|
||||
|
||||
export class FormFieldGenerator {
|
||||
constructor(emit) {
|
||||
@@ -66,6 +67,10 @@ export class FormFieldGenerator {
|
||||
type = ''
|
||||
field.component = Switcher
|
||||
break
|
||||
case 'list':
|
||||
type = 'input'
|
||||
field.component = TagInput
|
||||
break
|
||||
case 'object_related_field':
|
||||
field.component = ObjectSelect2
|
||||
break
|
||||
@@ -80,6 +85,10 @@ export class FormFieldGenerator {
|
||||
field.el = { ...field.el, ...fieldMeta }
|
||||
field.el.fields = this.generateNestFields(field, fieldMeta, fieldRemoteMeta)
|
||||
field.el.errors = {}
|
||||
field.hidden = () => {
|
||||
const hidden = fieldMeta['hiddenFields'] || (() => field.el.fields.length === 0)
|
||||
return hidden(fieldMeta, fieldRemoteMeta, field.el.fields)
|
||||
}
|
||||
break
|
||||
default:
|
||||
type = 'input'
|
||||
@@ -98,9 +107,12 @@ export class FormFieldGenerator {
|
||||
|
||||
generateNestFields(field, fieldMeta, fieldRemoteMeta) {
|
||||
const fields = []
|
||||
const nestedFields = fieldMeta.fields || []
|
||||
let nestedFields = fieldMeta.fields || []
|
||||
const nestedFieldsMeta = fieldMeta.fieldsMeta || {}
|
||||
const nestedFieldsRemoteMeta = fieldRemoteMeta.children || {}
|
||||
if (nestedFields === '__all__') {
|
||||
nestedFields = Object.keys(nestedFieldsRemoteMeta)
|
||||
}
|
||||
for (const name of nestedFields) {
|
||||
const f = this.generateField(name, nestedFieldsMeta, nestedFieldsRemoteMeta)
|
||||
fields.push(f)
|
||||
@@ -167,7 +179,7 @@ export class FormFieldGenerator {
|
||||
id: groupTitle,
|
||||
title: groupTitle,
|
||||
fields: _fields,
|
||||
name: _fields[0].id
|
||||
name: _fields[0]?.id
|
||||
}
|
||||
this.groups.push(group)
|
||||
return _fields
|
||||
@@ -175,6 +187,9 @@ export class FormFieldGenerator {
|
||||
|
||||
generateFields(_fields, fieldsMeta, remoteFieldsMeta) {
|
||||
let fields = []
|
||||
if (_fields === '__all__') {
|
||||
_fields = Object.keys(remoteFieldsMeta)
|
||||
}
|
||||
for (let field of _fields) {
|
||||
if (field instanceof Array) {
|
||||
const items = this.generateFieldGroup(field, fieldsMeta, remoteFieldsMeta)
|
||||
@@ -25,7 +25,9 @@
|
||||
multiple
|
||||
style="width:100%"
|
||||
>
|
||||
<el-option v-for="(item,index) of weekList" :key="index" :value="index+1">{{ item }}</el-option>
|
||||
<el-option v-for="(item,index) of weekList" :key="index" :value="index === 6 ? 0 : (index + 1)">
|
||||
{{ item }}
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
@@ -1,19 +1,22 @@
|
||||
<template>
|
||||
<ElFormRender
|
||||
ref="form"
|
||||
:class="mobile? 'mobile' : 'desktop'"
|
||||
:content="fields"
|
||||
:form="basicForm"
|
||||
:label-position="labelPosition"
|
||||
label-width="20%"
|
||||
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
|
||||
ref="form"
|
||||
:class="mobile? 'mobile' : 'desktop'"
|
||||
:content="fields"
|
||||
:form="basicForm"
|
||||
:label-position="labelPosition"
|
||||
class="form-fields"
|
||||
label-width="22%"
|
||||
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">
|
||||
</ElFormRender>
|
||||
<div v-if="hasButtons" class="form-buttons">
|
||||
<el-button
|
||||
v-for="button in moreButtons"
|
||||
:key="button.title"
|
||||
@@ -38,14 +41,14 @@
|
||||
v-if="defaultButton"
|
||||
:disabled="!canSubmit"
|
||||
:loading="isSubmitting"
|
||||
size="small"
|
||||
:size="submitBtnSize"
|
||||
type="primary"
|
||||
@click="submitForm('form')"
|
||||
>
|
||||
{{ $t('common.Submit') }}
|
||||
{{ submitBtnText }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</ElFormRender>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -73,6 +76,16 @@ export default {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
submitBtnSize: {
|
||||
type: String,
|
||||
default: 'small'
|
||||
},
|
||||
submitBtnText: {
|
||||
type: String,
|
||||
default() {
|
||||
return this.$t('common.Submit')
|
||||
}
|
||||
},
|
||||
hasSaveContinue: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
@@ -101,6 +114,9 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
elForm() {
|
||||
return this.$refs.form
|
||||
},
|
||||
mobile() {
|
||||
return this.$store.state.app.device === 'mobile'
|
||||
},
|
||||
@@ -123,8 +139,8 @@ export default {
|
||||
})
|
||||
},
|
||||
// 重置表单
|
||||
resetForm(formName) {
|
||||
this.$refs[formName].resetFields()
|
||||
resetForm() {
|
||||
this.$refs['form'].resetFields()
|
||||
},
|
||||
handleClick(button) {
|
||||
const callback = button.callback || function(values, form) {
|
||||
@@ -133,6 +149,9 @@ export default {
|
||||
const form = this.$refs['form']
|
||||
const values = form.getFormValue()
|
||||
callback(values, form, button)
|
||||
},
|
||||
getFormValue() {
|
||||
return this.$refs.form.getFormValue()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,7 +163,7 @@ export default {
|
||||
}
|
||||
|
||||
.el-form ::v-deep .el-form-item__content {
|
||||
width: 75%;
|
||||
width: 73%;
|
||||
}
|
||||
|
||||
.mobile.el-form ::v-deep .el-form-item__content {
|
||||
@@ -182,5 +201,7 @@ export default {
|
||||
|
||||
.form-buttons {
|
||||
margin-top: 20px;
|
||||
margin-left: 22%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -18,7 +18,8 @@ export const IpCheck = {
|
||||
required: true,
|
||||
validator: (rule, value, callback) => {
|
||||
value = value?.trim()
|
||||
if (/^[\w://.?-]+$/.test(value)) {
|
||||
const urlRegExp = /^[\w://.?=&#-]+$/
|
||||
if (urlRegExp.test(value)) {
|
||||
callback()
|
||||
} else {
|
||||
callback(new Error(i18n.t('common.FormatError')))
|
||||
@@ -52,13 +53,27 @@ export const matchAlphanumericUnderscore = {
|
||||
trigger: ['blur', 'change']
|
||||
}
|
||||
|
||||
// 不能包含()
|
||||
export const MatchExcludeParenthesis = {
|
||||
validator: (rule, value, callback) => {
|
||||
value = value?.trim()
|
||||
if (!/^[^()]*$/.test(value)) {
|
||||
callback(new Error(i18n.t('common.notParenthesis')))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: ['blur', 'change']
|
||||
}
|
||||
|
||||
export default {
|
||||
IpCheck,
|
||||
Required,
|
||||
RequiredChange,
|
||||
EmailCheck,
|
||||
specialEmojiCheck,
|
||||
matchAlphanumericUnderscore
|
||||
matchAlphanumericUnderscore,
|
||||
MatchExcludeParenthesis
|
||||
}
|
||||
|
||||
export const JsonRequired = {
|
||||
77
src/components/Form/FormFields/AllOrSpec.vue
Normal file
77
src/components/Form/FormFields/AllOrSpec.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-radio-group v-model="type" @input="handleTypeChange">
|
||||
<el-radio v-for="tp of types" :key="tp.name" :label="tp.name">
|
||||
{{ tp.label }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
<Select2 v-if="type === 'spec'" v-model="selected" v-bind="select2" @change="onChangeEmit" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Select2 from '@/components/Form/FormFields/Select2.vue'
|
||||
|
||||
export default {
|
||||
name: 'AllOrSpec',
|
||||
components: { Select2 },
|
||||
props: {
|
||||
value: {
|
||||
type: [Array],
|
||||
default: () => ([])
|
||||
},
|
||||
select2: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
resource: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
type: 'all', // all, selected
|
||||
types: [
|
||||
{ name: 'all', label: this.$t('common.All') },
|
||||
{ name: 'spec', label: this.$t('common.Spec') + this.$t('common.WordSep') + this.resource }
|
||||
],
|
||||
selected: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iValue() {
|
||||
if (this.type === 'all') {
|
||||
return ['all']
|
||||
} else {
|
||||
return this.selected
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
mounted() {
|
||||
if (!this.value || this.value.length === 0) {
|
||||
return
|
||||
}
|
||||
console.log('Value: ', this.value)
|
||||
if (this.value.indexOf('all') > -1) {
|
||||
this.type = 'all'
|
||||
} else {
|
||||
this.type = 'spec'
|
||||
this.selected = this.value
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onChangeEmit() {
|
||||
this.$emit('input', this.iValue)
|
||||
},
|
||||
handleTypeChange() {
|
||||
this.$emit('input', this.iValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
62
src/components/Form/FormFields/AttrInput.vue
Normal file
62
src/components/Form/FormFields/AttrInput.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div>
|
||||
<GenericCreateUpdateForm
|
||||
class="attr-form"
|
||||
v-bind="formConfig"
|
||||
@submit="onSubmit"
|
||||
/>
|
||||
<DataTable :config="tableConfig" class="attr-list" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import GenericCreateUpdateForm from '@/layout/components/GenericCreateUpdateForm'
|
||||
import DataTable from '@/components/Table/DataTable/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'AttrInput',
|
||||
components: { DataTable, GenericCreateUpdateForm },
|
||||
props: {
|
||||
formConfig: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
tableConfig: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
beforeSubmit: {
|
||||
type: Function,
|
||||
default: (val) => { return true }
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
methods: {
|
||||
onSubmit(value) {
|
||||
if (this.beforeSubmit(value)) {
|
||||
const clonedValue = JSON.parse(JSON.stringify(value))
|
||||
this.tableConfig.totalData.push(clonedValue)
|
||||
this.$emit('submit', this.tableConfig.totalData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.attr-form {
|
||||
>>> .el-select {
|
||||
width: 100%;
|
||||
}
|
||||
>>> .el-form-item__content {
|
||||
width: 100%;
|
||||
}
|
||||
>>> .form-buttons {
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<template>
|
||||
<el-tree
|
||||
:data="iTree"
|
||||
show-checkbox
|
||||
node-key="value"
|
||||
:default-checked-keys="iValue"
|
||||
:default-expand-all="true"
|
||||
:default-expanded-keys="iValue"
|
||||
:default-checked-keys="iValue"
|
||||
:props="defaultProps"
|
||||
:render-content="renderContent"
|
||||
class="el-tree-custom"
|
||||
node-key="value"
|
||||
show-checkbox
|
||||
@check="handleCheckChange"
|
||||
/>
|
||||
</template>
|
||||
@@ -67,11 +69,41 @@ export default {
|
||||
}
|
||||
return item
|
||||
})
|
||||
},
|
||||
renderContent(h, { node, data, store }) {
|
||||
let label = node.label
|
||||
let helpText = ''
|
||||
const regex = /(.*?)\s*\((.*?)\)/
|
||||
const match = label.match(regex)
|
||||
if (match) {
|
||||
label = match[1]
|
||||
helpText = match[2]
|
||||
}
|
||||
|
||||
return (
|
||||
<span >
|
||||
<span>{label} </span>
|
||||
{helpText
|
||||
? (<el-tooltip content={helpText} placement='top'>
|
||||
<i class='fa fa-question-circle-o'></i>
|
||||
</el-tooltip>) : ''}
|
||||
</span>)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.el-tree-custom >>> {
|
||||
.help-tips {
|
||||
margin-left: 10px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
.el-tree-node__content:hover {
|
||||
background-color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
36
src/components/Form/FormFields/BoolTextReadonly.vue
Normal file
36
src/components/Form/FormFields/BoolTextReadonly.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div>
|
||||
{{ value? trueText : falseText }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: [String, Boolean],
|
||||
default: () => false
|
||||
},
|
||||
trueText: {
|
||||
type: String,
|
||||
default: function() {
|
||||
return this.$t('common.Yes')
|
||||
}
|
||||
},
|
||||
falseText: {
|
||||
type: String,
|
||||
default: function() {
|
||||
return this.$t('common.No')
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -16,17 +16,24 @@
|
||||
>
|
||||
<i :class="item.icon" style="margin-right: 4px;" />{{ item.name }}
|
||||
</el-button>
|
||||
|
||||
<el-autocomplete
|
||||
v-if="item.type === 'input' && item.el.autoComplete"
|
||||
v-if="item.type === 'input' &&item.el && item.el.autoComplete"
|
||||
v-model="item.value"
|
||||
:placeholder="item.placeholder"
|
||||
:fetch-suggestions="item.el.query"
|
||||
class="inline-input"
|
||||
size="mini"
|
||||
@select="item.callback(item.value)"
|
||||
@change="item.callback(item.value)"
|
||||
/>
|
||||
|
||||
<el-input
|
||||
v-else-if="item.type==='input'"
|
||||
v-model="item.value"
|
||||
:placeholder="item.placeholder"
|
||||
class="inline-input"
|
||||
size="mini"
|
||||
@change="item.callback(item.value)"
|
||||
/>
|
||||
<div v-if="item.type==='select' && item.el && item.el.create" class="select-content">
|
||||
<span class="filter-label">
|
||||
{{ item.name }}:
|
||||
@@ -41,37 +41,23 @@ export default {
|
||||
shortcuts: [
|
||||
{
|
||||
text: this.$t('common.DateLast24Hours'),
|
||||
onClick(picker) {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setTime(start.getTime() - 3600 * 1000 * 24)
|
||||
picker.$emit('pick', [start, end])
|
||||
}
|
||||
onClick: (picker) => this.onShortcutClick(picker, 1)
|
||||
},
|
||||
{
|
||||
text: this.$t('common.DateLastWeek'),
|
||||
onClick(picker) {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7)
|
||||
picker.$emit('pick', [start, end])
|
||||
}
|
||||
onClick: (picker) => this.onShortcutClick(picker, 7)
|
||||
}, {
|
||||
text: this.$t('common.DateLastMonth'),
|
||||
onClick(picker) {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30)
|
||||
picker.$emit('pick', [start, end])
|
||||
}
|
||||
onClick: (picker) => this.onShortcutClick(picker, 30)
|
||||
}, {
|
||||
text: this.$t('common.DateLast3Months'),
|
||||
onClick(picker) {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90)
|
||||
picker.$emit('pick', [start, end])
|
||||
}
|
||||
onClick: (picker) => this.onShortcutClick(picker, 90)
|
||||
}, {
|
||||
text: this.$t('common.DateLastHarfYear'),
|
||||
onClick: (picker) => this.onShortcutClick(picker, 183)
|
||||
}, {
|
||||
text: this.$t('common.DateLastYear'),
|
||||
onClick: (picker) => this.onShortcutClick(picker, 365)
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -86,6 +72,12 @@ export default {
|
||||
this.$log.debug('Date change: ', val)
|
||||
this.$emit('dateChange', val)
|
||||
}
|
||||
},
|
||||
onShortcutClick(picker, day) {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setTime(start.getTime() - 3600 * 1000 * 24 * day)
|
||||
picker.$emit('pick', [start, end])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,7 +100,7 @@ export default {
|
||||
.el-input__inner {
|
||||
border: 1px solid #dcdee2;
|
||||
border-radius: 3px;
|
||||
height: 32x;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.el-date-editor ::v-deep .el-range-separator {
|
||||
96
src/components/Form/FormFields/DynamicInput.vue
Normal file
96
src/components/Form/FormFields/DynamicInput.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-for="(command, index) in iValue" :key="index" :prop="'iValue.' + index + '.value'" class="command-item">
|
||||
<el-input v-model="iValue[index]" size="mini">
|
||||
<template slot="prepend"> {{ inputTitle + ' ' + (index + 1) }}</template>
|
||||
</el-input>
|
||||
<div class="input-button">
|
||||
<el-button
|
||||
:disabled="deleteDisabled()"
|
||||
icon="el-icon-minus"
|
||||
size="mini"
|
||||
style="flex-shrink: 0;"
|
||||
type="danger"
|
||||
@click="handleDelete(command)"
|
||||
/>
|
||||
<el-button
|
||||
v-if="index === iValue.length - 1"
|
||||
icon="el-icon-plus"
|
||||
size="mini"
|
||||
style="flex-shrink: 0;"
|
||||
type="primary"
|
||||
@click="handleAdd()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => ['']
|
||||
},
|
||||
inputTitle: {
|
||||
type: String,
|
||||
default: () => ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
iValue: ['']
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
iValue: {
|
||||
handler(v) {
|
||||
this.$emit('input', Array.from(v))
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.iValue = Array.from(this.value)
|
||||
},
|
||||
methods: {
|
||||
handleDelete(command) {
|
||||
const index = this.iValue.indexOf(command)
|
||||
if (index !== -1) {
|
||||
this.iValue.splice(index, 1)
|
||||
}
|
||||
},
|
||||
handleAdd() {
|
||||
this.iValue.push('')
|
||||
},
|
||||
deleteDisabled() {
|
||||
return this.iValue.length <= 1
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-input {
|
||||
width: 85%;
|
||||
}
|
||||
.command-item {
|
||||
display: flex;
|
||||
margin: 5px 0;
|
||||
}
|
||||
.input-button {
|
||||
margin-top: 2px;
|
||||
display: flex;
|
||||
margin-left: 20px
|
||||
}
|
||||
.input-button ::v-deep .el-button.el-button--mini {
|
||||
height: 25px;
|
||||
padding: 5px;
|
||||
}
|
||||
.el-input-group__append .el-button {
|
||||
font-size: 14px;
|
||||
color: #1a1a1a;
|
||||
padding: 9px 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<Dialog
|
||||
:destroy-on-close="true"
|
||||
:show-buttons="false"
|
||||
:title="$tc('common.SelectAttrs')"
|
||||
v-bind="$attrs"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<div v-if="!loading">
|
||||
<DataForm
|
||||
:form="form"
|
||||
class="attr-form"
|
||||
v-bind="formConfig"
|
||||
@submit="onAttrDialogConfirm"
|
||||
/>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DataForm from '@/components/Form/DataForm/index.vue'
|
||||
import Dialog from '@/components/Dialog/index.vue'
|
||||
import ValueField from '@/components/Form/FormFields/JSONManyToManySelect/ValueField.vue'
|
||||
import { attrMatchOptions, typeMatchMapper } from '@/components/const'
|
||||
|
||||
export default {
|
||||
name: 'AttrFormDialog',
|
||||
components: { Dialog, DataForm },
|
||||
props: {
|
||||
attrs: {
|
||||
type: Array,
|
||||
default: () => ([])
|
||||
},
|
||||
attrsAdded: {
|
||||
type: Array,
|
||||
default: () => ([])
|
||||
},
|
||||
form: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
const vm = this
|
||||
return {
|
||||
loading: true,
|
||||
currentValue: '',
|
||||
formConfig: {
|
||||
// 为了方便更新,避免去取 fields 的索引
|
||||
hasSaveContinue: false,
|
||||
fields: [
|
||||
{
|
||||
id: 'name',
|
||||
label: this.$t('common.AttrName'),
|
||||
type: 'select',
|
||||
options: this.attrs.map(attr => {
|
||||
let disabled = this.attrsAdded.includes(attr.name) && this.form.name !== attr.name
|
||||
if (attr.disabled) {
|
||||
disabled = true
|
||||
}
|
||||
return { label: attr.label, value: attr.name, disabled: disabled }
|
||||
}),
|
||||
on: {
|
||||
change: ([val], updateForm) => {
|
||||
// 变化会影响 match 的选项
|
||||
const attr = this.attrs.find(attr => attr.name === val)
|
||||
if (!attr) return
|
||||
const matchOption = vm.updateMatchOptions(attr)
|
||||
setTimeout(() => {
|
||||
updateForm({ match: matchOption.value })
|
||||
}, 10)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'match',
|
||||
label: this.$t('common.Match'),
|
||||
type: 'select',
|
||||
options: attrMatchOptions,
|
||||
on: {
|
||||
change: ([value], updateForm) => {
|
||||
// 变化会影响 value 的选项
|
||||
setTimeout(() => {
|
||||
this.formConfig.fields[2].el.match = value
|
||||
}, 10)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'value',
|
||||
label: this.$t('common.AttrValue'),
|
||||
component: ValueField,
|
||||
el: {
|
||||
match: attrMatchOptions[0].value,
|
||||
attr: this.attrs[0]
|
||||
},
|
||||
on: {
|
||||
input: ([value], updateForm) => {
|
||||
vm.currentValue = value
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.form.index === undefined || this.form.index === -1) {
|
||||
Object.assign(this.form, this.getDefaultAttrForm())
|
||||
}
|
||||
this.updateMatchOptions()
|
||||
this.$log.debug('Attr Form config: ', this.formConfig)
|
||||
this.loading = false
|
||||
},
|
||||
methods: {
|
||||
getDefaultAttrForm() {
|
||||
const attrKeys = this.attrs.map(attr => attr.name)
|
||||
const diff = attrKeys.filter(attr => !this.attrsAdded.includes(attr))
|
||||
let name = this.attrs[0].name
|
||||
if (diff.length > 0) {
|
||||
name = diff[0]
|
||||
}
|
||||
return {
|
||||
name: name,
|
||||
match: 'exact',
|
||||
value: '',
|
||||
rel: 'and'
|
||||
}
|
||||
},
|
||||
onAttrDialogConfirm(form) {
|
||||
this.$emit('confirm', form)
|
||||
},
|
||||
updateMatchOptions(attr) {
|
||||
if (!attr) {
|
||||
attr = this.attrs.find(attr => attr.name === this.form.name)
|
||||
}
|
||||
if (!attr) return
|
||||
const attrType = attr.type || 'str'
|
||||
const matchSupports = typeMatchMapper[attrType]
|
||||
attrMatchOptions.forEach((option) => {
|
||||
option.hidden = !matchSupports.includes(option.value)
|
||||
})
|
||||
this.formConfig.fields[2].el.attr = attr
|
||||
const supports = attrMatchOptions.filter(option => !option.hidden)
|
||||
const matchOption = supports.find(item => item.value === this.form.match) || supports[0]
|
||||
this.formConfig.fields[2].el.match = matchOption.value
|
||||
return matchOption
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.attr-form {
|
||||
>>> .el-select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<Dialog
|
||||
:destroy-on-close="true"
|
||||
:show-buttons="false"
|
||||
:title="$tc('common.MatchResult')"
|
||||
:v-bind="$attrs"
|
||||
:v-on="$listeners"
|
||||
:visible.sync="iVisible"
|
||||
>
|
||||
<ListTable v-bind="attrMatchTableConfig" />
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog/index.vue'
|
||||
import ListTable from '@/components/Table/ListTable/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'AttrMatchResultDialog',
|
||||
components: { ListTable, Dialog },
|
||||
props: {
|
||||
url: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
attrs: {
|
||||
type: Array,
|
||||
default: () => ([])
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
attrMatchTableConfig: {
|
||||
headerActions: {
|
||||
hasCreate: false,
|
||||
hasImport: false,
|
||||
hasExport: false,
|
||||
hasMoreActions: false
|
||||
},
|
||||
tableConfig: {
|
||||
url: this.url,
|
||||
columns: this.attrs.filter(item => item.inTable).map(item => {
|
||||
return {
|
||||
prop: item.name,
|
||||
label: item.label,
|
||||
formatter: item.formatter
|
||||
}
|
||||
}),
|
||||
columnsMeta: {
|
||||
actions: {
|
||||
has: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iVisible: {
|
||||
set(val) {
|
||||
this.$emit('update:visible', val)
|
||||
},
|
||||
get() {
|
||||
return this.visible
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<div v-if="!loading">
|
||||
<TagInput v-if="type === 'array'" :value="iValue" @input="handleInput" />
|
||||
<Select2 v-else-if="type === 'select'" :value="iValue" v-bind="attr.el" @change="handleInput" @input="handleInput" />
|
||||
<Switcher v-else-if="type === 'bool'" :value="iValue" @change="handleInput" @input="handleInput" />
|
||||
<el-input v-else :value="iValue" @input="handleInput" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TagInput from '@/components/Form/FormFields/TagInput.vue'
|
||||
import Select2 from '@/components/Form/FormFields/Select2.vue'
|
||||
import Switcher from '@/components/Form/FormFields/Switcher.vue'
|
||||
|
||||
export default {
|
||||
name: 'ValueField',
|
||||
components: { Switcher, TagInput, Select2 },
|
||||
props: {
|
||||
value: {
|
||||
type: [String, Number, Boolean, Array, Object],
|
||||
default: () => ''
|
||||
},
|
||||
match: {
|
||||
type: String,
|
||||
default: 'exact'
|
||||
},
|
||||
attr: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
type: 'string'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iValue: {
|
||||
get() {
|
||||
const multipleTypes = ['array', 'select']
|
||||
let newValue = this.value
|
||||
if (multipleTypes.includes(this.type)) {
|
||||
if (!Array.isArray(this.value)) {
|
||||
newValue = []
|
||||
}
|
||||
} else if (this.type === 'bool') {
|
||||
newValue = !!this.value
|
||||
} else {
|
||||
if (Array.isArray(this.value)) {
|
||||
newValue = ''
|
||||
} else {
|
||||
newValue = this.value.toString()
|
||||
}
|
||||
}
|
||||
if (this.value !== newValue) {
|
||||
this.handleInput(newValue)
|
||||
}
|
||||
return newValue
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
match() {
|
||||
this.changeValueType()
|
||||
},
|
||||
attr: {
|
||||
handler() {
|
||||
this.changeValueType()
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.changeValueType()
|
||||
},
|
||||
methods: {
|
||||
handleInput(value) {
|
||||
this.$emit('input', value)
|
||||
},
|
||||
changeValueType() {
|
||||
this.loading = true
|
||||
this.type = this.getType()
|
||||
this.$nextTick(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
getType() {
|
||||
const attrType = this.attr.type || 'str'
|
||||
this.$log.debug('Value field attr type: ', attrType, this.attr, this.match)
|
||||
if (['m2m', 'fk', 'select'].includes(attrType)) {
|
||||
return 'select'
|
||||
} else if (attrType === 'bool') {
|
||||
return 'bool'
|
||||
}
|
||||
if (['in', 'ip_in'].includes(this.match)) {
|
||||
return 'array'
|
||||
} else if (this.match.startsWith('m2m')) {
|
||||
return 'select'
|
||||
} else {
|
||||
return 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<span v-if="attr.type === 'bool'">
|
||||
<i v-if="value" class="fa fa-check text-primary" />
|
||||
<i v-else class="fa fa-times text-danger" />
|
||||
</span>
|
||||
<span v-else :title="value">
|
||||
{{ value }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseFormatter from '@/components/Table/TableFormatters/base.vue'
|
||||
import { setUrlParam } from '@/utils/common'
|
||||
|
||||
export default {
|
||||
name: 'ValueFormatter',
|
||||
extends: BaseFormatter,
|
||||
props: {
|
||||
formatterArgsDefault: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {
|
||||
attrs: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
const formatterArgs = Object.assign(this.formatterArgsDefault, this.col.formatterArgs)
|
||||
return {
|
||||
formatterArgs: formatterArgs,
|
||||
loading: true,
|
||||
attr: {},
|
||||
value: ''
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
cellValue: {
|
||||
handler() {
|
||||
this.getValue()
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
formatterArgs: {
|
||||
handler() {
|
||||
this.getValue()
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
row: {
|
||||
handler() {
|
||||
this.getValue()
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
setTimeout(() => {
|
||||
this.getValue()
|
||||
}, 100)
|
||||
},
|
||||
methods: {
|
||||
async getValue() {
|
||||
this.attr = this.formatterArgs.attrs.find(attr => attr.name === this.row.name)
|
||||
const match = this.row.match
|
||||
this.$log.debug('ValueFormatter: ', this.attr, this.row.name)
|
||||
if (this.attr.type === 'm2m') {
|
||||
const url = setUrlParam(this.attr.el.url, 'ids', this.cellValue.join(','))
|
||||
const data = await this.$axios.get(url) || []
|
||||
if (data.length > 0) {
|
||||
const displayField = this.attr.el.displayField || 'name'
|
||||
this.value = data.map(item => item[displayField]).join(', ')
|
||||
}
|
||||
} else if (this.attr.type === 'select') {
|
||||
const options = this.attr.el.options || []
|
||||
const items = options.filter(item => this.cellValue.includes(item.value))
|
||||
this.value = items.map(item => item.label).join(', ')
|
||||
} else if (['in', 'ip_in'].includes(match)) {
|
||||
this.value = this.cellValue.join(', ')
|
||||
} else {
|
||||
this.value = this.cellValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
240
src/components/Form/FormFields/JSONManyToManySelect/index.vue
Normal file
240
src/components/Form/FormFields/JSONManyToManySelect/index.vue
Normal file
@@ -0,0 +1,240 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-radio-group v-model="iValue.type" @input="handleTypeChange">
|
||||
<el-radio v-for="tp of types" :key="tp.name" :label="tp.name">
|
||||
{{ tp.label }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
<Select2 v-if="iValue.type === 'ids'" v-model="ids" v-bind="select2" @change="onChangeEmit" />
|
||||
<div v-if="iValue.type === 'attrs'">
|
||||
<DataTable :config="tableConfig" class="attr-list" />
|
||||
<div class="actions">
|
||||
<el-button size="mini" type="primary" @click="handleAttrAdd">
|
||||
{{ $t('common.Add') }}
|
||||
</el-button>
|
||||
<span style="padding-left: 10px; font-size: 13px">
|
||||
<span class="help-tips; ">{{ $t('common.MatchedCount') }}:</span>
|
||||
<a class="text-link" style="padding: 0 5px;" @click="showAttrMatchTable">{{ attrMatchCount }}</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AttrFormDialog
|
||||
v-if="attrFormVisible"
|
||||
:attrs="attrs"
|
||||
:attrs-added="attrsAdded"
|
||||
:form="attrForm"
|
||||
:visible.sync="attrFormVisible"
|
||||
@confirm="handleAttrDialogConfirm"
|
||||
/>
|
||||
<AttrMatchResultDialog
|
||||
v-if="attrMatchTableVisible"
|
||||
:attrs="attrs"
|
||||
:url="attrMatchTableUrl"
|
||||
:visible.sync="attrMatchTableVisible"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Select2 from '../Select2.vue'
|
||||
import DataTable from '@/components/Table/DataTable/index.vue'
|
||||
import ValueFormatter from './ValueFormatter.vue'
|
||||
import AttrFormDialog from './AttrFormDialog.vue'
|
||||
import AttrMatchResultDialog from './AttrMatchResultDialog.vue'
|
||||
import { setUrlParam } from '@/utils/common'
|
||||
import { attrMatchOptions } from '@/components/const'
|
||||
import { toM2MJsonParams } from '@/utils/jms'
|
||||
|
||||
export default {
|
||||
name: 'JSONManyToManySelect',
|
||||
components: { AttrFormDialog, DataTable, Select2, AttrMatchResultDialog },
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {
|
||||
type: 'all'
|
||||
}
|
||||
}
|
||||
},
|
||||
select2: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
attrs: {
|
||||
type: Array,
|
||||
default: () => ([])
|
||||
},
|
||||
resource: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
attrTableColumns: {
|
||||
type: Array,
|
||||
default: () => (['name'])
|
||||
}
|
||||
},
|
||||
data() {
|
||||
const tableFormatter = (colName) => {
|
||||
return (row, col, cellValue) => {
|
||||
const value = cellValue
|
||||
switch (colName) {
|
||||
case 'name':
|
||||
return this.attrs.find(attr => attr.name === value)?.label || value
|
||||
case 'match':
|
||||
return attrMatchOptions.find(opt => opt.value === value).label || value
|
||||
case 'value':
|
||||
return Array.isArray(value) ? value.join(', ') : value
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
iValue: Object.assign({ type: 'all' }, this.value),
|
||||
attrFormVisible: false,
|
||||
attrForm: {},
|
||||
attrMatchCount: 0,
|
||||
attrMatchTableVisible: false,
|
||||
attrMatchTableUrl: '',
|
||||
ids: this.value.ids || [],
|
||||
editIndex: -1,
|
||||
types: [
|
||||
{ name: 'all', label: this.$t('common.All') + this.resource },
|
||||
{ name: 'ids', label: this.$t('common.Spec') + this.resource },
|
||||
{ name: 'attrs', label: this.$t('common.SelectByAttr') }
|
||||
],
|
||||
tableConfig: {
|
||||
columns: [
|
||||
{ prop: 'name', label: this.$t('common.AttrName'), formatter: tableFormatter('name') },
|
||||
{ prop: 'match', label: this.$t('common.Match'), formatter: tableFormatter('match') },
|
||||
{ prop: 'value', label: this.$t('common.AttrValue'), formatter: ValueFormatter, formatterArgs: { attrs: this.attrs }},
|
||||
{ prop: 'action', label: this.$t('common.Action'), align: 'center', width: '120px', formatter: (row, col, cellValue, index) => {
|
||||
return (
|
||||
<div className='input-button'>
|
||||
<el-button
|
||||
icon='el-icon-edit'
|
||||
size='mini'
|
||||
style={{ 'flexShrink': 0 }}
|
||||
type='primary'
|
||||
onClick={this.handleAttrEdit({ row, col, cellValue, index })}
|
||||
/>
|
||||
<el-button
|
||||
icon='el-icon-minus'
|
||||
size='mini'
|
||||
style={{ 'flexShrink': 0 }}
|
||||
type='danger'
|
||||
onClick={this.handleAttrDelete({ row, col, cellValue, index })}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
} }
|
||||
],
|
||||
totalData: this.value.attrs || [],
|
||||
hasPagination: false
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
attrsAdded() {
|
||||
return this.tableConfig.totalData.map(item => item.name)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
attrFormVisible(val) {
|
||||
if (!val) {
|
||||
this.getAttrsCount()
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.value.type === 'attrs') {
|
||||
this.getAttrsCount()
|
||||
}
|
||||
this.$emit('input', this.iValue)
|
||||
},
|
||||
methods: {
|
||||
showAttrMatchTable() {
|
||||
const [key, value] = this.getAttrFilterKey()
|
||||
this.attrMatchTableUrl = setUrlParam(this.select2.url, key, value)
|
||||
this.attrMatchTableVisible = true
|
||||
},
|
||||
getAttrFilterKey() {
|
||||
if (this.tableConfig.totalData.length === 0) return ''
|
||||
let attrFilter = { type: 'attrs', attrs: this.tableConfig.totalData }
|
||||
attrFilter = toM2MJsonParams(attrFilter)
|
||||
return attrFilter
|
||||
},
|
||||
getAttrsCount() {
|
||||
const attrFilter = this.getAttrFilterKey()
|
||||
if (!attrFilter) {
|
||||
this.attrMatchCount = 0
|
||||
return
|
||||
}
|
||||
const [key, value] = attrFilter
|
||||
let url = setUrlParam(this.select2.url, key, value)
|
||||
url = setUrlParam(url, 'limit', 1)
|
||||
return this.$axios.get(url).then(res => {
|
||||
this.attrMatchCount = res.count
|
||||
})
|
||||
},
|
||||
handleAttrEdit({ row, index }) {
|
||||
return () => {
|
||||
this.attrForm = Object.assign({ index }, row)
|
||||
this.editIndex = index
|
||||
this.attrFormVisible = true
|
||||
}
|
||||
},
|
||||
handleAttrDelete({ index }) {
|
||||
return () => {
|
||||
this.tableConfig.totalData.splice(index, 1)
|
||||
this.getAttrsCount()
|
||||
}
|
||||
},
|
||||
handleAttrAdd() {
|
||||
this.attrForm = {}
|
||||
this.editIndex = -1
|
||||
this.attrFormVisible = true
|
||||
},
|
||||
onChangeEmit() {
|
||||
const tp = this.iValue.type
|
||||
this.handleTypeChange(tp)
|
||||
},
|
||||
handleTypeChange(val) {
|
||||
switch (val) {
|
||||
case 'ids':
|
||||
this.$emit('input', { type: 'ids', ids: this.ids })
|
||||
break
|
||||
case 'attrs':
|
||||
this.$emit('input', { type: 'attrs', attrs: this.tableConfig.totalData })
|
||||
break
|
||||
default:
|
||||
this.$emit('input', { type: 'all' })
|
||||
break
|
||||
}
|
||||
},
|
||||
handleAttrDialogConfirm(form) {
|
||||
if (this.editIndex > -1) {
|
||||
this.tableConfig.totalData.splice(this.editIndex, 1)
|
||||
}
|
||||
const allAttrs = this.tableConfig.totalData
|
||||
// 因为可能 attr 的 name 会重复,所以需要先删除再添加
|
||||
const setIndex = allAttrs.findIndex(attr => attr.name === form.name)
|
||||
if (setIndex === -1) {
|
||||
allAttrs.push(Object.assign({}, form))
|
||||
} else {
|
||||
allAttrs.splice(setIndex, 1, Object.assign({}, form))
|
||||
}
|
||||
this.attrFormVisible = false
|
||||
this.onChangeEmit()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.attr-list {
|
||||
width: 99%;
|
||||
}
|
||||
</style>
|
||||
@@ -4,6 +4,7 @@
|
||||
v-model="resultInfo"
|
||||
:mode="'code'"
|
||||
:show-btns="false"
|
||||
:class="{resize: resize === 'vertical'}"
|
||||
@json-change="onJsonChange"
|
||||
@json-save="onJsonSave"
|
||||
@has-error="onError"
|
||||
@@ -20,6 +21,13 @@ export default {
|
||||
value: {
|
||||
type: [String, Object, Array],
|
||||
default: () => ({})
|
||||
},
|
||||
resize: {
|
||||
type: String,
|
||||
validator: (value) => {
|
||||
return ['none', 'vertical'].indexOf(value) !== -1
|
||||
},
|
||||
default: 'vertical'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -46,15 +54,21 @@ export default {
|
||||
},
|
||||
onError: _.debounce(function(value) {
|
||||
this.$message.error(this.$tc('common.FormatError'))
|
||||
}, 1100)
|
||||
}, 1500)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "~@/styles/variables.scss";
|
||||
@import "~@/styles/variables";
|
||||
|
||||
.json-editor {
|
||||
.resize {
|
||||
& > > > .jsoneditor {
|
||||
resize: vertical;
|
||||
cursor: s-resize;
|
||||
}
|
||||
}
|
||||
& > > > .jsoneditor {
|
||||
border: 1px solid #e5e6e7;
|
||||
}
|
||||
97
src/components/Form/FormFields/ListField.vue
Normal file
97
src/components/Form/FormFields/ListField.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-for="(item, index) in value || []" :key="index" class="value-item">
|
||||
<el-input :value="item" class="input-z" @input="updateValue($event, index)" />
|
||||
<div class="input-button">
|
||||
<el-button
|
||||
:disabled="disableDelete(item)"
|
||||
icon="el-icon-minus"
|
||||
size="mini"
|
||||
style="flex-shrink: 0;"
|
||||
type="danger"
|
||||
@click="handleDelete(index)"
|
||||
/>
|
||||
<el-button
|
||||
:disabled="disableAdd(item, index)"
|
||||
icon="el-icon-plus"
|
||||
size="mini"
|
||||
style="flex-shrink: 0;"
|
||||
type="primary"
|
||||
@click="handleAdd(index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ListField',
|
||||
props: {
|
||||
value: {
|
||||
type: [Array, String],
|
||||
default: () => ([])
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
},
|
||||
mounted() {
|
||||
const value = this.value
|
||||
if (!value || !Array.isArray(value) || value.length === 0) {
|
||||
this.$emit('input', [''])
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateValue(v, index) {
|
||||
const value = this.value
|
||||
value[index] = v
|
||||
this.$emit('input', value)
|
||||
},
|
||||
disableDelete() {
|
||||
return false
|
||||
},
|
||||
disableAdd() {
|
||||
return false
|
||||
},
|
||||
handleAdd(index) {
|
||||
const value = this.value
|
||||
value.splice(index + 1, 0, '')
|
||||
this.$emit('input', value)
|
||||
},
|
||||
handleDelete(index) {
|
||||
const value = this.value
|
||||
value.splice(index, 1)
|
||||
this.$emit('input', value)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.input-z {
|
||||
flex-shrink: 1;
|
||||
width: calc(100% - 80px) !important;
|
||||
}
|
||||
|
||||
.value-item {
|
||||
display: flex;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.input-button {
|
||||
display: flex;
|
||||
margin-left: 20px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.input-button ::v-deep .el-button.el-button--mini {
|
||||
height: 25px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -9,7 +9,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Select2 from './Select2'
|
||||
import Select2 from './Select2.vue'
|
||||
|
||||
export default {
|
||||
name: 'NestedObjectSelect2',
|
||||
101
src/components/Form/FormFields/PasswordRule.vue
Normal file
101
src/components/Form/FormFields/PasswordRule.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<div style="display: block">
|
||||
<el-button size="mini" type="primary" @click="visible=true">
|
||||
{{ $t('common.Setting') }}
|
||||
</el-button>
|
||||
<Dialog
|
||||
:destroy-on-close="true"
|
||||
:title="$tc('common.PasswordRule')"
|
||||
:visible.sync="visible"
|
||||
width="600px"
|
||||
@cancel="handleCancel"
|
||||
@confirm="handleConfirm"
|
||||
@open="handleOpen"
|
||||
>
|
||||
<AutoDataForm ref="dataform" v-bind="form" />
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog/index.vue'
|
||||
import AutoDataForm from '@/components/Form/AutoDataForm/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'PasswordRule',
|
||||
components: { Dialog, AutoDataForm },
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
length: 16
|
||||
})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
form: {
|
||||
url: '',
|
||||
hasButtons: false,
|
||||
hasReset: false,
|
||||
hasSaveContinue: false,
|
||||
form: Object.assign({}, this.value),
|
||||
fields: [
|
||||
{
|
||||
id: 'length',
|
||||
label: this.$t('common.Length'),
|
||||
type: 'input-number',
|
||||
el: {
|
||||
min: 8,
|
||||
max: 30
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'uppercase',
|
||||
label: this.$t('common.Uppercase'),
|
||||
type: 'switch'
|
||||
},
|
||||
{
|
||||
id: 'lowercase',
|
||||
label: this.$t('common.Lowercase'),
|
||||
type: 'switch'
|
||||
},
|
||||
{
|
||||
id: 'digit',
|
||||
label: this.$t('common.Digit'),
|
||||
type: 'switch'
|
||||
},
|
||||
{
|
||||
id: 'symbol',
|
||||
label: this.$t('common.SpecialSymbol'),
|
||||
type: 'switch'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleConfirm() {
|
||||
const formValue = this.$refs.dataform.dataForm.getFormValue()
|
||||
this.form.form = formValue
|
||||
this.$emit('input', formValue)
|
||||
setTimeout(() => {
|
||||
this.visible = false
|
||||
}, 100)
|
||||
},
|
||||
handleCancel() {
|
||||
this.$refs.dataform.dataForm.resetForm()
|
||||
setTimeout(() => {
|
||||
this.visible = false
|
||||
}, 100)
|
||||
},
|
||||
handleOpen() {
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
80
src/components/Form/FormFields/PhoneInput.vue
Normal file
80
src/components/Form/FormFields/PhoneInput.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-input v-model="rawValue.phone" :placeholder="$tc('users.inputPhone')" @input="OnInputChange">
|
||||
<el-select
|
||||
slot="prepend"
|
||||
:value="rawValue.code"
|
||||
:placeholder="$tc('common.Select')"
|
||||
style="width: 90px;"
|
||||
@change="OnChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="country in countries"
|
||||
:key="country.value"
|
||||
:label="country.value"
|
||||
:value="country.value"
|
||||
style="width: 200px;"
|
||||
>
|
||||
<span style="float: left">{{ country.name }}</span>
|
||||
<span style="float: right; font-size: 13px">{{ country.value }}</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-input>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'PhoneInput',
|
||||
props: {
|
||||
value: {
|
||||
type: [Object, String],
|
||||
default: () => ({ 'code': '+86', 'phone': '' })
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
rawValue: {},
|
||||
countries: [
|
||||
{ name: 'China(中国)', value: '+86' },
|
||||
{ name: 'HongKong(中国香港)', value: '+852' },
|
||||
{ name: 'Macao(中国澳门)', value: '+853' },
|
||||
{ name: 'Taiwan(中国台湾)', value: '+886' },
|
||||
{ name: 'America(America)', value: '+1' },
|
||||
{ name: 'Russia(Россия)', value: '+7' },
|
||||
{ name: 'France(français)', value: '+33' },
|
||||
{ name: 'Britain(Britain)', value: '+44' },
|
||||
{ name: 'Germany(Deutschland)', value: '+49' },
|
||||
{ name: 'Japan(日本)', value: '+81' },
|
||||
{ name: 'Korea(한국)', value: '+82' },
|
||||
{ name: 'India(भारत)', value: '+91' }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
fullPhone() {
|
||||
if (!this.rawValue.phone) {
|
||||
return ''
|
||||
}
|
||||
return `${this.rawValue.code}${this.rawValue.phone}`
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.rawValue = this.value || { code: '+86', phone: '' }
|
||||
this.$emit('input', this.fullPhone)
|
||||
},
|
||||
methods: {
|
||||
OnChange(countryCode) {
|
||||
this.rawValue.code = countryCode
|
||||
this.OnInputChange()
|
||||
},
|
||||
OnInputChange() {
|
||||
this.$emit('input', this.fullPhone)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<Dialog
|
||||
v-if="$attrs.visible"
|
||||
:close-on-click-modal="false"
|
||||
:destroy-on-close="true"
|
||||
:show-cancel="false"
|
||||
:show-confirm="false"
|
||||
:title="$tc('assets.PlatformProtocolConfig') + ':' + protocol.name"
|
||||
class="setting-dialog"
|
||||
v-bind="$attrs"
|
||||
width="70%"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<el-alert v-if="disabled && platformDetail" style="margin-bottom: 10px" type="success">
|
||||
{{ $t('assets.InheritPlatformConfig') }}
|
||||
<el-link :href="platformDetail" class="link-more" target="_blank">
|
||||
{{ $t('common.View') }}
|
||||
</el-link>
|
||||
<i class="fa fa-external-link" />
|
||||
</el-alert>
|
||||
<AutoDataForm
|
||||
:disabled="disabled"
|
||||
:form="form"
|
||||
class="data-form"
|
||||
v-bind="config"
|
||||
@submit="onSubmit"
|
||||
/>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { AutoDataForm, Dialog } from '@/components'
|
||||
import JsonEditor from '@/components/Form/FormFields/JsonEditor.vue'
|
||||
|
||||
export default {
|
||||
name: 'ProtocolSetting',
|
||||
components: {
|
||||
Dialog,
|
||||
AutoDataForm
|
||||
},
|
||||
props: {
|
||||
protocol: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
const vm = this
|
||||
const platform = this.$route.query.platform
|
||||
return {
|
||||
loading: true,
|
||||
form: this.protocol,
|
||||
platformDetail: platform ? '#/console/assets/platforms/' + platform : '',
|
||||
config: {
|
||||
hasSaveContinue: false,
|
||||
hasButtons: !this.disabled,
|
||||
url: '/api/v1/assets/protocol-settings/?name=' + this.protocol.name,
|
||||
fields: [
|
||||
[vm.$t('common.Basic'), [
|
||||
'primary', 'required', 'default', 'public'
|
||||
]],
|
||||
[vm.$t('common.Advanced'), ['setting']]
|
||||
],
|
||||
fieldsMeta: {
|
||||
setting: {
|
||||
fields: '__all__',
|
||||
fieldsMeta: {
|
||||
username_selector: {
|
||||
hidden: (formValue) => formValue['autofill'] !== 'basic'
|
||||
},
|
||||
password_selector: {
|
||||
hidden: (formValue) => formValue['autofill'] !== 'basic'
|
||||
},
|
||||
submit_selector: {
|
||||
hidden: (formValue) => formValue['autofill'] !== 'basic'
|
||||
},
|
||||
script: {
|
||||
component: JsonEditor,
|
||||
hidden: (formValue) => formValue['autofill'] !== 'script'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onSubmit(form) {
|
||||
this.protocol = Object.assign(this.protocol, form)
|
||||
this.$emit('update:visible', false)
|
||||
this.$emit('confirm', this.protocol)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.data-form > > > .el-form-item.form-buttons {
|
||||
padding-top: 10px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.setting-dialog > > > .el-dialog__body {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.link-more {
|
||||
font-size: 10px;
|
||||
border-bottom: solid 1px;
|
||||
color: inherit;
|
||||
}
|
||||
</style>
|
||||
366
src/components/Form/FormFields/ProtocolSelector/index.vue
Normal file
366
src/components/Form/FormFields/ProtocolSelector/index.vue
Normal file
@@ -0,0 +1,366 @@
|
||||
<template>
|
||||
<div v-if="!loading" :class="showSetting ? 'show-setting' : 'hide-setting'">
|
||||
<div v-for="(item, index) in items" :key="item.name" class="protocol-item">
|
||||
<el-input
|
||||
v-model="item.port"
|
||||
:class="isPortReadonly(item) ? '' : 'input-with-select'"
|
||||
:placeholder="portPlaceholder"
|
||||
:readonly="isPortReadonly(item)"
|
||||
:title="isPortReadonly(item) ? '端口由 URL 指定' : ''"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<template #prepend>
|
||||
<el-select
|
||||
:disabled="disableSelect(item)"
|
||||
:value="item.display_name ? item.display_name : item.name"
|
||||
class="prepend"
|
||||
@change="handleProtocolChange($event, item)"
|
||||
>
|
||||
<el-option
|
||||
v-for="p of remainProtocols"
|
||||
:key="p.name"
|
||||
:label="p.name"
|
||||
:value="p.name"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
<template #append>
|
||||
<el-button
|
||||
v-if="showSetting(item)"
|
||||
icon="el-icon-setting"
|
||||
@click="onSettingClick(item)"
|
||||
/>
|
||||
</template>
|
||||
</el-input>
|
||||
<div v-if="!readonly" class="input-button">
|
||||
<el-button
|
||||
:disabled="disableDelete(item)"
|
||||
icon="el-icon-minus"
|
||||
size="mini"
|
||||
style="flex-shrink: 0;"
|
||||
type="danger"
|
||||
@click="handleDelete(index)"
|
||||
/>
|
||||
<el-button
|
||||
v-if="index === items.length - 1"
|
||||
:disabled="disableAdd(item, index)"
|
||||
icon="el-icon-plus"
|
||||
size="mini"
|
||||
style="flex-shrink: 0;"
|
||||
type="primary"
|
||||
@click="handleAdd(index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="items.length === 0"
|
||||
icon="el-icon-plus"
|
||||
size="mini"
|
||||
style="flex-shrink: 0;"
|
||||
type="primary"
|
||||
@click="handleAdd(0)"
|
||||
/>
|
||||
<ProtocolSettingDialog
|
||||
v-if="showDialog"
|
||||
:disabled="settingReadonly || readonly"
|
||||
:protocol="currentProtocol"
|
||||
:visible.sync="showDialog"
|
||||
@confirm="handleSettingConfirm"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ProtocolSettingDialog from './ProtocolSettingDialog.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ProtocolSettingDialog
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: [String, Array],
|
||||
default: () => []
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
choices: {
|
||||
type: Array,
|
||||
default: () => ([])
|
||||
},
|
||||
readonly: {
|
||||
// 这个是在详情中,不可编辑,包括所有
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
settingReadonly: {
|
||||
// 这个是在资产添加时设置协议使用,不能修改 setting
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showSetting: {
|
||||
type: Function,
|
||||
default: (item) => true
|
||||
},
|
||||
instance: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
items: [],
|
||||
currentProtocol: {},
|
||||
showDialog: false,
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
selectedProtocolNames() {
|
||||
return this.items.map(item => item.name)
|
||||
},
|
||||
remainProtocols() {
|
||||
return this.choices.filter(proto => {
|
||||
return this.selectedProtocolNames.indexOf(proto.name) === -1
|
||||
})
|
||||
},
|
||||
portPlaceholder() {
|
||||
if (this.settingReadonly) {
|
||||
return this.$t('applications.port')
|
||||
} else {
|
||||
return this.$t('assets.DefaultPort')
|
||||
}
|
||||
},
|
||||
iChoices() {
|
||||
return this.choices.map(item => {
|
||||
delete item?.id
|
||||
return item
|
||||
})
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
choices: {
|
||||
handler(value, oldValue) {
|
||||
if (value?.length === oldValue?.length) {
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
setTimeout(() => {
|
||||
this.setDefaultItems(value)
|
||||
this.loading = false
|
||||
},)
|
||||
},
|
||||
deep: true,
|
||||
immediate: true
|
||||
},
|
||||
items: {
|
||||
handler(value) {
|
||||
if (this.settingReadonly) {
|
||||
value = value.map(i => {
|
||||
return { name: i.name, port: i.port }
|
||||
})
|
||||
}
|
||||
this.$emit('input', value)
|
||||
},
|
||||
immediate: true,
|
||||
deep: true
|
||||
},
|
||||
instance: {
|
||||
handler(value) {
|
||||
const port = this.getPortFromInstance(value)
|
||||
if (!port) {
|
||||
return
|
||||
}
|
||||
for (const item of this.items) {
|
||||
if (item['port_from_addr']) {
|
||||
item.port = port
|
||||
}
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.setDefaultItems(this.iChoices)
|
||||
this.$log.debug('Choices: ', this.choices)
|
||||
this.$log.debug('Value: ', this.value)
|
||||
this.$log.debug('Items: ', this.items)
|
||||
},
|
||||
methods: {
|
||||
getPortFromInstance(instance) {
|
||||
if (!instance) {
|
||||
return 0
|
||||
}
|
||||
let address = instance.address || ''
|
||||
if (address.indexOf('://') === -1) {
|
||||
address = `https://${address}`
|
||||
}
|
||||
const parse = require('url-parse')
|
||||
const path = parse(address)
|
||||
let port = path.port
|
||||
if (port < 0 || port > 65535) {
|
||||
port = 0
|
||||
}
|
||||
if (!port) {
|
||||
port = path.protocol === 'https:' ? 443 : 80
|
||||
}
|
||||
return port
|
||||
},
|
||||
handleSettingConfirm() {
|
||||
if (this.currentProtocol.primary) {
|
||||
const others = this.items
|
||||
.filter(item => item.name !== this.currentProtocol.name)
|
||||
.map(item => {
|
||||
item.primary = false
|
||||
return item
|
||||
})
|
||||
this.items = [this.currentProtocol, ...others]
|
||||
}
|
||||
if (this.currentProtocol.name === 'winrm') {
|
||||
if (this.currentProtocol.setting?.use_ssl) {
|
||||
this.currentProtocol.port = 5986
|
||||
} else {
|
||||
this.currentProtocol.port = 5985
|
||||
}
|
||||
}
|
||||
},
|
||||
handleDelete(index) {
|
||||
this.items = this.items.filter((value, i) => i !== index)
|
||||
},
|
||||
isRequired(item) {
|
||||
const full = this.iChoices.find(choice => {
|
||||
return choice.name === item.name
|
||||
})
|
||||
return full?.primary || full?.required
|
||||
},
|
||||
disableSelect(item) {
|
||||
return this.isRequired(item)
|
||||
},
|
||||
disableDelete(item) {
|
||||
if (this.items.length === 1) {
|
||||
return true
|
||||
}
|
||||
// 代表是设置平台
|
||||
if (!this.settingReadonly) {
|
||||
return false
|
||||
}
|
||||
return this.isRequired(item)
|
||||
},
|
||||
disableAdd(item) {
|
||||
return this.remainProtocols.length === 0 || !item.port
|
||||
},
|
||||
handleAdd(index) {
|
||||
this.items.push({ ...this.remainProtocols[0] })
|
||||
},
|
||||
handleProtocolChange(evt, item) {
|
||||
const selected = this.choices.find(item => item.name === evt)
|
||||
item.name = selected.name
|
||||
item.port = selected.port
|
||||
},
|
||||
isPortFormAddr(item) {
|
||||
return !!item['port_from_addr']
|
||||
},
|
||||
isPortReadonly(item) {
|
||||
return this.readonly || this.isPortFormAddr(item)
|
||||
},
|
||||
setPrimaryIfNeed(items) {
|
||||
// 如果没有设置主协议,设置第一个为主协议
|
||||
if (this.settingReadonly) {
|
||||
return items
|
||||
}
|
||||
const primaryProtocols = items.filter(item => item.primary)
|
||||
if (primaryProtocols.length === 0) {
|
||||
items[0].default = true
|
||||
items[0].public = true
|
||||
} else if (primaryProtocols.length > 1) {
|
||||
primaryProtocols.slice(1, primaryProtocols.length).forEach(item => {
|
||||
item.primary = false
|
||||
})
|
||||
}
|
||||
return items
|
||||
},
|
||||
setDefaultItems(choices) {
|
||||
let items = []
|
||||
const requiredItems = choices.filter(item => (item.required || item.primary))
|
||||
|
||||
if (this.value instanceof Array && this.value.length > 0) {
|
||||
const protocols = []
|
||||
this.value.forEach(item => {
|
||||
// 有默认值的情况下,设置为只读或者有id、有setting是平台
|
||||
if (!this.settingReadonly || (item?.id && item?.setting)) {
|
||||
protocols.push(item)
|
||||
} else {
|
||||
// 获取资产协议配置
|
||||
const assetDefaultItems = this.getAssetDefaultItems(item, choices)
|
||||
protocols.push(...assetDefaultItems)
|
||||
}
|
||||
})
|
||||
const notFound = requiredItems.filter(item => !protocols.find(p => p.name === item.name))
|
||||
protocols.push(...notFound)
|
||||
const allProtocolNames = protocols.map(item => item.name)
|
||||
items = protocols.filter(item => allProtocolNames.indexOf(item.name) !== -1)
|
||||
} else {
|
||||
const defaults = choices.filter(item => (item.required || item.primary || item.default))
|
||||
if (defaults.length === 0 && choices.length !== 0) {
|
||||
defaults.push(choices[0])
|
||||
}
|
||||
items = defaults
|
||||
}
|
||||
items = this.setPrimaryIfNeed(items)
|
||||
this.items = items
|
||||
},
|
||||
getAssetDefaultItems(item, choices) {
|
||||
const protocols = []
|
||||
const protocol = choices.find(i => i.name === item.name) || {}
|
||||
protocols.push({ ...protocol, ...item })
|
||||
return protocols
|
||||
},
|
||||
onSettingClick(item) {
|
||||
this.currentProtocol = item
|
||||
this.showDialog = true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.el-select >>> .el-input__inner {
|
||||
width: 120px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.input-with-select {
|
||||
flex-shrink: 1;
|
||||
width: calc(100% - 80px) !important;
|
||||
}
|
||||
|
||||
.input-with-select .el-input-group__prepend {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.protocol-item {
|
||||
display: flex;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.input-button {
|
||||
margin-top: 4px;
|
||||
display: flex;
|
||||
margin-left: 20px
|
||||
}
|
||||
|
||||
.input-button ::v-deep .el-button.el-button--mini {
|
||||
height: 25px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.el-input-group__append .el-button {
|
||||
font-size: 14px;
|
||||
color: #1a1a1a;
|
||||
padding: 9px 20px;
|
||||
}
|
||||
</style>
|
||||
5
src/components/Form/FormFields/Secret.vue
Normal file
5
src/components/Form/FormFields/Secret.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
Todo: 抽象 Secret
|
||||
</div>
|
||||
</template>
|
||||
@@ -2,10 +2,11 @@
|
||||
<el-select
|
||||
ref="select"
|
||||
v-model="iValue"
|
||||
v-loading="!initialized"
|
||||
v-loadmore="loadMore"
|
||||
:clearable="clearable"
|
||||
:disabled="selectDisabled"
|
||||
:collapse-tags="collapseTags"
|
||||
:disabled="!!selectDisabled"
|
||||
:loading="!initialized"
|
||||
:multiple="multiple"
|
||||
:options="iOptions"
|
||||
:remote="remote"
|
||||
@@ -90,6 +91,10 @@ export default {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
collapseTagsCount: {
|
||||
type: Number,
|
||||
default: 10
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -128,6 +133,9 @@ export default {
|
||||
selectRef() {
|
||||
return this.$refs.select
|
||||
},
|
||||
collapseTags() {
|
||||
return this.multiple && this.collapseTagsCount > 0 && this.value.length > this.collapseTagsCount
|
||||
},
|
||||
optionsValues() {
|
||||
return this.iOptions.map((v) => v.value)
|
||||
},
|
||||
@@ -207,6 +215,13 @@ export default {
|
||||
handler(newValue, oldValue) {
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
iOptions(val) {
|
||||
if (val.length === 0) {
|
||||
this.remote = false
|
||||
} else {
|
||||
this.remote = true
|
||||
}
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
@@ -216,11 +231,11 @@ export default {
|
||||
this.$log.debug('Value is : ', this.value)
|
||||
this.iValue = this.value
|
||||
this.initialized = true
|
||||
})
|
||||
}, 100)
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
// 因为elform存在问题,这个来清楚验证
|
||||
const elFormItem = this.$refs.select.elFormItem
|
||||
const elFormItem = this.$refs.select?.elFormItem
|
||||
if (elFormItem && elFormItem.clearValidate) {
|
||||
elFormItem.clearValidate()
|
||||
}
|
||||
@@ -315,8 +330,9 @@ export default {
|
||||
if (this.iOptions.length === 0) {
|
||||
this.remote = false
|
||||
}
|
||||
} else {
|
||||
this.remote = false
|
||||
}
|
||||
this.iValue = this.value
|
||||
},
|
||||
refresh() {
|
||||
this.resetParams()
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<el-switch
|
||||
v-model="iValue"
|
||||
inactive-color="#dcdfe6"
|
||||
:class="type"
|
||||
inactive-color="#dcdfe6"
|
||||
v-bind="$attrs"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
@@ -17,7 +17,7 @@ export default {
|
||||
default: 'primary'
|
||||
},
|
||||
value: {
|
||||
type: Boolean,
|
||||
type: [Boolean, String],
|
||||
default: true
|
||||
}
|
||||
},
|
||||
@@ -31,9 +31,14 @@ export default {
|
||||
this.$emit('input', newValue)
|
||||
},
|
||||
get: function() {
|
||||
return this.value
|
||||
return !!this.value
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value(val) {
|
||||
this.$log.debug('Switcher Value changed: ', val)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
195
src/components/Form/FormFields/TagInput.vue
Normal file
195
src/components/Form/FormFields/TagInput.vue
Normal file
@@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<div class="filter-field">
|
||||
<el-tag
|
||||
v-for="(v, k) in filterTags"
|
||||
:key="k"
|
||||
:disable-transitions="true"
|
||||
:type="tagType(v)"
|
||||
closable
|
||||
size="small"
|
||||
@click="handleTagClick(v, k)"
|
||||
@close="handleTagClose(v)"
|
||||
>
|
||||
{{ isCheckShowPassword ? changeTagShowValue(v) : v }}
|
||||
</el-tag>
|
||||
<component
|
||||
:is="component"
|
||||
ref="SearchInput"
|
||||
v-model.trim="filterValue"
|
||||
:fetch-suggestions="autocomplete"
|
||||
:placeholder="this.$t('common.EnterToContinue')"
|
||||
:type="inputType"
|
||||
class="search-input"
|
||||
@blur="focus = false"
|
||||
@change="handleConfirm"
|
||||
@focus="focus = true"
|
||||
@select="handleSelect"
|
||||
@keyup.enter.native="handleConfirm"
|
||||
/>
|
||||
<span
|
||||
v-if="replaceShowPassword && filterTags.length > 0"
|
||||
class="show-password"
|
||||
@click="handleShowPassword"
|
||||
>
|
||||
<i :class="[isCheckShowPassword ? 'fa-eye-slash' : 'fa-eye']" class="fa" />
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import i18n from '@/i18n/i18n'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
tagType: {
|
||||
type: Function,
|
||||
default: () => {
|
||||
return 'info'
|
||||
}
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: () => i18n.t('perms.Input')
|
||||
},
|
||||
autocomplete: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
replaceShowPassword: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
replaceRule: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
replaceContent: {
|
||||
type: String,
|
||||
default: '*'
|
||||
},
|
||||
inputType: {
|
||||
type: String,
|
||||
default: () => 'text'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
filterTags: this.value,
|
||||
focus: false,
|
||||
filterValue: '',
|
||||
isCheckShowPassword: this.replaceShowPassword,
|
||||
component: this.autocomplete ? 'el-autocomplete' : 'el-input'
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value(val) {
|
||||
this.filterTags = val
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleTagClose(tag) {
|
||||
this.filterTags.splice(this.filterTags.indexOf(tag), 1)
|
||||
this.$emit('change', this.filterTags)
|
||||
},
|
||||
handleSelect(item) {
|
||||
this.filterValue = item.value
|
||||
this.handleConfirm()
|
||||
},
|
||||
handleConfirm() {
|
||||
if (this.filterValue === '') return
|
||||
|
||||
if (!this.filterTags.includes(this.filterValue)) {
|
||||
this.filterTags.push(this.filterValue)
|
||||
this.filterValue = ''
|
||||
this.$emit('change', this.filterTags)
|
||||
}
|
||||
},
|
||||
handleTagClick(v, k) {
|
||||
if (this.filterValue.length !== 0) {
|
||||
this.handleConfirm()
|
||||
}
|
||||
this.$delete(this.filterTags, k)
|
||||
this.filterValue = v
|
||||
this.$refs.SearchInput.focus()
|
||||
},
|
||||
matchRule(value) {
|
||||
const regex = new RegExp(this.replaceRule)
|
||||
const replacedValue = value.replace(regex, (match, p1, p2, p3) => {
|
||||
const stars = p2.replace(/./g, this.replaceContent)
|
||||
return p1 + stars + p3
|
||||
})
|
||||
return replacedValue
|
||||
},
|
||||
changeTagShowValue(value) {
|
||||
if (this.replaceShowPassword && this.replaceRule) {
|
||||
value = this.matchRule(value)
|
||||
}
|
||||
return value
|
||||
},
|
||||
handleShowPassword() {
|
||||
this.isCheckShowPassword = !this.isCheckShowPassword
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-tag + .el-tag {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.filter-field {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
padding: 1px 2px 1px;
|
||||
border: 1px solid #dcdee2;
|
||||
border-radius: 1px;
|
||||
background-color: #fff;
|
||||
line-height: 32px;
|
||||
|
||||
&:hover {
|
||||
border-color: #C0C4CC;
|
||||
}
|
||||
|
||||
&>>> .el-tag {
|
||||
margin-top: 1px;
|
||||
font-family: sans-serif !important;
|
||||
}
|
||||
|
||||
&>>> .el-autocomplete {
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
&>>> .el-input__inner {
|
||||
max-width: 100%;
|
||||
border: none;
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-input >>> .el-input__inner {
|
||||
border: none !important;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.filter-field >>> .el-input__inner {
|
||||
height: 29px;
|
||||
}
|
||||
|
||||
.show-password {
|
||||
display: inherit;
|
||||
padding-right: 6px;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
37
src/components/Form/FormFields/TextReadonly.vue
Normal file
37
src/components/Form/FormFields/TextReadonly.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div class="input-text">
|
||||
{{ value.toString() || text }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: [String, Boolean],
|
||||
default: () => ''
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: () => ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
.input-text {
|
||||
line-height: 32px;
|
||||
padding-left: 8px;
|
||||
height: 32px;
|
||||
margin-top: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.bolder {
|
||||
border: solid 1px #dcdfe6;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,17 +1,17 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-button
|
||||
v-show="!isShow"
|
||||
type="text"
|
||||
class="button-text"
|
||||
v-show="!iShowSelect"
|
||||
:disabled="disabled"
|
||||
@click="isShow=true"
|
||||
class="button-text"
|
||||
type="text"
|
||||
@click="iShowSelect=true"
|
||||
>
|
||||
{{ iLabel }}
|
||||
<svg-icon class-name="icon" icon-class="switch" />
|
||||
</el-button>
|
||||
<Select2
|
||||
v-show="isShow"
|
||||
v-show="iShowSelect"
|
||||
ref="select2"
|
||||
v-model="iValue"
|
||||
:disabled="disabled"
|
||||
@@ -23,7 +23,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Select2 from './Select2'
|
||||
import Select2 from './Select2.vue'
|
||||
import { hasUUID } from '@/utils/common'
|
||||
|
||||
export default {
|
||||
@@ -41,7 +41,7 @@ export default {
|
||||
},
|
||||
showSelect: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
default: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
@@ -50,8 +50,8 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isShow: this.showSelect,
|
||||
iLabel: this.label
|
||||
iShowSelect: this.showSelect,
|
||||
iLabel: this.label || '-'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -67,14 +67,14 @@ export default {
|
||||
created() {
|
||||
const { path } = this.$route
|
||||
if (hasUUID(path) && this.value) {
|
||||
this.isShow = false
|
||||
this.iShowSelect = false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onSelectChange(val) {
|
||||
const options = this.$refs.select2.options.filter(item => item.value === val)
|
||||
const label = options.length > 0 ? options[0].label : ''
|
||||
this.isShow = false
|
||||
this.iShowSelect = false
|
||||
this.iLabel = val ? label : '-'
|
||||
}
|
||||
}
|
||||
@@ -84,8 +84,7 @@ export default {
|
||||
<style scoped>
|
||||
.button-text {
|
||||
color: #676a6c;
|
||||
padding-left: 0!important;
|
||||
padding-right: 0!important;
|
||||
padding: 5px!important;
|
||||
}
|
||||
.icon {
|
||||
color: #676a6c!important;
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div>
|
||||
<input ref="upLoadFile" type="file" style="display: none" @change="Onchange">
|
||||
<input ref="upLoadFile" :accept="accept" style="display: none" type="file" @change="Onchange">
|
||||
<el-button size="mini" @click.native.stop="onUpLoad">
|
||||
{{ this.$t('common.SelectFile') }}
|
||||
</el-button>
|
||||
<span>{{ fileName }}</span>
|
||||
<div v-if="tip !== ''">{{ tip }}</div>
|
||||
<input v-model="value" type="text" hidden v-on="$listeners">
|
||||
<div v-if="tip !== ''" class="help-block">{{ tip }}</div>
|
||||
<input v-model="value" hidden type="text" v-on="$listeners">
|
||||
<div>
|
||||
<img :src="preview" v-bind="$attrs">
|
||||
</div>
|
||||
@@ -23,6 +23,10 @@ export default {
|
||||
tip: {
|
||||
type: String,
|
||||
default: () => ''
|
||||
},
|
||||
accept: {
|
||||
type: String,
|
||||
default: '*'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
115
src/components/Form/FormFields/UploadSecret.vue
Normal file
115
src/components/Form/FormFields/UploadSecret.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div class="">
|
||||
<el-input
|
||||
v-model="iValue"
|
||||
type="textarea"
|
||||
:rows="rows"
|
||||
:placeholder="placeholder"
|
||||
/>
|
||||
<el-upload
|
||||
ref="upload"
|
||||
class="upload-secret"
|
||||
:action="''"
|
||||
:accept="accept"
|
||||
:auto-upload="false"
|
||||
:limit="limit"
|
||||
v-bind="$attrs"
|
||||
:on-change="handleChange"
|
||||
:on-remove="handleRemove"
|
||||
:file-list="fileList"
|
||||
>
|
||||
<el-button size="mini" type="primary">
|
||||
{{ btnText }}
|
||||
</el-button>
|
||||
<div v-if="tip" slot="tip" class="el-upload__tip">
|
||||
{{ tip }}
|
||||
</div>
|
||||
</el-upload>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: () => ''
|
||||
},
|
||||
btnText: {
|
||||
type: String,
|
||||
default: function() {
|
||||
return this.$t('common.SelectFile')
|
||||
}
|
||||
},
|
||||
rows: {
|
||||
type: Number,
|
||||
default: () => 4
|
||||
},
|
||||
limit: {
|
||||
type: Number,
|
||||
default: () => 2
|
||||
},
|
||||
accept: {
|
||||
type: String,
|
||||
default: () => ''
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: () => ''
|
||||
},
|
||||
tip: {
|
||||
type: String,
|
||||
default: () => ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
fileName: '',
|
||||
fileList: [],
|
||||
iValue: this.value
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
iValue(val) {
|
||||
this.$emit('input', val)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleChange(file, fileList) {
|
||||
const vm = this
|
||||
const newFileList = fileList.slice(-1)
|
||||
this.fileList = newFileList
|
||||
const reader = new FileReader()
|
||||
reader.onload = function(res) {
|
||||
const result = res.target.result
|
||||
vm.iValue = result
|
||||
vm.$emit('input', vm.iValue)
|
||||
}
|
||||
reader.readAsText(file.raw)
|
||||
},
|
||||
|
||||
handleRemove() {
|
||||
this.iValue = ''
|
||||
this.fileList = []
|
||||
this.$emit('input', this.iValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.upload-secret {
|
||||
display: flex;
|
||||
&>>> .el-list-enter-active,
|
||||
&>>> .el-list-leave-active {
|
||||
transition: none;
|
||||
}
|
||||
&>>> .el-list-enter,
|
||||
&>>> .el-list-leave-active {
|
||||
opacity: 0;
|
||||
}
|
||||
&>>> .el-upload-list {
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -7,7 +7,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PasswordInput from './PasswordInput'
|
||||
import PasswordInput from './PasswordInput.vue'
|
||||
import { mapGetters } from 'vuex'
|
||||
import store from '@/store'
|
||||
import i18n from '@/i18n/i18n'
|
||||
@@ -2,10 +2,10 @@
|
||||
<div class="c-weektime">
|
||||
<div class="c-schedue" />
|
||||
<div :class="{'c-schedue': true, 'c-schedue-notransi': mode}" :style="styleValue" />
|
||||
<table class="c-weektime-table" :class="{'c-min-table': colspan < 2}">
|
||||
<table :class="{'c-min-table': colspan < 2}" class="c-weektime-table">
|
||||
<thead class="c-weektime-head">
|
||||
<tr>
|
||||
<th rowspan="8" class="week-td">{{ this.$t('common.WeekCronSelect.WeekOrTime') }}</th>
|
||||
<th class="week-td" rowspan="8">{{ this.$t('common.WeekCronSelect.WeekOrTime') }}</th>
|
||||
<th :colspan="12 * colspan">00:00 - 12:00</th>
|
||||
<th :colspan="12 * colspan">12:00 - 24:00</th>
|
||||
</tr>
|
||||
@@ -13,23 +13,23 @@
|
||||
<td v-for="t in theadArr" :key="t" :colspan="colspan">{{ t }}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="c-weektime-body">
|
||||
<tbody class="c-weektime-body" @mouseleave="containerLeave()">
|
||||
<tr v-for="t in weektimeData" :key="t.row">
|
||||
<td>{{ t.value }}</td>
|
||||
<td
|
||||
v-for="n in t.child"
|
||||
:key="`${n.row}-${n.col}`"
|
||||
:data-week="n.row"
|
||||
:data-time="n.col"
|
||||
:class="selectClasses(n)"
|
||||
:data-time="n.col"
|
||||
:data-week="n.row"
|
||||
class="weektime-atom-item"
|
||||
@mouseenter="cellEnter(n)"
|
||||
@mousedown="cellDown(n)"
|
||||
@mouseenter="cellEnter(n)"
|
||||
@mouseup="cellUp(n)"
|
||||
/>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="49" class="c-weektime-preview">
|
||||
<td class="c-weektime-preview" colspan="49">
|
||||
<div class="g-clearfix c-weektime-con">
|
||||
<span class="g-pull-left">{{ this.$t('common.WeekCronSelect.CanDragSelect') }}</span>
|
||||
<a class="g-pull-right" @click.prevent="clearWeektime">{{ this.$t('common.WeekCronSelect.ClearSelection') }}</a>
|
||||
@@ -230,6 +230,11 @@ export default {
|
||||
})
|
||||
this.setTimeRange()
|
||||
},
|
||||
containerLeave() {
|
||||
this.width = 0
|
||||
this.height = 0
|
||||
this.mode = 0
|
||||
},
|
||||
setTimeRange() {
|
||||
this.timeRange = this.weektimeData.map(item => {
|
||||
return {
|
||||
@@ -314,6 +319,7 @@ export default {
|
||||
min-width: 640px;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding-right: 20px;
|
||||
}
|
||||
.c-schedue {
|
||||
background: #598fe6;
|
||||
69
src/components/Form/FormFields/index.js
Normal file
69
src/components/Form/FormFields/index.js
Normal file
@@ -0,0 +1,69 @@
|
||||
import Link from './Link.vue'
|
||||
import Select2 from './Select2.vue'
|
||||
import TagInput from './TagInput.vue'
|
||||
import Switcher from './Switcher.vue'
|
||||
import AttrInput from './AttrInput.vue'
|
||||
import UploadKey from './UploadKey.vue'
|
||||
import JsonEditor from './JsonEditor.vue'
|
||||
import PhoneInput from './PhoneInput.vue'
|
||||
import UploadField from './UploadField.vue'
|
||||
import UpdateToken from './UpdateToken.vue'
|
||||
import UserPassword from './UserPassword.vue'
|
||||
import UploadSecret from './UploadSecret.vue'
|
||||
import TextReadonly from './TextReadonly.vue'
|
||||
import DynamicInput from './DynamicInput.vue'
|
||||
import PasswordInput from './PasswordInput.vue'
|
||||
import WeekCronSelect from './WeekCronSelect.vue'
|
||||
import BoolTextReadonly from './BoolTextReadonly.vue'
|
||||
import NestedObjectSelect2 from './NestedObjectSelect2.vue'
|
||||
import DatetimeRangePicker from './DatetimeRangePicker.vue'
|
||||
import JSONManyToManySelect from './JSONManyToManySelect/index.vue'
|
||||
import PasswordRule from './PasswordRule.vue'
|
||||
|
||||
export default {
|
||||
Link,
|
||||
Switcher,
|
||||
Select2,
|
||||
TagInput,
|
||||
AttrInput,
|
||||
UploadKey,
|
||||
JsonEditor,
|
||||
UpdateToken,
|
||||
PhoneInput,
|
||||
UploadField,
|
||||
UserPassword,
|
||||
DynamicInput,
|
||||
PasswordInput,
|
||||
UploadSecret,
|
||||
PasswordRule,
|
||||
TextReadonly,
|
||||
WeekCronSelect,
|
||||
BoolTextReadonly,
|
||||
NestedObjectSelect2,
|
||||
DatetimeRangePicker,
|
||||
JSONManyToManySelect
|
||||
}
|
||||
|
||||
export {
|
||||
Link,
|
||||
Switcher,
|
||||
Select2,
|
||||
TagInput,
|
||||
AttrInput,
|
||||
UploadKey,
|
||||
JsonEditor,
|
||||
UpdateToken,
|
||||
PhoneInput,
|
||||
UploadField,
|
||||
UserPassword,
|
||||
DynamicInput,
|
||||
PasswordInput,
|
||||
UploadSecret,
|
||||
PasswordRule,
|
||||
TextReadonly,
|
||||
WeekCronSelect,
|
||||
BoolTextReadonly,
|
||||
NestedObjectSelect2,
|
||||
DatetimeRangePicker,
|
||||
JSONManyToManySelect
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
<template>
|
||||
<Dialog
|
||||
:close-on-click-modal="false"
|
||||
:destroy-on-close="true"
|
||||
:show-cancel="false"
|
||||
:show-confirm="false"
|
||||
:title="$tc('assets.PlatformProtocolConfig') + ':' + item.name"
|
||||
class="setting-dialog"
|
||||
v-bind="$attrs"
|
||||
width="70%"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<el-alert v-if="disabled" type="success">
|
||||
{{ $t('assets.InheritPlatformConfig') }}
|
||||
<el-link :href="platformDetail" class="link-more" target="_blank">
|
||||
{{ $t('common.View') }}
|
||||
</el-link>
|
||||
<i class="fa fa-external-link" />
|
||||
</el-alert>
|
||||
<AutoDataForm
|
||||
:disabled="disabled"
|
||||
:form="form"
|
||||
class="data-form"
|
||||
v-bind="config"
|
||||
@submit="onSubmit"
|
||||
/>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { AutoDataForm, Dialog } from '@/components'
|
||||
|
||||
export default {
|
||||
name: 'ProtocolSetting',
|
||||
components: {
|
||||
Dialog,
|
||||
AutoDataForm
|
||||
},
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
bases: ['required', 'default'],
|
||||
defaultSetting: {
|
||||
sftp_enabled: true,
|
||||
sftp_home: '/tmp',
|
||||
username_selector: '#username',
|
||||
password_selector: '#password',
|
||||
submit_selector: '.btn-submit'
|
||||
},
|
||||
loading: true,
|
||||
form: {},
|
||||
platformDetail: '#/console/assets/platforms/' + this.$route.query.platform,
|
||||
config: {
|
||||
hasSaveContinue: false,
|
||||
hasButtons: !this.disabled,
|
||||
url: '',
|
||||
fields: [
|
||||
[this.$t('common.Basic'), [
|
||||
{
|
||||
id: 'required',
|
||||
label: this.$t('assets.Required'),
|
||||
type: 'switch',
|
||||
helpText: this.$t('assets.RequiredProtocols')
|
||||
},
|
||||
{
|
||||
id: 'default',
|
||||
label: this.$t('assets.Default'),
|
||||
type: 'switch',
|
||||
helpText: this.$t('assets.DefaultProtocol')
|
||||
}
|
||||
]],
|
||||
[this.$t('assets.LoginConfig'), [
|
||||
{
|
||||
id: 'console',
|
||||
label: 'Console',
|
||||
type: 'switch',
|
||||
hidden: () => this.item.name !== 'rdp'
|
||||
},
|
||||
{
|
||||
id: 'security',
|
||||
label: 'Security',
|
||||
hidden: () => this.item.name !== 'rdp',
|
||||
type: 'radio-group',
|
||||
options: [
|
||||
{ label: 'Any', value: 'any' },
|
||||
{ label: 'RDP', value: 'rdp' },
|
||||
{ label: 'NLA', value: 'nla' },
|
||||
{ label: 'TLS', value: 'tls' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'sftp_enabled',
|
||||
label: this.$t('common.Enable') + ' SFTP',
|
||||
type: 'switch',
|
||||
hidden: () => this.item.name !== 'ssh'
|
||||
},
|
||||
{
|
||||
id: 'sftp_home',
|
||||
label: 'SFTP home',
|
||||
type: 'input',
|
||||
helpText: this.$t('assets.SFTPHelpMessage'),
|
||||
hidden: (form) => this.item.name !== 'ssh' || !form['sftp_enabled']
|
||||
},
|
||||
{
|
||||
id: 'username_selector',
|
||||
label: this.$t('assets.UserNameSelector'),
|
||||
type: 'input',
|
||||
hidden: (form) => this.item.name !== 'http'
|
||||
},
|
||||
{
|
||||
id: 'password_selector',
|
||||
label: this.$t('assets.PasswordSelector'),
|
||||
type: 'input',
|
||||
hidden: (form) => this.item.name !== 'http'
|
||||
},
|
||||
{
|
||||
id: 'submit_selector',
|
||||
label: this.$t('assets.SubmitSelector'),
|
||||
type: 'input',
|
||||
hidden: (form) => this.item.name !== 'http'
|
||||
},
|
||||
{
|
||||
id: 'auth_username',
|
||||
label: this.$t('assets.AuthUsername'),
|
||||
type: 'switch',
|
||||
hidden: (form) => this.item.name !== 'redis'
|
||||
}
|
||||
]]
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
const itemSetting = this.item.setting || this.defaultSetting
|
||||
for (const i of this.bases) {
|
||||
if (this.item.hasOwnProperty(i)) {
|
||||
itemSetting[i] = this.item[i]
|
||||
}
|
||||
}
|
||||
this.form = itemSetting
|
||||
},
|
||||
methods: {
|
||||
onSubmit(form) {
|
||||
for (const i of this.bases) {
|
||||
if (form.hasOwnProperty(i)) {
|
||||
this.item[i] = form[i]
|
||||
}
|
||||
}
|
||||
this.item.setting = form
|
||||
this.$emit('update:visible', false)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.data-form > > > .el-form-item.form-buttons {
|
||||
padding-top: 10px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.setting-dialog > > > .el-dialog__body {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.link-more {
|
||||
font-size: 10px;
|
||||
margin-left: 10px;
|
||||
border-bottom: solid 1px;
|
||||
color: inherit;
|
||||
}
|
||||
</style>
|
||||
@@ -1,233 +0,0 @@
|
||||
<template>
|
||||
<div :class="showSetting ? 'show-setting' : 'hide-setting'">
|
||||
<div v-for="(item, index) in items" :key="item.name" class="protocol-item">
|
||||
<el-input
|
||||
v-model="item.port"
|
||||
:class="readonly ? '' : 'input-with-select'"
|
||||
:placeholder="portPlaceholder"
|
||||
:readonly="readonly"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<el-select
|
||||
slot="prepend"
|
||||
v-model="item.name"
|
||||
:disabled="cannotDelete(item)"
|
||||
class="prepend"
|
||||
@change="handleProtocolChange($event, item)"
|
||||
>
|
||||
<el-option v-for="p of remainProtocols" :key="p.name" :label="p.name" :value="p.name" />
|
||||
</el-select>
|
||||
<el-button
|
||||
v-if="showSetting(item)"
|
||||
slot="append"
|
||||
icon="el-icon-setting"
|
||||
@click="onSettingClick(item)"
|
||||
/>
|
||||
</el-input>
|
||||
<div v-if="!readonly" class="input-button" style="display: flex; margin-left: 20px">
|
||||
<el-button
|
||||
:disabled="cannotDelete(item)"
|
||||
icon="el-icon-minus"
|
||||
size="mini"
|
||||
style="flex-shrink: 0;"
|
||||
type="danger"
|
||||
@click="handleDelete(index)"
|
||||
/>
|
||||
<el-button
|
||||
v-if="index === items.length - 1"
|
||||
:disabled="remainProtocols.length === 0 || !item.port"
|
||||
icon="el-icon-plus"
|
||||
size="mini"
|
||||
style="flex-shrink: 0;"
|
||||
type="primary"
|
||||
@click="handleAdd(index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ProtocolSettingDialog
|
||||
v-if="showDialog"
|
||||
:disabled="settingReadonly || readonly"
|
||||
:item="settingItem"
|
||||
:visible.sync="showDialog"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ProtocolSettingDialog from './ProtocolSettingDialog'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ProtocolSettingDialog
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: [String, Array],
|
||||
default: () => []
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
choices: {
|
||||
type: Array,
|
||||
default: () => ([])
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
settingReadonly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showSetting: {
|
||||
type: Function,
|
||||
default: (item) => true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
items: [],
|
||||
settingItem: {},
|
||||
showDialog: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
selectedProtocolNames() {
|
||||
return this.items.map(item => item.name)
|
||||
},
|
||||
remainProtocols() {
|
||||
return this.choices.filter(proto => {
|
||||
return this.selectedProtocolNames.indexOf(proto.name) === -1
|
||||
})
|
||||
},
|
||||
portPlaceholder() {
|
||||
if (this.settingReadonly) {
|
||||
return this.$t('applications.port')
|
||||
} else {
|
||||
return this.$t('assets.DefaultPort')
|
||||
}
|
||||
},
|
||||
iChoices() {
|
||||
return this.choices.map(item => {
|
||||
delete item?.id
|
||||
return item
|
||||
})
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
iChoices: {
|
||||
handler(value) {
|
||||
this.setDefaultItems(value)
|
||||
}
|
||||
},
|
||||
items: {
|
||||
handler(value) {
|
||||
this.$emit('input', value)
|
||||
},
|
||||
immediate: true,
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.setDefaultItems(this.iChoices)
|
||||
this.$log.debug('Choices: ', this.choices)
|
||||
this.$log.debug('Value: ', this.value)
|
||||
},
|
||||
methods: {
|
||||
handleDelete(index) {
|
||||
this.items = this.items.filter((value, i) => {
|
||||
return i !== index
|
||||
})
|
||||
},
|
||||
cannotDelete(item) {
|
||||
const full = this.iChoices.find(choice => {
|
||||
return choice.name === item.name
|
||||
})
|
||||
return full?.primary || full?.required
|
||||
},
|
||||
handleAdd(index) {
|
||||
this.items.push({ ...this.remainProtocols[0] })
|
||||
},
|
||||
handleProtocolChange(evt, item) {
|
||||
const selected = this.choices.find(item => item.name === evt)
|
||||
item.name = selected.name
|
||||
item.port = selected.port
|
||||
},
|
||||
setDefaultItems(choices) {
|
||||
if (this.value instanceof Array && this.value.length > 0) {
|
||||
const protocols = []
|
||||
this.value.forEach(item => {
|
||||
// 有默认值的情况下,设置为只读或者有id、有setting是平台
|
||||
if (!this.settingReadonly || (item?.id && item?.setting)) {
|
||||
protocols.push(item)
|
||||
} else {
|
||||
// 获取资产协议配置
|
||||
const assetDefaultItems = this.getAssetDefaultItems(item, choices)
|
||||
protocols.push(...assetDefaultItems)
|
||||
}
|
||||
})
|
||||
this.items = protocols
|
||||
} else {
|
||||
const defaults = choices.filter(item => (item.required || item.primary || item.default))
|
||||
this.items = defaults
|
||||
}
|
||||
},
|
||||
getAssetDefaultItems(item, choices) {
|
||||
const protocols = []
|
||||
const protocol = choices.find(i => i.name === item.name) || {}
|
||||
protocols.push({ ...protocol, ...item })
|
||||
|
||||
return protocols
|
||||
},
|
||||
onSettingClick(item) {
|
||||
this.settingItem = item
|
||||
this.showDialog = true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.el-select .el-input {
|
||||
width: 130px;
|
||||
}
|
||||
|
||||
.el-select {
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
.input-with-select {
|
||||
flex-shrink: 1;
|
||||
width: calc(100% - 80px) !important;
|
||||
}
|
||||
|
||||
.input-with-select .el-input-group__prepend {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.el-select ::v-deep .el-input__inner {
|
||||
width: 110px;
|
||||
}
|
||||
|
||||
.protocol-item {
|
||||
display: flex;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.input-button {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.input-button ::v-deep .el-button.el-button--mini {
|
||||
height: 25px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.el-input-group__append .el-button {
|
||||
font-size: 14px;
|
||||
color: #1a1a1a;
|
||||
padding: 9px 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,110 +0,0 @@
|
||||
<template>
|
||||
<div class="filter-field">
|
||||
<el-tag
|
||||
v-for="(v, k) in filterTags"
|
||||
:key="k"
|
||||
:disable-transitions="true"
|
||||
:type="tagType"
|
||||
closable
|
||||
size="small"
|
||||
@click="handleTagClick(v,k)"
|
||||
@close="handleTagClose(v)"
|
||||
>
|
||||
{{ v }}
|
||||
</el-tag>
|
||||
<el-input
|
||||
ref="SearchInput"
|
||||
v-model.trim="filterValue"
|
||||
:placeholder="this.$t('common.EnterToContinue')"
|
||||
class="search-input"
|
||||
@blur="focus = false"
|
||||
@change="handleConfirm"
|
||||
@focus="focus = true"
|
||||
@keyup.enter.native="handleConfirm"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import i18n from '@/i18n/i18n'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
tagType: {
|
||||
type: String,
|
||||
default: 'info'
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: () => i18n.t('perms.Input')
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
filterTags: this.value,
|
||||
focus: false,
|
||||
filterValue: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleTagClose(tag) {
|
||||
this.filterTags.splice(this.filterTags.indexOf(tag), 1)
|
||||
this.$emit('change', this.filterTags)
|
||||
},
|
||||
handleConfirm() {
|
||||
if (this.filterValue === '') return
|
||||
if (!this.filterTags.includes(this.filterValue)) {
|
||||
this.filterTags.push(this.filterValue)
|
||||
this.filterValue = ''
|
||||
this.$emit('change', this.filterTags)
|
||||
}
|
||||
},
|
||||
handleTagClick(v, k) {
|
||||
if (this.filterValue.length !== 0) {
|
||||
this.handleConfirm()
|
||||
}
|
||||
this.$delete(this.filterTags, k)
|
||||
this.filterValue = v
|
||||
this.$refs.SearchInput.focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-tag + .el-tag {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.filter-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 2px;
|
||||
border: 1px solid #dcdee2;
|
||||
border-radius: 1px;
|
||||
background-color: #fff;
|
||||
|
||||
&:hover {
|
||||
border-color: #C0C4CC;
|
||||
}
|
||||
}
|
||||
|
||||
.search-input > > > .el-input__inner {
|
||||
max-width: 100%;
|
||||
border: none;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.el-input > > > .el-input__inner {
|
||||
border: none !important;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.filter-field > > > .el-input__inner {
|
||||
//height: 32px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,26 +0,0 @@
|
||||
<template>
|
||||
<span>
|
||||
{{ text }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Text',
|
||||
props: {
|
||||
text: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onClick() {
|
||||
window.open(this.href)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,48 +0,0 @@
|
||||
import Link from './Link'
|
||||
import Text from './Text'
|
||||
import Select2 from './Select2'
|
||||
import TagInput from './TagInput'
|
||||
import Switcher from './Switcher'
|
||||
import UploadKey from './UploadKey'
|
||||
import JsonEditor from './JsonEditor'
|
||||
import UploadField from './UploadField'
|
||||
import UpdateToken from './UpdateToken'
|
||||
import UserPassword from './UserPassword'
|
||||
import PasswordInput from './PasswordInput'
|
||||
import WeekCronSelect from './WeekCronSelect'
|
||||
import NestedObjectSelect2 from './NestedObjectSelect2'
|
||||
import DatetimeRangePicker from './DatetimeRangePicker'
|
||||
|
||||
export default {
|
||||
Text,
|
||||
Link,
|
||||
Switcher,
|
||||
Select2,
|
||||
TagInput,
|
||||
UploadKey,
|
||||
JsonEditor,
|
||||
UpdateToken,
|
||||
UploadField,
|
||||
UserPassword,
|
||||
PasswordInput,
|
||||
WeekCronSelect,
|
||||
NestedObjectSelect2,
|
||||
DatetimeRangePicker
|
||||
}
|
||||
|
||||
export {
|
||||
Text,
|
||||
Link,
|
||||
Switcher,
|
||||
Select2,
|
||||
TagInput,
|
||||
UploadKey,
|
||||
JsonEditor,
|
||||
UpdateToken,
|
||||
UploadField,
|
||||
UserPassword,
|
||||
PasswordInput,
|
||||
WeekCronSelect,
|
||||
NestedObjectSelect2,
|
||||
DatetimeRangePicker
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user