Compare commits
678 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
47ebe55291 | ||
|
f65c5a8c56 | ||
|
a6f0791852 | ||
|
05181dbed3 | ||
|
f5dc53e5b8 | ||
|
40b452cd4b | ||
|
d322761ef7 | ||
|
ba7b78af63 | ||
|
f575291a1f | ||
|
183b8c1f7d | ||
|
12743c20fe | ||
|
b7c3cce83b | ||
|
191ce95717 | ||
|
9f7a91ab26 | ||
|
9ffa9c943d | ||
|
9a2229ab10 | ||
|
d4518002f9 | ||
|
584215b569 | ||
|
52b0a41d16 | ||
|
8a16badf25 | ||
|
1dc006426b | ||
|
1a2929c44d | ||
|
ccc45bec0f | ||
|
aa3beefe75 | ||
|
c4997458f4 | ||
|
c70153c3a7 | ||
|
6532a937bf | ||
|
7cb6bcae1e | ||
|
6b16d393c7 | ||
|
c03dc851a6 | ||
|
f7e748701d | ||
|
1bf8588984 | ||
|
54d645b711 | ||
|
c50133b3e4 | ||
|
cce88bb4b0 | ||
|
1138783267 | ||
|
48951ecd0a | ||
|
ce8ac744d6 | ||
|
c790aa3ab4 | ||
|
2d09bd9c13 | ||
|
2f3b5efea7 | ||
|
e6be756e42 | ||
|
00ceee3408 | ||
|
9afdec9ba3 | ||
|
75c31d4256 | ||
|
a09336f00b | ||
|
dbbff56395 | ||
|
0ce42334f2 | ||
|
b967de2d10 | ||
|
e369a55333 | ||
|
4b5d43de57 | ||
|
bbcc0f986e | ||
|
06e2924256 | ||
|
c986cfc6a6 | ||
|
310bb6ea39 | ||
|
626aa7b094 | ||
|
03b1139bb3 | ||
|
4b300e772d | ||
|
3beea17770 | ||
|
26f0a65ae4 | ||
|
6ccf5b261d | ||
|
afda71f135 | ||
|
5ccbfec178 | ||
|
179463e733 | ||
|
d0779d17fa | ||
|
12c7b564cd | ||
|
9a3897838d | ||
|
766609ad89 | ||
|
70bdd8f151 | ||
|
4e363176fe | ||
|
ef8ddeebe7 | ||
|
478e9519e8 | ||
|
5487d07b53 | ||
|
ade4fc72e3 | ||
|
13ed4671b0 | ||
|
c3b7303a08 | ||
|
d3080bad3c | ||
|
1f3df6921d | ||
|
6ccc010d08 | ||
|
69bde5ee29 | ||
|
52eef2d315 | ||
|
9bf621dc2c | ||
|
ad91d208b8 | ||
|
6d404c2e3e | ||
|
17d75fb329 | ||
|
97311b2b51 | ||
|
f3b0efabb4 | ||
|
7b65ab325e | ||
|
5d3221b93a | ||
|
785c63e397 | ||
|
9244eea71b | ||
|
aaa3a1e829 | ||
|
61cf798a3a | ||
|
40d1a53537 | ||
|
5bc294e405 | ||
|
2717f65280 | ||
|
f166824efb | ||
|
e235224e3c | ||
|
b417d98469 | ||
|
65ecd827ba | ||
|
dec7435e9b | ||
|
ffcb533957 | ||
|
3c1a2fe3e4 | ||
|
24d9f3758e | ||
|
d18c1fba6e | ||
|
9729725c6a | ||
|
e0c255ffa9 | ||
|
b4241c92a0 | ||
|
b75143108f | ||
|
6ae4707323 | ||
|
0a087df03f | ||
|
0679fb96e9 | ||
|
2a38401f5a | ||
|
1e5ca4ff43 | ||
|
a23d091667 | ||
|
09b10eec45 | ||
|
23c3ac44bf | ||
|
e893ea1b19 | ||
|
d80c1d7ad2 | ||
|
785b4d4a7d | ||
|
0c1b017266 | ||
|
a77aefb436 | ||
|
6ad3d167e1 | ||
|
5c8f050585 | ||
|
41d7cdf4d2 | ||
|
95506e6f4e | ||
|
f280603d00 | ||
|
72beec08d7 | ||
|
c08e4529de | ||
|
b9c701bfb0 | ||
|
c0c84de600 | ||
|
126eb03550 | ||
|
deea300620 | ||
|
ad1bb86daf | ||
|
5d3fe652b0 | ||
|
9ace26cd04 | ||
|
021b53dad4 | ||
|
3720008c2f | ||
|
e4c3a4bee1 | ||
|
1336a24044 | ||
|
16c4a08b74 | ||
|
88def811ec | ||
|
fa9bd5a926 | ||
|
578b26737a | ||
|
00d468efc6 | ||
|
2b30bd4486 | ||
|
caf7642863 | ||
|
c748352d82 | ||
|
6954c3bd7e | ||
|
5cbcbaf93d | ||
|
82ceb75d5b | ||
|
ba1064495b | ||
|
3b7c1adfb4 | ||
|
4063b148a1 | ||
|
4e441ee7a9 | ||
|
341fb410f2 | ||
|
fff6da943d | ||
|
53dcf5d0d6 | ||
|
3e9ae3e73a | ||
|
39145989c3 | ||
|
3ad8378eab | ||
|
d00544d92f | ||
|
8343d4eee3 | ||
|
ecfc3e073d | ||
|
23ee5e75b9 | ||
|
bd61775048 | ||
|
86afad1f68 | ||
|
d017e5083a | ||
|
e6f2aadc13 | ||
|
72c0fbcd11 | ||
|
ca352e984b | ||
|
26266b2d6d | ||
|
9a49636f63 | ||
|
20f1e82ffa | ||
|
244df5fa88 | ||
|
4c652959d5 | ||
|
4a42d26eef | ||
|
3401fb6ac3 | ||
|
7c7b107180 | ||
|
f1cb8569a2 | ||
|
dd611a1523 | ||
|
6b3b00c9ea | ||
|
a59e8b74ea | ||
|
41131e82ce | ||
|
5e0e64861f | ||
|
c5761fc805 | ||
|
e7f02817bf | ||
|
23f1edcc65 | ||
|
b379ff74c0 | ||
|
cbc58d4a4c | ||
|
a4be8a77aa | ||
|
3a1920da32 | ||
|
b5b8f899fe | ||
|
87d65a35bd | ||
|
819cd2884b | ||
|
24e2f3fde4 | ||
|
ed23d6d930 | ||
|
f81cae18ee | ||
|
81d25dbaed | ||
|
fb1a2dd151 | ||
|
7908fd1b96 | ||
|
062c729d98 | ||
|
0b683478cd | ||
|
8137a599d1 | ||
|
3398fdf499 | ||
|
ebe460934d | ||
|
33602dcff5 | ||
|
5327fc3445 | ||
|
fa737e75c3 | ||
|
1a774490ac | ||
|
5033c539de | ||
|
104d163db8 | ||
|
b29f498748 | ||
|
5358fb41b2 | ||
|
b7a6484579 | ||
|
df3dc7cb1b | ||
|
4dfbf7cd62 | ||
|
0209ba3778 | ||
|
95eeda2ebe | ||
|
0aeac5b0df | ||
|
b0e7748ad0 | ||
|
9ccaaffa4a | ||
|
58981a7301 | ||
|
29dab8ad06 | ||
|
f0713c5ac8 | ||
|
e4750d122e | ||
|
2da89ca1ef | ||
|
5f6f22bff4 | ||
|
9d0990b58b | ||
|
51027e9ac2 | ||
|
d276b3122e | ||
|
e727391fed | ||
|
46f4cb5be0 | ||
|
59d948fb54 | ||
|
11ff531730 | ||
|
f89c18db51 | ||
|
a889fe503a | ||
|
55960aeb54 | ||
|
0a13ca82c6 | ||
|
6955714951 | ||
|
c92a3a5f31 | ||
|
a903738cdc | ||
|
6b05ab1acc | ||
|
dc454f081d | ||
|
618c68423a | ||
|
123c35c890 | ||
|
3b4aa14bad | ||
|
2b639dd11f | ||
|
3b95fb9bb5 | ||
|
657d57a742 | ||
|
c0fc534958 | ||
|
373dda6f41 | ||
|
bd31043608 | ||
|
3da43b6cef | ||
|
03ec2e7d01 | ||
|
7d9ef229c2 | ||
|
b4326722e6 | ||
|
69fb7f88ae | ||
|
d2b7161e39 | ||
|
d811f4d83f | ||
|
644cd98af9 | ||
|
f07b87e568 | ||
|
e06bf67b5e | ||
|
5a2581d569 | ||
|
f317e24ae8 | ||
|
7d1a05e487 | ||
|
0966d104a7 | ||
|
11dc7a6013 | ||
|
00b022d620 | ||
|
e6ffcf9ebd | ||
|
e6eb1b8247 | ||
|
780dbbc280 | ||
|
0d7101c9f8 | ||
|
5008fe0491 | ||
|
bb72881f3b | ||
|
2f7896b3db | ||
|
cf8ed6cda6 | ||
|
9e62780d50 | ||
|
a97d3d6198 | ||
|
9dfea3b478 | ||
|
c252ef2d08 | ||
|
10406942a0 | ||
|
33e58a658b | ||
|
bb7fd13cb2 | ||
|
7f5e5a0921 | ||
|
9793734655 | ||
|
8f74be216e | ||
|
a7c3a0a072 | ||
|
0437da5797 | ||
|
f0ac4d10ff | ||
|
85dcb997fb | ||
|
a4729a3c1d | ||
|
6cda354c21 | ||
|
702d8d65f0 | ||
|
9ce5a96232 | ||
|
79c9abe383 | ||
|
e9d2365766 | ||
|
614766563e | ||
|
472642c958 | ||
|
5ad73366ad | ||
|
2c12f5fc6f | ||
|
b08fa206e4 | ||
|
dc569c32a5 | ||
|
ec55dadc57 | ||
|
24af71c1fc | ||
|
cf45f608d4 | ||
|
53943f1244 | ||
|
6b2d4902af | ||
|
2b0261f055 | ||
|
b63ca2a059 | ||
|
4ebaf9c102 | ||
|
df5c62d98e | ||
|
39e38b10cd | ||
|
02f332606e | ||
|
f30b8ecd3a | ||
|
7a170ab397 | ||
|
34204ec4c6 | ||
|
f867ccbf94 | ||
|
60df081e49 | ||
|
2979d2056a | ||
|
06af1f656d | ||
|
fe8582447e | ||
|
d96d529aa9 | ||
|
aa000cabe2 | ||
|
27affe02a8 | ||
|
f010b9625e | ||
|
ef1d0c34cf | ||
|
7474a92377 | ||
|
26c3404f28 | ||
|
d74f201710 | ||
|
5b3fb7ee32 | ||
|
0f404fe9bf | ||
|
498feee0a2 | ||
|
2bd1a45ae6 | ||
|
d4279600b5 | ||
|
c52fa3bc80 | ||
|
849af21855 | ||
|
861564c0da | ||
|
9511e33736 | ||
|
b6ef49f139 | ||
|
9141c06530 | ||
|
9bc0ab6009 | ||
|
67081ef005 | ||
|
4111ac8d31 | ||
|
7b593ce1bc | ||
|
e1b81561c9 | ||
|
c53e5ecd30 | ||
|
6f6da3c228 | ||
|
95d85234c6 | ||
|
996151f0f4 | ||
|
c2ba819076 | ||
|
77d8a21bde | ||
|
0f23feda2f | ||
|
21bb741a02 | ||
|
6c7bc690cc | ||
|
56dab953e7 | ||
|
cdf7506b5d | ||
|
f3ca1fbea3 | ||
|
6c9c987979 | ||
|
4e48dd2b37 | ||
|
1a8d54d4e2 | ||
|
be0712f202 | ||
|
ff8d4bd51b | ||
|
af148b7c8f | ||
|
5e79aab93d | ||
|
99022bdabb | ||
|
19c6009c64 | ||
|
b3ef1aa5c1 | ||
|
2992bc2fae | ||
|
5c2cb9073f | ||
|
ee50ea1cf3 | ||
|
11259b4067 | ||
|
a23bdab10e | ||
|
5c8e93e194 | ||
|
f0bf740d70 | ||
|
98025ae47c | ||
|
f65d81bf46 | ||
|
33f9f190e9 | ||
|
8646f4693a | ||
|
d0575331d5 | ||
|
885c346407 | ||
|
b887de2ab8 | ||
|
51c42f90be | ||
|
a938477d85 | ||
|
f965ad3bf3 | ||
|
5fe6676d83 | ||
|
41ad610c00 | ||
|
7b8e120974 | ||
|
1ee8ed7c4f | ||
|
1da629b877 | ||
|
5279d96c84 | ||
|
718335231c | ||
|
c361997591 | ||
|
3e2943b49e | ||
|
75122f7a40 | ||
|
6674510697 | ||
|
4a0233df24 | ||
|
9afef06c54 | ||
|
37b3c1aa01 | ||
|
e597c2aee9 | ||
|
b61c14ba07 | ||
|
3a760c3a80 | ||
|
92b54f045f | ||
|
3e6ebec9af | ||
|
4a92d95d2f | ||
|
0806f2ed73 | ||
|
ef052c4ab9 | ||
|
3c315ff397 | ||
|
fc4bb7ffb4 | ||
|
e1c797b6a1 | ||
|
faeb9a04f9 | ||
|
c89ebf6518 | ||
|
a21afbe909 | ||
|
6aef26b82c | ||
|
ee0b74bec7 | ||
|
32e073f3fd | ||
|
67d64abf42 | ||
|
093065551b | ||
|
b84d5d717e | ||
|
65664ae8f9 | ||
|
7f1d796fd1 | ||
|
6c45ca7cb0 | ||
|
64f891df31 | ||
|
beb2f01ec9 | ||
|
4b133c56fd | ||
|
a1a6b11072 | ||
|
379cabbfca | ||
|
f1350a2940 | ||
|
86fbd62048 | ||
|
4d783235b7 | ||
|
34c846581e | ||
|
7c7b360ffc | ||
|
4b8c5b8495 | ||
|
c523ffaf65 | ||
|
315aa40b72 | ||
|
7ef23bd779 | ||
|
91555ffa64 | ||
|
2699c263d9 | ||
|
8f3421bc29 | ||
|
17612105a5 | ||
|
21d445458d | ||
|
3b73971bf8 | ||
|
8856115e7c | ||
|
fb371a0d46 | ||
|
6aa02eed73 | ||
|
e5d4015f7e | ||
|
1b16a0b9c0 | ||
|
3d4c4d6b56 | ||
|
a50f6d4fe4 | ||
|
07412169b3 | ||
|
e577dce4ee | ||
|
c394c41c3c | ||
|
2554a7d1be | ||
|
2c70cb49e1 | ||
|
5e5ec3f887 | ||
|
5132481fef | ||
|
f75756b71a | ||
|
4440654a25 | ||
|
072daeea35 | ||
|
1ccbafce08 | ||
|
e785d1a2f6 | ||
|
2d59757ba7 | ||
|
a94575df0d | ||
|
36a451686e | ||
|
c273ecdd29 | ||
|
2154834f44 | ||
|
c020d2fce8 | ||
|
02b6e1d66b | ||
|
be72a4579b | ||
|
3c660e0969 | ||
|
b44d9d7e47 | ||
|
b31bbd3d63 | ||
|
be32b9b043 | ||
|
6c200f12a7 | ||
|
92dc81ec53 | ||
|
4754bb59f4 | ||
|
9e180af9f4 | ||
|
67779ccc67 | ||
|
8c47105fbc | ||
|
30ccb87499 | ||
|
0ac14fa318 | ||
|
819b994c2d | ||
|
6047c59f51 | ||
|
c869827ffd | ||
|
bdd2adcfc2 | ||
|
e0e2ca6294 | ||
|
2a5091c51a | ||
|
788dd684ab | ||
|
ee1b068b62 | ||
|
b08ed43105 | ||
|
41d810642b | ||
|
f033af2f58 | ||
|
cbe579fdb4 | ||
|
4e1f25b389 | ||
|
a27b9a37df | ||
|
ebc8a1e254 | ||
|
e51fa08208 | ||
|
55c2c557fc | ||
|
5aa36daaac | ||
|
ba519a2c43 | ||
|
9dbea9f403 | ||
|
d7bbc3ccf8 | ||
|
501d86341a | ||
|
59915f5f11 | ||
|
b8fed4e655 | ||
|
297270063c | ||
|
ccce5c830a | ||
|
31d24b19a8 | ||
|
89a2985ee4 | ||
|
d42513c4a4 | ||
|
55f74a4eba | ||
|
17dd2e2a93 | ||
|
a779b37c44 | ||
|
cf3f961b0d | ||
|
4f7eddf906 | ||
|
8b729e63cd | ||
|
bc91b33beb | ||
|
d2985a7564 | ||
|
2ef4270ae7 | ||
|
11c6f9001b | ||
|
ab26033cea | ||
|
ca0f332650 | ||
|
43be749c58 | ||
|
788fefd9a1 | ||
|
36a24d4a68 | ||
|
c2066b53f1 | ||
|
69d3d3b047 | ||
|
c236851d2f | ||
|
249ba7ad5c | ||
|
d8399f8723 | ||
|
6e94c72031 | ||
|
6e9871dfd8 | ||
|
e5ccb9a499 | ||
|
ad841f9732 | ||
|
47dbe5ba18 | ||
|
737b29f7d6 | ||
|
8c703fb6d9 | ||
|
afe365bd38 | ||
|
5fa187bf46 | ||
|
44f051dac9 | ||
|
94cdb42477 | ||
|
cca8fccb55 | ||
|
eea379537f | ||
|
2c74d107dd | ||
|
8f6b54cd12 | ||
|
ae5636b702 | ||
|
9afb5179f7 | ||
|
bba3a2b931 | ||
|
15f264681b | ||
|
b099bc212b | ||
|
3fd4a9fed6 | ||
|
ad0d32652e | ||
|
8225159d5a | ||
|
4a3db88ae6 | ||
|
e49ede0b3a | ||
|
6fe6cf4600 | ||
|
4fb3138acd | ||
|
d1dc6c2fab | ||
|
9f8f3a29b7 | ||
|
063ed40440 | ||
|
96176c706a | ||
|
81faab5a20 | ||
|
a39fd08e0d | ||
|
cd954775bc | ||
|
5f550208ce | ||
|
88c9fb5ae3 | ||
|
2aa420ec07 | ||
|
6e51e9ef4c | ||
|
2f8856772b | ||
|
c74fee8aa9 | ||
|
e390b1ed7c | ||
|
ce5884b2b9 | ||
|
7700afdf1c | ||
|
771439e008 | ||
|
7918b81f33 | ||
|
9547fce484 | ||
|
d25c3760a1 | ||
|
6df845d662 | ||
|
0ac5b9cfe4 | ||
|
f74fd2ea8f | ||
|
1e4033d0a6 | ||
|
6ce139bec3 | ||
|
d763edc6bf | ||
|
11fe5ca457 | ||
|
57b1b5fb67 | ||
|
349ddbd98a | ||
|
700c411ec3 | ||
|
4d36d448a7 | ||
|
0f1cb01f05 | ||
|
7e06713b87 | ||
|
41ee3b7f87 | ||
|
36910d0052 | ||
|
52578d78d6 | ||
|
9b563f7b57 | ||
|
96187bcd47 | ||
|
1ba3f85f7e | ||
|
3071983d6b | ||
|
836892d909 | ||
|
a24ba8f7cd | ||
|
d0d68c613b | ||
|
9b976675c0 | ||
|
2200b122f0 | ||
|
190e82efe9 | ||
|
1dae48c583 | ||
|
5c25433e03 | ||
|
f8e94b7c70 | ||
|
c64610e9ea | ||
|
3f49586c9f | ||
|
91b886d632 | ||
|
7740031bd8 | ||
|
76a41a8e82 | ||
|
6843eb57c4 | ||
|
20caae8263 | ||
|
5e61da038f | ||
|
348a34d862 | ||
|
2471af867a | ||
|
775f65ba7b | ||
|
ebd0b1dc2e | ||
|
14b5119d94 | ||
|
45167225c9 | ||
|
e576d41497 | ||
|
4c33d6dee9 | ||
|
74de3ec0b3 | ||
|
5e5ae296e0 | ||
|
e81606aa81 | ||
|
dc6da9ba2a | ||
|
d40e69aa30 | ||
|
4cecdb10fb | ||
|
ddf02213db | ||
|
28dea81036 | ||
|
9756f70044 | ||
|
88cea199c1 | ||
|
684c1ab924 | ||
|
cb01b577a5 | ||
|
694abf78b3 | ||
|
7e38dd8fab | ||
|
7afefe1070 | ||
|
31a4cb62d8 | ||
|
79a2fd32c1 | ||
|
ef09497d67 | ||
|
51c6d50b38 | ||
|
edb74d5790 | ||
|
9b7d6d8f12 | ||
|
61e178530f | ||
|
3632f5f4a6 | ||
|
cd11b002ee | ||
|
883d7776e1 | ||
|
695e9f7546 | ||
|
c3cbdc606f | ||
|
5cb5650cde | ||
|
8d95092b52 | ||
|
458b1d144b | ||
|
ec0f3d5a9f | ||
|
8c21df189f | ||
|
a882a62d1c | ||
|
d7d2e127d3 | ||
|
82f4bb5b65 | ||
|
96ba04dc31 | ||
|
0263842b70 | ||
|
c0443a4e3f | ||
|
10b7f9dfcb | ||
|
c2cca38f1c | ||
|
742cd4ead1 | ||
|
5e0ea75fae | ||
|
7715d52e33 | ||
|
bd72abba10 | ||
|
8dc437bc81 | ||
|
5d299bd71a | ||
|
3d3453e257 | ||
|
c196c75985 | ||
|
7443f96813 | ||
|
ea6c5c8566 | ||
|
c4caa464d3 | ||
|
648c1cad68 | ||
|
095d3b2cfb | ||
|
d8a62fc885 | ||
|
ccf96b17bc | ||
|
ae99d7f909 |
11
README.md
@@ -27,7 +27,6 @@
|
||||
|
||||
<img src=docs/images/dashboard.png />
|
||||
|
||||
[查看更多展示](docs/screenshot.md)
|
||||
|
||||
### 相关文章
|
||||
|
||||
@@ -51,14 +50,12 @@
|
||||
|
||||
### 主要功能
|
||||
|
||||
- 模型属性支持索引、多值、默认排序、字体颜色,支持计算属性
|
||||
- 支持自动发现、定时巡检、文件导入
|
||||
- 自定义模型和模型关系,模型属性支持下拉列表、字体颜色、计算属性等高级特性
|
||||
- 支持计算机、网络设备、存储设备、数据库、中间件、公有云资源等自动发现
|
||||
- 支持资源、层级、关系视图展示
|
||||
- 支持模型间关系配置和展示
|
||||
- 细粒度访问控制,完备的操作日志
|
||||
- 支持跨模型搜索
|
||||
|
||||
|
||||
- 通用的资源搜索和关系搜索
|
||||
- 支持IP地址管理(IPAM), 数据中心基础设施管理(DCIM)
|
||||
|
||||
|
||||
|
||||
|
@@ -23,6 +23,7 @@ from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2
|
||||
from api.lib.cmdb.const import ResourceTypeEnum
|
||||
from api.lib.cmdb.const import RoleEnum
|
||||
from api.lib.cmdb.const import ValueTypeEnum
|
||||
from api.lib.cmdb.dcim.rack import RackManager
|
||||
from api.lib.exception import AbortException
|
||||
from api.lib.perm.acl.acl import ACLManager
|
||||
from api.lib.perm.acl.acl import UserCache
|
||||
@@ -195,7 +196,7 @@ def cmdb_counter():
|
||||
today = datetime.date.today()
|
||||
while True:
|
||||
try:
|
||||
db.session.remove()
|
||||
db.session.commit()
|
||||
|
||||
CMDBCounterCache.reset()
|
||||
|
||||
@@ -209,6 +210,8 @@ def cmdb_counter():
|
||||
|
||||
CMDBCounterCache.flush_sub_counter()
|
||||
|
||||
RackManager().check_u_slot()
|
||||
|
||||
i += 1
|
||||
except:
|
||||
import traceback
|
||||
|
@@ -512,7 +512,7 @@ class CMDBCounterCache(object):
|
||||
result[i.type_id]['rule_count'] = len(adts) + AutoDiscoveryCITypeRelation.get_by(
|
||||
ad_type_id=i.type_id, only_query=True).count()
|
||||
result[i.type_id]['exec_target_count'] = len(
|
||||
set([i.oneagent_id for adt in adts for i in db.session.query(
|
||||
set([j.oneagent_id for adt in adts for j in db.session.query(
|
||||
AutoDiscoveryRuleSyncHistory.oneagent_id).filter(
|
||||
AutoDiscoveryRuleSyncHistory.adt_id == adt.id)]))
|
||||
|
||||
|
@@ -357,6 +357,7 @@ class CIManager(object):
|
||||
is_auto_discovery=False,
|
||||
_is_admin=False,
|
||||
ticket_id=None,
|
||||
_sync=False,
|
||||
**ci_dict):
|
||||
"""
|
||||
add ci
|
||||
@@ -366,6 +367,7 @@ class CIManager(object):
|
||||
:param is_auto_discovery: default is False
|
||||
:param _is_admin: default is False
|
||||
:param ticket_id:
|
||||
:param _sync:
|
||||
:param ci_dict:
|
||||
:return:
|
||||
"""
|
||||
@@ -496,10 +498,16 @@ class CIManager(object):
|
||||
record_id = cls.save_password(ci.id, attr_id, password_dict[attr_id], record_id, ci_type.id)
|
||||
|
||||
if record_id or has_dynamic: # has changed
|
||||
ci_cache.apply_async(args=(ci.id, operate_type, record_id), queue=CMDB_QUEUE)
|
||||
if not _sync:
|
||||
ci_cache.apply_async(args=(ci.id, operate_type, record_id), queue=CMDB_QUEUE)
|
||||
else:
|
||||
ci_cache(ci.id, operate_type, record_id)
|
||||
|
||||
if ref_ci_dict: # add relations
|
||||
ci_relation_add.apply_async(args=(ref_ci_dict, ci.id, current_user.uid), queue=CMDB_QUEUE)
|
||||
if not _sync:
|
||||
ci_relation_add.apply_async(args=(ref_ci_dict, ci.id, current_user.uid), queue=CMDB_QUEUE)
|
||||
else:
|
||||
ci_relation_add(ref_ci_dict, ci.id, current_user.uid)
|
||||
|
||||
return ci.id
|
||||
|
||||
@@ -1281,10 +1289,10 @@ class CIRelationManager(object):
|
||||
return existed.id
|
||||
|
||||
@staticmethod
|
||||
def delete(cr_id, apply_async=True):
|
||||
def delete(cr_id, apply_async=True, valid=True):
|
||||
cr = CIRelation.get_by_id(cr_id) or abort(404, ErrFormat.relation_not_found.format("id={}".format(cr_id)))
|
||||
|
||||
if current_app.config.get('USE_ACL') and current_user.username != 'worker':
|
||||
if current_app.config.get('USE_ACL') and current_user.username != 'worker' and valid:
|
||||
resource_name = CITypeRelationManager.acl_resource_name(cr.first_ci.ci_type.name, cr.second_ci.ci_type.name)
|
||||
if not ACLManager().has_permission(
|
||||
resource_name,
|
||||
@@ -1323,7 +1331,7 @@ class CIRelationManager(object):
|
||||
return cr
|
||||
|
||||
@classmethod
|
||||
def delete_3(cls, first_ci_id, second_ci_id, apply_async=True):
|
||||
def delete_3(cls, first_ci_id, second_ci_id, apply_async=True, valid=True):
|
||||
cr = CIRelation.get_by(first_ci_id=first_ci_id,
|
||||
second_ci_id=second_ci_id,
|
||||
to_dict=False,
|
||||
@@ -1333,7 +1341,7 @@ class CIRelationManager(object):
|
||||
# ci_relation_delete.apply_async(args=(first_ci_id, second_ci_id, cr.ancestor_ids), queue=CMDB_QUEUE)
|
||||
# delete_id_filter.apply_async(args=(second_ci_id,), queue=CMDB_QUEUE)
|
||||
|
||||
cls.delete(cr.id, apply_async=apply_async)
|
||||
cls.delete(cr.id, apply_async=apply_async, valid=valid)
|
||||
|
||||
return cr
|
||||
|
||||
@@ -1526,7 +1534,8 @@ class CITriggerManager(object):
|
||||
ci_dict.update(attr_dict)
|
||||
|
||||
@classmethod
|
||||
def _exec_webhook(cls, operate_type, webhook, ci_dict, trigger_id, trigger_name, record_id, ci_id=None, app=None):
|
||||
def _exec_webhook(cls, operate_type, webhook, ci_dict, trigger_id, trigger_name, record_id,
|
||||
ci_id=None, app=None, record_history=True):
|
||||
app = app or current_app
|
||||
|
||||
with app.app_context():
|
||||
@@ -1544,19 +1553,20 @@ class CITriggerManager(object):
|
||||
current_app.logger.warning("exec webhook failed: {}".format(e))
|
||||
response = e
|
||||
is_ok = False
|
||||
|
||||
CITriggerHistoryManager.add(operate_type,
|
||||
record_id,
|
||||
ci_dict.get('_id'),
|
||||
trigger_id,
|
||||
trigger_name,
|
||||
is_ok=is_ok,
|
||||
webhook=response)
|
||||
if record_history:
|
||||
CITriggerHistoryManager.add(operate_type,
|
||||
record_id,
|
||||
ci_dict.get('_id'),
|
||||
trigger_id,
|
||||
trigger_name,
|
||||
is_ok=is_ok,
|
||||
webhook=response)
|
||||
|
||||
return is_ok
|
||||
|
||||
@classmethod
|
||||
def _exec_notify(cls, operate_type, notify, ci_dict, trigger_id, trigger_name, record_id, ci_id=None, app=None):
|
||||
def _exec_notify(cls, operate_type, notify, ci_dict, trigger_id, trigger_name, record_id,
|
||||
ci_id=None, app=None, record_history=True):
|
||||
app = app or current_app
|
||||
|
||||
with app.app_context():
|
||||
@@ -1580,13 +1590,14 @@ class CITriggerManager(object):
|
||||
response = "{}\n{}".format(response, e)
|
||||
is_ok = False
|
||||
|
||||
CITriggerHistoryManager.add(operate_type,
|
||||
record_id,
|
||||
ci_dict.get('_id'),
|
||||
trigger_id,
|
||||
trigger_name,
|
||||
is_ok=is_ok,
|
||||
notify=response.strip())
|
||||
if record_history:
|
||||
CITriggerHistoryManager.add(operate_type,
|
||||
record_id,
|
||||
ci_dict.get('_id'),
|
||||
trigger_id,
|
||||
trigger_name,
|
||||
is_ok=is_ok,
|
||||
notify=response.strip())
|
||||
|
||||
return is_ok
|
||||
|
||||
@@ -1663,25 +1674,47 @@ class CITriggerManager(object):
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def trigger_notify(cls, trigger, ci):
|
||||
def trigger_notify(cls, trigger, ci, only_test=False):
|
||||
"""
|
||||
only for date attribute
|
||||
:param trigger:
|
||||
:param ci:
|
||||
:param only_test:
|
||||
:return:
|
||||
"""
|
||||
if (trigger.option.get('notifies', {}).get('notify_at') == datetime.datetime.now().strftime("%H:%M") or
|
||||
not trigger.option.get('notifies', {}).get('notify_at')):
|
||||
not trigger.option.get('notifies', {}).get('notify_at')) or only_test:
|
||||
|
||||
if trigger.option.get('webhooks'):
|
||||
threading.Thread(target=cls._exec_webhook, args=(
|
||||
None, trigger.option['webhooks'], None, trigger.id, trigger.option.get('name'), None, ci.ci_id,
|
||||
current_app._get_current_object())).start()
|
||||
threading.Thread(
|
||||
target=cls._exec_webhook,
|
||||
args=(None, trigger.option['webhooks'], None, trigger.id,
|
||||
trigger.option.get('name'), None,
|
||||
ci and ci.ci_id,
|
||||
current_app._get_current_object(), not only_test)).start()
|
||||
elif trigger.option.get('notifies'):
|
||||
threading.Thread(target=cls._exec_notify, args=(
|
||||
None, trigger.option['notifies'], None, trigger.id, trigger.option.get('name'), None, ci.ci_id,
|
||||
current_app._get_current_object())).start()
|
||||
None, trigger.option['notifies'], None, trigger.id,
|
||||
trigger.option.get('name'), None,
|
||||
ci and ci.ci_id,
|
||||
current_app._get_current_object(), not only_test)).start()
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def trigger_notify_test(cls, type_id, trigger_id):
|
||||
trigger = CITypeTrigger.get_by_id(trigger_id) or abort(
|
||||
404, ErrFormat.ci_type_trigger_not_found.format(trigger_id))
|
||||
|
||||
ci_type = CITypeCache.get(type_id) or abort(404, ErrFormat.ci_type_not_found.format(type_id))
|
||||
attr = AttributeCache.get(ci_type.unique_id)
|
||||
value_table = TableMap(attr=attr).table
|
||||
if not value_table:
|
||||
return
|
||||
|
||||
value = value_table.get_by(attr_id=attr.id, only_query=True).join(
|
||||
CI, value_table.ci_id == CI.id).filter(CI.type_id == type_id).first()
|
||||
|
||||
cls.trigger_notify(trigger, value, only_test=True)
|
||||
|
@@ -421,7 +421,10 @@ class CITypeGroupManager(object):
|
||||
group_types = set()
|
||||
for group in groups:
|
||||
for t in sorted(CITypeGroupItem.get_by(group_id=group['id']), key=lambda x: x['order'] or 0):
|
||||
ci_type = CITypeCache.get(t['type_id']).to_dict()
|
||||
ci_type = CITypeCache.get(t['type_id'])
|
||||
if ci_type is None:
|
||||
continue
|
||||
ci_type = ci_type.to_dict()
|
||||
if type_ids is not None and ci_type['id'] not in type_ids:
|
||||
continue
|
||||
if resources is None or (ci_type and ci_type['name'] in resources):
|
||||
@@ -879,6 +882,8 @@ class CITypeRelationManager(object):
|
||||
def _wrap_relation_type_dict(type_id, relation_inst):
|
||||
ci_type_dict = CITypeCache.get(type_id).to_dict()
|
||||
ci_type_dict["ctr_id"] = relation_inst.id
|
||||
show_key = AttributeCache.get(ci_type_dict.get('show_id') or ci_type_dict['unique_id'])
|
||||
ci_type_dict["show_key"] = show_key and show_key.name
|
||||
ci_type_dict["attributes"] = CITypeAttributeManager.get_attributes_by_type_id(ci_type_dict["id"])
|
||||
attr_filter = CIFilterPermsCRUD.get_attr_filter(type_id)
|
||||
if attr_filter:
|
||||
@@ -1551,7 +1556,10 @@ class CITypeTemplateManager(object):
|
||||
if existed is None:
|
||||
_group['type_id'] = type_id_map.get(_group['type_id'], _group['type_id'])
|
||||
|
||||
existed = CITypeAttributeGroup.create(flush=True, **_group)
|
||||
try:
|
||||
existed = CITypeAttributeGroup.create(flush=True, **_group)
|
||||
except:
|
||||
continue
|
||||
|
||||
for order, attr in enumerate(group['attributes'] or []):
|
||||
item_existed = CITypeAttributeGroupItem.get_by(group_id=existed.id,
|
||||
|
@@ -123,6 +123,11 @@ class BuiltinModelEnum(BaseEnum):
|
||||
IPAM_ADDRESS = "ipam_address"
|
||||
IPAM_SCOPE = "ipam_scope"
|
||||
|
||||
DCIM_REGION = "dcim_region"
|
||||
DCIM_IDC = "dcim_idc"
|
||||
DCIM_SERVER_ROOM = "dcim_server_room"
|
||||
DCIM_RACK = "dcim_rack"
|
||||
|
||||
|
||||
BUILTIN_ATTRIBUTES = {
|
||||
"_updated_at": _l("Update Time"),
|
||||
|
1
cmdb-api/api/lib/cmdb/dcim/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# -*- coding:utf-8 -*-
|
33
cmdb-api/api/lib/cmdb/dcim/base.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
from api.lib.cmdb.ci import CIManager
|
||||
from api.lib.cmdb.ci import CIRelationManager
|
||||
from api.lib.cmdb.const import ExistPolicy
|
||||
|
||||
|
||||
class DCIMBase(object):
|
||||
def __init__(self):
|
||||
self.type_id = None
|
||||
|
||||
@staticmethod
|
||||
def add_relation(parent_id, child_id):
|
||||
if not parent_id or not child_id:
|
||||
return
|
||||
|
||||
CIRelationManager().add(parent_id, child_id, valid=False, apply_async=False)
|
||||
|
||||
def add(self, parent_id, **kwargs):
|
||||
ci_id = CIManager().add(self.type_id, exist_policy=ExistPolicy.REJECT, **kwargs)
|
||||
|
||||
if parent_id:
|
||||
self.add_relation(parent_id, ci_id)
|
||||
|
||||
return ci_id
|
||||
|
||||
@classmethod
|
||||
def update(cls, _id, **kwargs):
|
||||
CIManager().update(_id, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def delete(cls, _id):
|
||||
CIManager().delete(_id)
|
17
cmdb-api/api/lib/cmdb/dcim/const.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
|
||||
from api.lib.utils import BaseEnum
|
||||
|
||||
|
||||
class RackBuiltinAttributes(BaseEnum):
|
||||
U_COUNT = 'u_count'
|
||||
U_START = 'u_start'
|
||||
FREE_U_COUNT = 'free_u_count'
|
||||
U_SLOT_ABNORMAL = 'u_slot_abnormal'
|
||||
|
||||
|
||||
class OperateTypeEnum(BaseEnum):
|
||||
ADD_DEVICE = "0"
|
||||
REMOVE_DEVICE = "1"
|
||||
MOVE_DEVICE = "2"
|
40
cmdb-api/api/lib/cmdb/dcim/history.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from flask_login import current_user
|
||||
|
||||
from api.lib.cmdb.cache import AttributeCache
|
||||
from api.lib.cmdb.cache import CITypeCache
|
||||
from api.lib.cmdb.ci import CIManager
|
||||
from api.lib.mixin import DBMixin
|
||||
from api.models.cmdb import DCIMOperationHistory
|
||||
|
||||
|
||||
class OperateHistoryManager(DBMixin):
|
||||
cls = DCIMOperationHistory
|
||||
|
||||
@classmethod
|
||||
def search(cls, page, page_size, fl=None, only_query=False, reverse=False, count_query=False,
|
||||
last_size=None, **kwargs):
|
||||
numfound, result = super(OperateHistoryManager, cls).search(page, page_size, fl, only_query, reverse,
|
||||
count_query, last_size, **kwargs)
|
||||
|
||||
ci_ids = [i['ci_id'] for i in result]
|
||||
id2ci = {i['_id']: i for i in (CIManager.get_cis_by_ids(ci_ids) or []) if i}
|
||||
type2show_key = dict()
|
||||
for i in id2ci.values():
|
||||
if i.get('_type') not in type2show_key:
|
||||
ci_type = CITypeCache.get(i.get('_type'))
|
||||
if ci_type:
|
||||
show_key = AttributeCache.get(ci_type.show_id or ci_type.unique_id)
|
||||
type2show_key[i['_type']] = show_key and show_key.name
|
||||
|
||||
return numfound, result, id2ci, type2show_key
|
||||
|
||||
def _can_add(self, **kwargs):
|
||||
kwargs['uid'] = current_user.uid
|
||||
|
||||
return kwargs
|
||||
|
||||
def _can_update(self, **kwargs):
|
||||
pass
|
||||
|
||||
def _can_delete(self, **kwargs):
|
||||
pass
|
19
cmdb-api/api/lib/cmdb/dcim/idc.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
|
||||
from flask import abort
|
||||
|
||||
from api.lib.cmdb.cache import CITypeCache
|
||||
from api.lib.cmdb.const import BuiltinModelEnum
|
||||
from api.lib.cmdb.dcim.base import DCIMBase
|
||||
from api.lib.cmdb.resp_format import ErrFormat
|
||||
|
||||
|
||||
class IDCManager(DCIMBase):
|
||||
def __init__(self):
|
||||
super(IDCManager, self).__init__()
|
||||
|
||||
self.ci_type = CITypeCache.get(BuiltinModelEnum.DCIM_IDC) or abort(
|
||||
404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_IDC))
|
||||
|
||||
self.type_id = self.ci_type.id
|
182
cmdb-api/api/lib/cmdb/dcim/rack.py
Normal file
@@ -0,0 +1,182 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
import itertools
|
||||
import redis_lock
|
||||
from flask import abort
|
||||
|
||||
from api.extensions import rd
|
||||
from api.lib.cmdb.cache import CITypeCache
|
||||
from api.lib.cmdb.ci import CIManager
|
||||
from api.lib.cmdb.ci import CIRelationManager
|
||||
from api.lib.cmdb.const import BuiltinModelEnum
|
||||
from api.lib.cmdb.dcim.base import DCIMBase
|
||||
from api.lib.cmdb.dcim.const import OperateTypeEnum
|
||||
from api.lib.cmdb.dcim.const import RackBuiltinAttributes
|
||||
from api.lib.cmdb.dcim.history import OperateHistoryManager
|
||||
from api.lib.cmdb.resp_format import ErrFormat
|
||||
from api.lib.cmdb.search.ci.db.search import Search as SearchFromDB
|
||||
from api.lib.cmdb.search.ci_relation.search import Search as RelationSearch
|
||||
|
||||
|
||||
class RackManager(DCIMBase):
|
||||
def __init__(self):
|
||||
super(RackManager, self).__init__()
|
||||
|
||||
self.ci_type = CITypeCache.get(BuiltinModelEnum.DCIM_RACK) or abort(
|
||||
404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_RACK))
|
||||
|
||||
self.type_id = self.ci_type.id
|
||||
|
||||
@classmethod
|
||||
def update(cls, _id, **kwargs):
|
||||
if RackBuiltinAttributes.U_COUNT in kwargs:
|
||||
devices, _, _, _, _, _ = RelationSearch(
|
||||
[_id],
|
||||
level=[1],
|
||||
fl=[RackBuiltinAttributes.U_COUNT, RackBuiltinAttributes.U_START],
|
||||
count=1000000).search()
|
||||
for device in devices:
|
||||
u_start = device.get(RackBuiltinAttributes.U_START)
|
||||
u_count = device.get(RackBuiltinAttributes.U_COUNT) or 2
|
||||
if u_start and u_start + u_count - 1 > kwargs[RackBuiltinAttributes.U_COUNT]:
|
||||
return abort(400, ErrFormat.dcim_rack_u_count_invalid)
|
||||
|
||||
CIManager().update(_id, _sync=True, **kwargs)
|
||||
|
||||
if RackBuiltinAttributes.U_COUNT in kwargs:
|
||||
payload = {RackBuiltinAttributes.FREE_U_COUNT: cls.calc_u_free_count(_id)}
|
||||
|
||||
CIManager().update(_id, _sync=True, **payload)
|
||||
|
||||
def delete(self, _id):
|
||||
super(RackManager, self).delete(_id)
|
||||
|
||||
payload = {RackBuiltinAttributes.U_START: None}
|
||||
_, _, second_cis = CIRelationManager.get_second_cis(_id, per_page='all')
|
||||
for ci in second_cis:
|
||||
CIManager().update(ci['_id'], **payload)
|
||||
|
||||
@staticmethod
|
||||
def calc_u_free_count(rack_id, device_id=None, u_start=None, u_count=None):
|
||||
rack = CIManager.get_ci_by_id(rack_id, need_children=False)
|
||||
if not rack.get(RackBuiltinAttributes.U_COUNT):
|
||||
return 0
|
||||
|
||||
if device_id is not None and u_count is None:
|
||||
ci = CIManager().get_ci_by_id(device_id, need_children=False)
|
||||
u_count = ci.get(RackBuiltinAttributes.U_COUNT) or 2
|
||||
|
||||
if u_start and u_start + u_count - 1 > rack.get(RackBuiltinAttributes.U_COUNT):
|
||||
return abort(400, ErrFormat.dcim_rack_u_slot_invalid)
|
||||
|
||||
devices, _, _, _, _, _ = RelationSearch(
|
||||
[rack_id],
|
||||
level=[1],
|
||||
fl=[RackBuiltinAttributes.U_COUNT, RackBuiltinAttributes.U_START],
|
||||
count=1000000).search()
|
||||
|
||||
u_count_sum = 0
|
||||
for device in devices:
|
||||
u_count_sum += (device.get(RackBuiltinAttributes.U_COUNT) or 2)
|
||||
if device_id is not None:
|
||||
_u_start = device.get(RackBuiltinAttributes.U_START)
|
||||
_u_count = device.get(RackBuiltinAttributes.U_COUNT) or 2
|
||||
if not _u_start:
|
||||
continue
|
||||
|
||||
if device.get('_id') != device_id and set(range(u_start, u_start + u_count)) & set(
|
||||
range(_u_start, _u_start + _u_count)):
|
||||
return abort(400, ErrFormat.dcim_rack_u_slot_invalid)
|
||||
|
||||
return rack[RackBuiltinAttributes.U_COUNT] - u_count_sum
|
||||
|
||||
def check_u_slot(self):
|
||||
racks, _, _, _, _, _ = SearchFromDB(
|
||||
"_type:{}".format(self.type_id),
|
||||
count=10000000,
|
||||
fl=[RackBuiltinAttributes.U_START, RackBuiltinAttributes.U_COUNT, RackBuiltinAttributes.U_SLOT_ABNORMAL],
|
||||
parent_node_perm_passed=True).search()
|
||||
|
||||
for rack in racks:
|
||||
devices, _, _, _, _, _ = RelationSearch(
|
||||
[rack['_id']],
|
||||
level=[1],
|
||||
fl=[RackBuiltinAttributes.U_COUNT, RackBuiltinAttributes.U_START],
|
||||
count=1000000).search()
|
||||
|
||||
u_slot_sets = []
|
||||
for device in devices:
|
||||
u_start = device.get(RackBuiltinAttributes.U_START)
|
||||
u_count = device.get(RackBuiltinAttributes.U_COUNT) or 2
|
||||
if u_start is not None and str(u_start).isdigit():
|
||||
u_slot_sets.append(set(range(u_start, u_start + u_count)))
|
||||
|
||||
if len(u_slot_sets) > 1:
|
||||
u_slot_abnormal = False
|
||||
for a, b in itertools.combinations(u_slot_sets, 2):
|
||||
if a.intersection(b):
|
||||
u_slot_abnormal = True
|
||||
break
|
||||
if u_slot_abnormal != rack.get(RackBuiltinAttributes.U_SLOT_ABNORMAL):
|
||||
payload = {RackBuiltinAttributes.U_SLOT_ABNORMAL: u_slot_abnormal}
|
||||
CIManager().update(rack['_id'], **payload)
|
||||
|
||||
def add_device(self, rack_id, device_id, u_start, u_count=None):
|
||||
with (redis_lock.Lock(rd.r, "DCIM_RACK_OPERATE_{}".format(rack_id))):
|
||||
self.calc_u_free_count(rack_id, device_id, u_start, u_count)
|
||||
|
||||
self.add_relation(rack_id, device_id)
|
||||
|
||||
payload = {RackBuiltinAttributes.U_START: u_start}
|
||||
if u_count:
|
||||
payload[RackBuiltinAttributes.U_COUNT] = u_count
|
||||
CIManager().update(device_id, _sync=True, **payload)
|
||||
|
||||
payload = {
|
||||
RackBuiltinAttributes.FREE_U_COUNT: self.calc_u_free_count(rack_id, device_id, u_start, u_count)}
|
||||
CIManager().update(rack_id, _sync=True, **payload)
|
||||
|
||||
OperateHistoryManager().add(operate_type=OperateTypeEnum.ADD_DEVICE, rack_id=rack_id, ci_id=device_id)
|
||||
|
||||
def remove_device(self, rack_id, device_id):
|
||||
with (redis_lock.Lock(rd.r, "DCIM_RACK_OPERATE_{}".format(rack_id))):
|
||||
CIRelationManager.delete_3(rack_id, device_id, apply_async=False, valid=False)
|
||||
|
||||
payload = {RackBuiltinAttributes.FREE_U_COUNT: self.calc_u_free_count(rack_id)}
|
||||
CIManager().update(rack_id, _sync=True, **payload)
|
||||
|
||||
payload = {RackBuiltinAttributes.U_START: None}
|
||||
CIManager().update(device_id, _sync=True, **payload)
|
||||
|
||||
OperateHistoryManager().add(operate_type=OperateTypeEnum.REMOVE_DEVICE, rack_id=rack_id, ci_id=device_id)
|
||||
|
||||
def move_device(self, rack_id, device_id, to_u_start):
|
||||
with (redis_lock.Lock(rd.r, "DCIM_RACK_OPERATE_{}".format(rack_id))):
|
||||
payload = {RackBuiltinAttributes.FREE_U_COUNT: self.calc_u_free_count(rack_id, device_id, to_u_start)}
|
||||
CIManager().update(rack_id, _sync=True, **payload)
|
||||
|
||||
CIManager().update(device_id, _sync=True, **{RackBuiltinAttributes.U_START: to_u_start})
|
||||
|
||||
OperateHistoryManager().add(operate_type=OperateTypeEnum.MOVE_DEVICE, rack_id=rack_id, ci_id=device_id)
|
||||
|
||||
def migrate_device(self, rack_id, device_id, to_rack_id, to_u_start):
|
||||
with (redis_lock.Lock(rd.r, "DCIM_RACK_OPERATE_{}".format(rack_id))):
|
||||
self.calc_u_free_count(to_rack_id, device_id, to_u_start)
|
||||
|
||||
if rack_id != to_rack_id:
|
||||
CIRelationManager.delete_3(rack_id, device_id, apply_async=False, valid=False)
|
||||
|
||||
self.add_relation(to_rack_id, device_id)
|
||||
|
||||
payload = {
|
||||
RackBuiltinAttributes.FREE_U_COUNT: self.calc_u_free_count(to_rack_id, device_id, to_u_start)}
|
||||
CIManager().update(to_rack_id, _sync=True, **payload)
|
||||
|
||||
CIManager().update(device_id, _sync=True, **{RackBuiltinAttributes.U_START: to_u_start})
|
||||
|
||||
if rack_id != to_rack_id:
|
||||
payload = {RackBuiltinAttributes.FREE_U_COUNT: self.calc_u_free_count(rack_id)}
|
||||
CIManager().update(rack_id, _sync=True, **payload)
|
||||
|
||||
OperateHistoryManager().add(operate_type=OperateTypeEnum.REMOVE_DEVICE, rack_id=rack_id, ci_id=device_id)
|
||||
OperateHistoryManager().add(operate_type=OperateTypeEnum.ADD_DEVICE, rack_id=to_rack_id, ci_id=device_id)
|
29
cmdb-api/api/lib/cmdb/dcim/region.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
|
||||
from flask import abort
|
||||
|
||||
from api.lib.cmdb.cache import CITypeCache
|
||||
from api.lib.cmdb.ci import CIManager
|
||||
from api.lib.cmdb.const import BuiltinModelEnum
|
||||
from api.lib.cmdb.const import ExistPolicy
|
||||
from api.lib.cmdb.resp_format import ErrFormat
|
||||
|
||||
|
||||
class RegionManager(object):
|
||||
def __init__(self):
|
||||
self.ci_type = CITypeCache.get(BuiltinModelEnum.DCIM_REGION) or abort(
|
||||
404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_REGION))
|
||||
|
||||
self.type_id = self.ci_type.id
|
||||
|
||||
def add(self, **kwargs):
|
||||
return CIManager().add(self.type_id, exist_policy=ExistPolicy.REJECT, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def update(cls, _id, **kwargs):
|
||||
CIManager().update(_id, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def delete(cls, _id):
|
||||
CIManager().delete(_id)
|
56
cmdb-api/api/lib/cmdb/dcim/server_room.py
Normal file
@@ -0,0 +1,56 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
|
||||
from flask import abort
|
||||
|
||||
from api.lib.cmdb.cache import CITypeCache
|
||||
from api.lib.cmdb.const import BuiltinModelEnum
|
||||
from api.lib.cmdb.dcim.base import DCIMBase
|
||||
from api.lib.cmdb.dcim.const import RackBuiltinAttributes
|
||||
from api.lib.cmdb.resp_format import ErrFormat
|
||||
from api.lib.cmdb.search.ci.db.search import Search as SearchFromDB
|
||||
from api.models.cmdb import CI
|
||||
from api.models.cmdb import CIRelation
|
||||
|
||||
|
||||
class ServerRoomManager(DCIMBase):
|
||||
def __init__(self):
|
||||
super(ServerRoomManager, self).__init__()
|
||||
|
||||
self.ci_type = CITypeCache.get(BuiltinModelEnum.DCIM_SERVER_ROOM) or abort(
|
||||
404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_SERVER_ROOM))
|
||||
self.type_id = self.ci_type.id
|
||||
|
||||
@staticmethod
|
||||
def get_racks(_id, q=None):
|
||||
rack_type = CITypeCache.get(BuiltinModelEnum.DCIM_RACK) or abort(
|
||||
404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_RACK))
|
||||
|
||||
relations = CIRelation.get_by(first_ci_id=_id, only_query=True).join(
|
||||
CI, CI.id == CIRelation.second_ci_id).filter(CI.type_id == rack_type.id)
|
||||
rack_ids = [i.second_ci_id for i in relations]
|
||||
|
||||
q = "_type:{}".format(rack_type.id) if not q else "_type:{},{}".format(rack_type.id, q)
|
||||
if rack_ids:
|
||||
response, _, _, _, numfound, _ = SearchFromDB(
|
||||
q,
|
||||
ci_ids=list(rack_ids),
|
||||
count=1000000,
|
||||
parent_node_perm_passed=True).search()
|
||||
else:
|
||||
response, numfound = [], 0
|
||||
|
||||
counter = dict(rack_count=numfound)
|
||||
u_count = 0
|
||||
free_u_count = 0
|
||||
for i in response:
|
||||
_u_count = i.get(RackBuiltinAttributes.U_COUNT) or 0
|
||||
u_count += _u_count
|
||||
free_u_count += (_u_count if i.get(RackBuiltinAttributes.FREE_U_COUNT) is None else
|
||||
i.get(RackBuiltinAttributes.FREE_U_COUNT))
|
||||
counter["u_count"] = u_count
|
||||
counter["u_used_count"] = u_count - free_u_count
|
||||
counter["device_count"] = CIRelation.get_by(only_query=True).filter(
|
||||
CIRelation.first_ci_id.in_(rack_ids)).count()
|
||||
|
||||
return counter, response
|
85
cmdb-api/api/lib/cmdb/dcim/tree_view.py
Normal file
@@ -0,0 +1,85 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from flask import abort
|
||||
|
||||
from api.lib.cmdb.cache import AttributeCache
|
||||
from api.lib.cmdb.cache import CITypeCache
|
||||
from api.lib.cmdb.const import BuiltinModelEnum
|
||||
from api.lib.cmdb.resp_format import ErrFormat
|
||||
from api.lib.cmdb.search.ci.db.search import Search as SearchFromDB
|
||||
from api.models.cmdb import CI
|
||||
from api.models.cmdb import CIRelation
|
||||
|
||||
|
||||
class TreeViewManager(object):
|
||||
@classmethod
|
||||
def get(cls):
|
||||
region_type = CITypeCache.get(BuiltinModelEnum.DCIM_REGION) or abort(
|
||||
404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_REGION))
|
||||
|
||||
idc_type = CITypeCache.get(BuiltinModelEnum.DCIM_IDC) or abort(
|
||||
404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_IDC))
|
||||
|
||||
server_room_type = CITypeCache.get(BuiltinModelEnum.DCIM_SERVER_ROOM) or abort(
|
||||
404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_SERVER_ROOM))
|
||||
|
||||
rack_type = CITypeCache.get(BuiltinModelEnum.DCIM_RACK) or abort(
|
||||
404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_RACK))
|
||||
|
||||
relations = defaultdict(set)
|
||||
ids = set()
|
||||
has_parent_ids = set()
|
||||
|
||||
for i in CIRelation.get_by(only_query=True).join(CI, CI.id == CIRelation.first_ci_id).filter(
|
||||
CI.type_id.in_([region_type.id, idc_type.id])):
|
||||
relations[i.first_ci_id].add(i.second_ci_id)
|
||||
ids.add(i.first_ci_id)
|
||||
ids.add(i.second_ci_id)
|
||||
has_parent_ids.add(i.second_ci_id)
|
||||
|
||||
for i in CIRelation.get_by(only_query=True).join(
|
||||
CI, CI.id == CIRelation.second_ci_id).filter(CI.type_id.in_([idc_type.id, server_room_type.id])):
|
||||
relations[i.first_ci_id].add(i.second_ci_id)
|
||||
ids.add(i.first_ci_id)
|
||||
ids.add(i.second_ci_id)
|
||||
has_parent_ids.add(i.second_ci_id)
|
||||
|
||||
for i in CI.get_by(only_query=True).filter(CI.type_id.in_([region_type.id, idc_type.id])):
|
||||
ids.add(i.id)
|
||||
|
||||
for _id in ids:
|
||||
if _id not in has_parent_ids:
|
||||
relations[None].add(_id)
|
||||
|
||||
type2name = dict()
|
||||
type2name[region_type.id] = AttributeCache.get(region_type.show_id or region_type.unique_id).name
|
||||
type2name[idc_type.id] = AttributeCache.get(idc_type.show_id or idc_type.unique_id).name
|
||||
type2name[server_room_type.id] = AttributeCache.get(server_room_type.show_id or server_room_type.unique_id).name
|
||||
|
||||
response, _, _, _, _, _ = SearchFromDB(
|
||||
"_type:({})".format(";".join(map(str, [region_type.id, idc_type.id, server_room_type.id]))),
|
||||
ci_ids=list(ids),
|
||||
count=1000000,
|
||||
fl=list(type2name.values()),
|
||||
parent_node_perm_passed=True).search()
|
||||
id2ci = {i['_id']: i for i in response}
|
||||
|
||||
def _build_tree(_tree, parent_id=None):
|
||||
tree = []
|
||||
for child_id in _tree.get(parent_id, []):
|
||||
children = sorted(_build_tree(_tree, child_id), key=lambda x: x['_id'])
|
||||
if not id2ci.get(child_id):
|
||||
continue
|
||||
ci = id2ci[child_id]
|
||||
if ci['ci_type'] == BuiltinModelEnum.DCIM_SERVER_ROOM:
|
||||
ci['rack_count'] = CIRelation.get_by(first_ci_id=child_id, only_query=True).join(
|
||||
CI, CI.id == CIRelation.second_ci_id).filter(CI.type_id == rack_type.id).count()
|
||||
|
||||
tree.append({'children': children, **ci})
|
||||
return tree
|
||||
|
||||
result = sorted(_build_tree(relations), key=lambda x: x['_id'])
|
||||
|
||||
return result, type2name
|
@@ -3,7 +3,6 @@
|
||||
import redis_lock
|
||||
from flask import abort
|
||||
|
||||
from api.extensions import db
|
||||
from api.extensions import rd
|
||||
from api.lib.cmdb.cache import CITypeCache
|
||||
from api.lib.cmdb.ci import CIManager
|
||||
@@ -21,9 +20,8 @@ from api.lib.cmdb.search.ci_relation.search import Search as RelationSearch
|
||||
|
||||
class IpAddressManager(object):
|
||||
def __init__(self):
|
||||
self.ci_type = CITypeCache.get(BuiltinModelEnum.IPAM_ADDRESS)
|
||||
not self.ci_type and abort(400, ErrFormat.ipam_address_model_not_found.format(
|
||||
BuiltinModelEnum.IPAM_ADDRESS))
|
||||
self.ci_type = CITypeCache.get(BuiltinModelEnum.IPAM_ADDRESS) or abort(
|
||||
404, ErrFormat.ipam_address_model_not_found.format(BuiltinModelEnum.IPAM_ADDRESS))
|
||||
|
||||
self.type_id = self.ci_type.id
|
||||
|
||||
@@ -33,10 +31,11 @@ class IpAddressManager(object):
|
||||
|
||||
return numfound, result
|
||||
|
||||
def _get_cis(self, ips):
|
||||
response, _, _, _, _, _ = SearchFromDB(
|
||||
"_type:{},{}:({})".format(self.type_id, IPAddressBuiltinAttributes.IP, ";".join(ips or [])),
|
||||
count=10000000, parent_node_perm_passed=True).search()
|
||||
def _get_cis(self, subnet_id, ips):
|
||||
|
||||
q = "_type:{},{}:({})".format(self.type_id, IPAddressBuiltinAttributes.IP, ";".join(ips or []))
|
||||
|
||||
response, _, _, _, _, _ = RelationSearch([subnet_id], level=[1], query=q, count=1000000).search()
|
||||
|
||||
return response
|
||||
|
||||
@@ -48,25 +47,28 @@ class IpAddressManager(object):
|
||||
CIRelationManager().add(parent_id, child_id, valid=False, apply_async=False)
|
||||
|
||||
@staticmethod
|
||||
def calc_free_count(subnet_id):
|
||||
db.session.commit()
|
||||
def calc_used_count(subnet_id):
|
||||
q = "{}:(0;2),-{}:true".format(IPAddressBuiltinAttributes.ASSIGN_STATUS, IPAddressBuiltinAttributes.IS_USED)
|
||||
|
||||
return len(set(RelationSearch([subnet_id], level=[1], query=q).search(only_ids=True) or []))
|
||||
return len(set(RelationSearch([subnet_id], level=[1], query=q, count=1000000).search(only_ids=True) or []))
|
||||
|
||||
def _update_subnet_count(self, subnet_id, assign_count, used_count=None):
|
||||
@staticmethod
|
||||
def _calc_assign_count(subnet_id):
|
||||
q = "{}:(0;2)".format(IPAddressBuiltinAttributes.ASSIGN_STATUS)
|
||||
|
||||
return len(set(RelationSearch([subnet_id], level=[1], query=q, count=1000000).search(only_ids=True) or []))
|
||||
|
||||
def _update_subnet_count(self, subnet_id, assign_count_computed, used_count=None):
|
||||
payload = {}
|
||||
|
||||
cur = CIManager.get_ci_by_id(subnet_id, need_children=False)
|
||||
if assign_count is not None:
|
||||
payload[SubnetBuiltinAttributes.ASSIGN_COUNT] = (cur.get(
|
||||
SubnetBuiltinAttributes.ASSIGN_COUNT) or 0) + assign_count
|
||||
|
||||
if assign_count_computed:
|
||||
payload[SubnetBuiltinAttributes.ASSIGN_COUNT] = self._calc_assign_count(subnet_id)
|
||||
if used_count is not None:
|
||||
payload[SubnetBuiltinAttributes.USED_COUNT] = used_count
|
||||
|
||||
payload[SubnetBuiltinAttributes.FREE_COUNT] = (cur[SubnetBuiltinAttributes.HOSTS_COUNT] -
|
||||
self.calc_free_count(subnet_id))
|
||||
self.calc_used_count(subnet_id))
|
||||
CIManager().update(subnet_id, **payload)
|
||||
|
||||
def assign_ips(self, ips, subnet_id, cidr, **kwargs):
|
||||
@@ -91,39 +93,32 @@ class IpAddressManager(object):
|
||||
return abort(400, ErrFormat.ipam_address_model_not_found)
|
||||
|
||||
with (redis_lock.Lock(rd.r, "IPAM_ASSIGN_ADDRESS_{}".format(subnet_id))):
|
||||
cis = self._get_cis(ips)
|
||||
cis = self._get_cis(subnet_id, ips)
|
||||
ip2ci = {ci[IPAddressBuiltinAttributes.IP]: ci for ci in cis}
|
||||
|
||||
ci_ids = []
|
||||
status_change_num = 0
|
||||
for ip in ips:
|
||||
kwargs['name'] = ip
|
||||
kwargs[IPAddressBuiltinAttributes.IP] = ip
|
||||
if ip not in ip2ci:
|
||||
ci_id = CIManager.add(self.type_id, _sync=True, **kwargs)
|
||||
status_change_num += 1
|
||||
else:
|
||||
ci_id = ip2ci[ip]['_id']
|
||||
CIManager().update(ci_id, _sync=True, **kwargs)
|
||||
if IPAddressBuiltinAttributes.ASSIGN_STATUS in kwargs and (
|
||||
(kwargs[IPAddressBuiltinAttributes.ASSIGN_STATUS] or 2) !=
|
||||
(ip2ci[ip].get(IPAddressBuiltinAttributes.ASSIGN_STATUS) or 2)):
|
||||
status_change_num += 1
|
||||
ci_ids.append(ci_id)
|
||||
|
||||
self._add_relation(subnet_id, ci_id)
|
||||
|
||||
if ips and IPAddressBuiltinAttributes.ASSIGN_STATUS in kwargs:
|
||||
self._update_subnet_count(subnet_id, -status_change_num if kwargs.get(
|
||||
IPAddressBuiltinAttributes.ASSIGN_STATUS) == IPAddressAssignStatus.UNASSIGNED else status_change_num)
|
||||
self._update_subnet_count(subnet_id, True)
|
||||
|
||||
if ips and IPAddressBuiltinAttributes.IS_USED in kwargs:
|
||||
q = "{}:true".format(IPAddressBuiltinAttributes.IS_USED)
|
||||
cur_used_ids = RelationSearch([subnet_id], level=[1], query=q).search(only_ids=True)
|
||||
for _id in set(cur_used_ids) - set(ci_ids):
|
||||
CIManager().update(_id, _sync=True, **{IPAddressBuiltinAttributes.IS_USED: False})
|
||||
CIManager().update(_id, **{IPAddressBuiltinAttributes.IS_USED: False})
|
||||
|
||||
self._update_subnet_count(subnet_id, None, used_count=len(ips))
|
||||
self._update_subnet_count(subnet_id, False, used_count=len(ips))
|
||||
|
||||
if kwargs.get(IPAddressBuiltinAttributes.ASSIGN_STATUS) in (
|
||||
IPAddressAssignStatus.ASSIGNED, IPAddressAssignStatus.RESERVED):
|
||||
|
@@ -50,6 +50,10 @@ class ScanHistoryManager(DBMixin):
|
||||
if scan_rule is not None:
|
||||
scan_rule.update(last_scan_time=kwargs.get('start_at'))
|
||||
|
||||
for i in self.cls.get_by(subnet_scan_id=kwargs.get('subnet_scan_id'), only_query=True).order_by(
|
||||
self.cls.id.desc()).offset(100):
|
||||
i.delete()
|
||||
|
||||
def _can_update(self, **kwargs):
|
||||
pass
|
||||
|
||||
|
@@ -18,15 +18,13 @@ from api.models.cmdb import IPAMSubnetScan
|
||||
|
||||
class Stats(object):
|
||||
def __init__(self):
|
||||
self.address_type = CITypeCache.get(BuiltinModelEnum.IPAM_ADDRESS)
|
||||
not self.address_type and abort(400, ErrFormat.ipam_address_model_not_found.format(
|
||||
BuiltinModelEnum.IPAM_ADDRESS))
|
||||
self.address_type = CITypeCache.get(BuiltinModelEnum.IPAM_ADDRESS) or abort(
|
||||
404, ErrFormat.ipam_address_model_not_found.format(BuiltinModelEnum.IPAM_ADDRESS))
|
||||
|
||||
self.address_type_id = self.address_type.id
|
||||
|
||||
self.subnet_type = CITypeCache.get(BuiltinModelEnum.IPAM_SUBNET)
|
||||
not self.subnet_type and abort(400, ErrFormat.ipam_address_model_not_found.format(
|
||||
BuiltinModelEnum.IPAM_ADDRESS))
|
||||
self.subnet_type = CITypeCache.get(BuiltinModelEnum.IPAM_SUBNET) or abort(
|
||||
404, ErrFormat.ipam_address_model_not_found.format(BuiltinModelEnum.IPAM_ADDRESS))
|
||||
|
||||
self.subnet_type_id = self.subnet_type.id
|
||||
|
||||
@@ -40,8 +38,10 @@ class Stats(object):
|
||||
return list(set(ci_ids) - set(has_children_ci_ids))
|
||||
|
||||
else:
|
||||
type_id = CIManager().get_by_id(parent_id).type_id
|
||||
key = [(str(parent_id), type_id)]
|
||||
_type = CIManager().get_by_id(parent_id)
|
||||
if not _type:
|
||||
return abort(404, ErrFormat.ipam_subnet_not_found)
|
||||
key = [(str(parent_id), _type.type_id)]
|
||||
result = []
|
||||
while True:
|
||||
res = [json.loads(x).items() for x in [i or '{}' for i in rd.get(
|
||||
|
@@ -2,6 +2,7 @@
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
import datetime
|
||||
import ipaddress
|
||||
from flask import abort
|
||||
|
||||
@@ -9,7 +10,7 @@ from api.lib.cmdb.cache import AttributeCache
|
||||
from api.lib.cmdb.cache import CITypeCache
|
||||
from api.lib.cmdb.ci import CIManager
|
||||
from api.lib.cmdb.ci import CIRelationManager
|
||||
from api.lib.cmdb.const import BuiltinModelEnum, BUILTIN_ATTRIBUTES
|
||||
from api.lib.cmdb.const import BuiltinModelEnum
|
||||
from api.lib.cmdb.ipam.const import OperateTypeEnum
|
||||
from api.lib.cmdb.ipam.const import SubnetBuiltinAttributes
|
||||
from api.lib.cmdb.ipam.history import OperateHistoryManager
|
||||
@@ -22,9 +23,8 @@ from api.models.cmdb import IPAMSubnetScan
|
||||
|
||||
class SubnetManager(object):
|
||||
def __init__(self):
|
||||
self.ci_type = CITypeCache.get(BuiltinModelEnum.IPAM_SUBNET)
|
||||
not self.ci_type and abort(400, ErrFormat.ipam_subnet_model_not_found.format(
|
||||
BuiltinModelEnum.IPAM_SUBNET))
|
||||
self.ci_type = CITypeCache.get(BuiltinModelEnum.IPAM_SUBNET) or abort(
|
||||
404, ErrFormat.ipam_subnet_model_not_found.format(BuiltinModelEnum.IPAM_SUBNET))
|
||||
|
||||
self.type_id = self.ci_type.id
|
||||
|
||||
@@ -47,7 +47,7 @@ class SubnetManager(object):
|
||||
|
||||
new_last_update_at = ""
|
||||
for i in result:
|
||||
__last_update_at = max([i['updated_at'] or "", i['created_at'] or ""])
|
||||
__last_update_at = max([i['rule_updated_at'] or "", i['created_at'] or ""])
|
||||
if new_last_update_at < __last_update_at:
|
||||
new_last_update_at = __last_update_at
|
||||
|
||||
@@ -131,7 +131,11 @@ class SubnetManager(object):
|
||||
@staticmethod
|
||||
def _is_valid_cidr(cidr):
|
||||
try:
|
||||
return str(ipaddress.ip_network(cidr))
|
||||
cidr = ipaddress.ip_network(cidr)
|
||||
if not (8 <= cidr.prefixlen <= 31):
|
||||
raise ValueError
|
||||
|
||||
return str(cidr)
|
||||
except ValueError:
|
||||
return abort(400, ErrFormat.ipam_cidr_invalid_notation.format(cidr))
|
||||
|
||||
@@ -143,6 +147,7 @@ class SubnetManager(object):
|
||||
root_nodes = set(all_nodes) - set(none_root_nodes) - set(_id and [_id] or [])
|
||||
response, _, _, _, _, _ = SearchFromDB("_type:{}".format(self.type_id),
|
||||
ci_ids=list(root_nodes),
|
||||
count=1000000,
|
||||
parent_node_perm_passed=True).search()
|
||||
|
||||
cur_subnet = ipaddress.ip_network(cidr)
|
||||
@@ -163,6 +168,7 @@ class SubnetManager(object):
|
||||
|
||||
response, _, _, _, _, _ = SearchFromDB("_type:{}".format(self.type_id),
|
||||
ci_ids=list(child_nodes),
|
||||
count=1000000,
|
||||
parent_node_perm_passed=True).search()
|
||||
|
||||
cur_subnet = ipaddress.ip_network(cidr)
|
||||
@@ -199,7 +205,7 @@ class SubnetManager(object):
|
||||
return cidr
|
||||
|
||||
def _add_subnet(self, cidr, **kwargs):
|
||||
kwargs[SubnetBuiltinAttributes.HOSTS_COUNT] = ipaddress.ip_network(cidr).num_addresses - 2
|
||||
kwargs[SubnetBuiltinAttributes.HOSTS_COUNT] = len(list(ipaddress.ip_network(cidr).hosts()))
|
||||
kwargs[SubnetBuiltinAttributes.USED_COUNT] = 0
|
||||
kwargs[SubnetBuiltinAttributes.ASSIGN_COUNT] = 0
|
||||
kwargs[SubnetBuiltinAttributes.FREE_COUNT] = kwargs[SubnetBuiltinAttributes.HOSTS_COUNT]
|
||||
@@ -240,7 +246,8 @@ class SubnetManager(object):
|
||||
def _update_scan_rule(ci_id, agent_id, cron, scan_enabled=True):
|
||||
existed = IPAMSubnetScan.get_by(ci_id=ci_id, first=True, to_dict=False)
|
||||
if existed is not None:
|
||||
existed.update(ci_id=ci_id, agent_id=agent_id, cron=cron, scan_enabled=scan_enabled)
|
||||
existed.update(ci_id=ci_id, agent_id=agent_id, cron=cron, scan_enabled=scan_enabled,
|
||||
rule_updated_at=datetime.datetime.now())
|
||||
else:
|
||||
IPAMSubnetScan.create(ci_id=ci_id, agent_id=agent_id, cron=cron, scan_enabled=scan_enabled)
|
||||
|
||||
@@ -273,7 +280,9 @@ class SubnetManager(object):
|
||||
existed = IPAMSubnetScan.get_by(ci_id=_id, first=True, to_dict=False)
|
||||
existed and existed.delete()
|
||||
|
||||
delete_ci_ids = []
|
||||
for i in CIRelation.get_by(first_ci_id=_id, to_dict=False):
|
||||
delete_ci_ids.append(i.second_ci_id)
|
||||
i.delete()
|
||||
|
||||
cur = CIManager.get_ci_by_id(_id, need_children=False)
|
||||
@@ -284,6 +293,8 @@ class SubnetManager(object):
|
||||
cidr=cur.get(SubnetBuiltinAttributes.CIDR),
|
||||
description=cur.get(SubnetBuiltinAttributes.CIDR))
|
||||
|
||||
# batch_delete_ci.apply_async(args=(delete_ci_ids,))
|
||||
|
||||
return _id
|
||||
|
||||
|
||||
|
@@ -169,3 +169,8 @@ class ErrFormat(CommonErrFormat):
|
||||
ipam_subnet_cannot_delete = _l("Cannot delete because child nodes exist")
|
||||
ipam_subnet_not_found = _l("Subnet is not found")
|
||||
ipam_scope_cannot_delete = _l("Cannot delete because child nodes exist")
|
||||
|
||||
# # DCIM
|
||||
dcim_builtin_model_not_found = _l("The dcim model {} does not exist")
|
||||
dcim_rack_u_slot_invalid = _l("Irregularities in Rack Units")
|
||||
dcim_rack_u_count_invalid = _l("The device's position is greater than the rack unit height")
|
||||
|
@@ -64,7 +64,7 @@ class Search(object):
|
||||
|
||||
self.ancestor_ids = ancestor_ids
|
||||
self.descendant_ids = descendant_ids
|
||||
self.root_parent_path = root_parent_path
|
||||
self.root_parent_path = root_parent_path or []
|
||||
self.has_m2m = has_m2m or False
|
||||
if not self.has_m2m:
|
||||
if self.ancestor_ids:
|
||||
|
@@ -54,6 +54,7 @@ class CMDBApp(BaseApp):
|
||||
"create_topology_view"],
|
||||
},
|
||||
{"page": "IPAM", "page_cn": "IPAM", "perms": ["read"]},
|
||||
{"page": "DCIM", "page_cn": "数据中心", "perms": ["read"]},
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
|
@@ -676,6 +676,7 @@ class IPAMSubnetScan(Model):
|
||||
|
||||
ci_id = db.Column(db.Integer, index=True, nullable=False)
|
||||
scan_enabled = db.Column(db.Boolean, default=True)
|
||||
rule_updated_at = db.Column(db.DateTime)
|
||||
last_scan_time = db.Column(db.DateTime)
|
||||
|
||||
# scan rules
|
||||
@@ -706,3 +707,14 @@ class IPAMOperationHistory(Model2):
|
||||
cidr = db.Column(db.String(18), index=True)
|
||||
operate_type = db.Column(db.Enum(*OperateTypeEnum.all()))
|
||||
description = db.Column(db.Text)
|
||||
|
||||
|
||||
class DCIMOperationHistory(Model2):
|
||||
__tablename__ = "c_dcim_operation_histories"
|
||||
|
||||
from api.lib.cmdb.dcim.const import OperateTypeEnum
|
||||
|
||||
uid = db.Column(db.Integer, index=True)
|
||||
rack_id = db.Column(db.Integer, index=True)
|
||||
ci_id = db.Column(db.Integer, index=True)
|
||||
operate_type = db.Column(db.Enum(*OperateTypeEnum.all()))
|
||||
|
@@ -1,4 +1,4 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
|
||||
import datetime
|
||||
@@ -374,3 +374,25 @@ def build_relations_for_ad_accept(adc, ci_id, ad_key2attr):
|
||||
source=RelationSourceEnum.AUTO_DISCOVERY)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
@celery.task(name="cmdb.dcim_calc_u_free_count", queue=CMDB_QUEUE)
|
||||
@reconnect_db
|
||||
def dcim_calc_u_free_count():
|
||||
from api.lib.cmdb.ci import CIManager
|
||||
from api.lib.cmdb.dcim.rack import RackManager
|
||||
from api.lib.cmdb.dcim.const import RackBuiltinAttributes
|
||||
|
||||
if not has_request_context():
|
||||
current_app.test_request_context().push()
|
||||
login_user(UserCache.get('worker'))
|
||||
|
||||
try:
|
||||
rack_m = RackManager()
|
||||
except Exception:
|
||||
return
|
||||
|
||||
racks = CI.get_by(type_id=rack_m.type_id, to_dict=False)
|
||||
for rack in racks:
|
||||
payload = {RackBuiltinAttributes.FREE_U_COUNT: rack_m.calc_u_free_count(rack.id)}
|
||||
CIManager().update(rack.id, **payload)
|
||||
|
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2024-11-11 17:40+0800\n"
|
||||
"POT-Creation-Date: 2024-11-26 18:54+0800\n"
|
||||
"PO-Revision-Date: 2023-12-25 20:21+0800\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: zh\n"
|
||||
@@ -92,11 +92,11 @@ msgstr "您没有操作权限!"
|
||||
msgid "Only the creator or administrator has permission!"
|
||||
msgstr "只有创建人或者管理员才有权限!"
|
||||
|
||||
#: api/lib/cmdb/const.py:128
|
||||
#: api/lib/cmdb/const.py:133
|
||||
msgid "Update Time"
|
||||
msgstr "更新时间"
|
||||
|
||||
#: api/lib/cmdb/const.py:129
|
||||
#: api/lib/cmdb/const.py:134
|
||||
msgid "Updated By"
|
||||
msgstr "更新人"
|
||||
|
||||
@@ -544,6 +544,18 @@ msgstr "因为子节点已经存在,不能删除"
|
||||
msgid "Subnet is not found"
|
||||
msgstr "子网不存在"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:174
|
||||
msgid "The dcim model {} does not exist"
|
||||
msgstr "DCIM模型 {} 不存在!"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:175
|
||||
msgid "Irregularities in Rack Units"
|
||||
msgstr "机架U位异常!"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:176
|
||||
msgid "The device's position is greater than the rack unit height"
|
||||
msgstr "有设备的位置大于机柜的U数!"
|
||||
|
||||
#: api/lib/common_setting/resp_format.py:8
|
||||
msgid "Company info already existed"
|
||||
msgstr "公司信息已存在,无法创建!"
|
||||
|
@@ -2,14 +2,14 @@
|
||||
|
||||
|
||||
import json
|
||||
from io import BytesIO
|
||||
|
||||
from flask import abort
|
||||
from flask import current_app
|
||||
from flask import request
|
||||
from io import BytesIO
|
||||
|
||||
from api.lib.cmdb.cache import AttributeCache
|
||||
from api.lib.cmdb.cache import CITypeCache
|
||||
from api.lib.cmdb.ci import CITriggerManager
|
||||
from api.lib.cmdb.ci_type import CITypeAttributeGroupManager
|
||||
from api.lib.cmdb.ci_type import CITypeAttributeManager
|
||||
from api.lib.cmdb.ci_type import CITypeGroupManager
|
||||
@@ -497,6 +497,16 @@ class CITypeTriggerView(APIView):
|
||||
return self.jsonify(code=200)
|
||||
|
||||
|
||||
class CITypeTriggerTestView(APIView):
|
||||
url_prefix = ("/ci_types/<int:type_id>/triggers/<int:_id>/test_notify",)
|
||||
|
||||
@has_perm_from_args("type_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id)
|
||||
def post(self, type_id, _id):
|
||||
CITriggerManager().trigger_notify_test(type_id, _id)
|
||||
|
||||
return self.jsonify(code=200)
|
||||
|
||||
|
||||
class CITypeGrantView(APIView):
|
||||
url_prefix = "/ci_types/<int:type_id>/roles/<int:rid>/grant"
|
||||
|
||||
|
1
cmdb-api/api/views/cmdb/dcim/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# -*- coding:utf-8 -*-
|
30
cmdb-api/api/views/cmdb/dcim/dcim_history.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
from flask import request
|
||||
|
||||
from api.lib.cmdb.dcim.history import OperateHistoryManager
|
||||
from api.lib.common_setting.decorator import perms_role_required
|
||||
from api.lib.common_setting.role_perm_base import CMDBApp
|
||||
from api.lib.utils import get_page
|
||||
from api.lib.utils import get_page_size
|
||||
from api.lib.utils import handle_arg_list
|
||||
from api.resource import APIView
|
||||
|
||||
app_cli = CMDBApp()
|
||||
|
||||
|
||||
class DCIMOperateHistoryView(APIView):
|
||||
url_prefix = ("/dcim/history/operate",)
|
||||
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
def get(self):
|
||||
page = get_page(request.values.pop("page", 1))
|
||||
page_size = get_page_size(request.values.pop("page_size", None))
|
||||
operate_type = handle_arg_list(request.values.pop('operate_type', []))
|
||||
if operate_type:
|
||||
request.values["operate_type"] = operate_type
|
||||
|
||||
numfound, result, id2ci, type2show_key = OperateHistoryManager.search(page, page_size, **request.values)
|
||||
|
||||
return self.jsonify(numfound=numfound, result=result, id2ci=id2ci, type2show_key=type2show_key)
|
35
cmdb-api/api/views/cmdb/dcim/idc.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
from flask import request
|
||||
|
||||
from api.lib.cmdb.dcim.idc import IDCManager
|
||||
from api.lib.common_setting.decorator import perms_role_required
|
||||
from api.lib.common_setting.role_perm_base import CMDBApp
|
||||
from api.resource import APIView
|
||||
|
||||
app_cli = CMDBApp()
|
||||
|
||||
|
||||
class IDCView(APIView):
|
||||
url_prefix = ("/dcim/idc", "/dcim/idc/<int:_id>")
|
||||
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
def post(self):
|
||||
parent_id = request.values.pop("parent_id")
|
||||
|
||||
return self.jsonify(ci_id=IDCManager().add(parent_id, **request.values))
|
||||
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
def put(self, _id):
|
||||
IDCManager().update(_id, **request.values)
|
||||
|
||||
return self.jsonify(ci_id=_id)
|
||||
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
def delete(self, _id):
|
||||
IDCManager().delete(_id)
|
||||
|
||||
return self.jsonify(ci_id=_id)
|
102
cmdb-api/api/views/cmdb/dcim/rack.py
Normal file
@@ -0,0 +1,102 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
from flask import request
|
||||
|
||||
from api.lib.cmdb.const import CMDB_QUEUE
|
||||
from api.lib.cmdb.dcim.const import RackBuiltinAttributes
|
||||
from api.lib.cmdb.dcim.rack import RackManager
|
||||
from api.lib.common_setting.decorator import perms_role_required
|
||||
from api.lib.common_setting.role_perm_base import CMDBApp
|
||||
from api.lib.decorator import args_required
|
||||
from api.resource import APIView
|
||||
from api.tasks.cmdb import dcim_calc_u_free_count
|
||||
|
||||
app_cli = CMDBApp()
|
||||
|
||||
|
||||
class RackView(APIView):
|
||||
url_prefix = ("/dcim/rack", "/dcim/rack/<int:_id>")
|
||||
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
@args_required("parent_id")
|
||||
def post(self):
|
||||
parent_id = request.values.pop("parent_id")
|
||||
|
||||
return self.jsonify(ci_id=RackManager().add(parent_id, **request.values))
|
||||
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
def put(self, _id):
|
||||
RackManager().update(_id, **request.values)
|
||||
|
||||
return self.jsonify(ci_id=_id)
|
||||
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
def delete(self, _id):
|
||||
RackManager().delete(_id)
|
||||
|
||||
return self.jsonify(ci_id=_id)
|
||||
|
||||
|
||||
class RackDetailView(APIView):
|
||||
url_prefix = ("/dcim/rack/<int:rack_id>/device/<int:device_id>",)
|
||||
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
@args_required(RackBuiltinAttributes.U_START)
|
||||
def post(self, rack_id, device_id):
|
||||
u_start = request.values.pop(RackBuiltinAttributes.U_START)
|
||||
u_count = request.values.get(RackBuiltinAttributes.U_COUNT)
|
||||
|
||||
RackManager().add_device(rack_id, device_id, u_start, u_count)
|
||||
|
||||
return self.jsonify(rack_id=rack_id, device_id=device_id)
|
||||
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
@args_required("to_u_start")
|
||||
def put(self, rack_id, device_id):
|
||||
to_u_start = request.values.pop("to_u_start")
|
||||
|
||||
RackManager().move_device(rack_id, device_id, to_u_start)
|
||||
|
||||
return self.jsonify(rack_id=rack_id, device_id=device_id, to_u_start=to_u_start)
|
||||
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
def delete(self, rack_id, device_id):
|
||||
RackManager().remove_device(rack_id, device_id)
|
||||
|
||||
return self.jsonify(code=200)
|
||||
|
||||
|
||||
class RackDeviceMigrateView(APIView):
|
||||
url_prefix = ("/dcim/rack/<int:rack_id>/device/<int:device_id>/migrate",)
|
||||
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
@args_required("to_rack_id")
|
||||
@args_required("to_u_start")
|
||||
def put(self, rack_id, device_id):
|
||||
to_rack_id = request.values.pop("to_rack_id")
|
||||
to_u_start = request.values.pop("to_u_start")
|
||||
|
||||
RackManager().migrate_device(rack_id, device_id, to_rack_id, to_u_start)
|
||||
|
||||
return self.jsonify(rack_id=rack_id,
|
||||
device_id=device_id,
|
||||
to_u_start=to_u_start,
|
||||
to_rack_id=to_rack_id)
|
||||
|
||||
|
||||
class RackCalcUFreeCountView(APIView):
|
||||
url_prefix = ("/dcim/rack/calc_u_free_count",)
|
||||
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
def post(self):
|
||||
dcim_calc_u_free_count.apply_async(queue=CMDB_QUEUE)
|
||||
|
||||
return self.jsonify(code=200)
|
33
cmdb-api/api/views/cmdb/dcim/region.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
from flask import request
|
||||
|
||||
from api.lib.cmdb.dcim.region import RegionManager
|
||||
from api.lib.common_setting.decorator import perms_role_required
|
||||
from api.lib.common_setting.role_perm_base import CMDBApp
|
||||
from api.resource import APIView
|
||||
|
||||
app_cli = CMDBApp()
|
||||
|
||||
|
||||
class RegionView(APIView):
|
||||
url_prefix = ("/dcim/region", "/dcim/region/<int:_id>")
|
||||
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
def post(self):
|
||||
return self.jsonify(ci_id=RegionManager().add(**request.values))
|
||||
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
def put(self, _id):
|
||||
RegionManager().update(_id, **request.values)
|
||||
|
||||
return self.jsonify(ci_id=_id)
|
||||
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
def delete(self, _id):
|
||||
RegionManager().delete(_id)
|
||||
|
||||
return self.jsonify(ci_id=_id)
|
43
cmdb-api/api/views/cmdb/dcim/server_room.py
Normal file
@@ -0,0 +1,43 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
from flask import request
|
||||
|
||||
from api.lib.cmdb.dcim.server_room import ServerRoomManager
|
||||
from api.lib.common_setting.decorator import perms_role_required
|
||||
from api.lib.common_setting.role_perm_base import CMDBApp
|
||||
from api.lib.decorator import args_required
|
||||
from api.resource import APIView
|
||||
|
||||
app_cli = CMDBApp()
|
||||
|
||||
|
||||
class ServerRoomView(APIView):
|
||||
url_prefix = ("/dcim/server_room", "/dcim/server_room/<int:_id>", "/dcim/server_room/<int:_id>/racks")
|
||||
|
||||
def get(self, _id):
|
||||
q = request.values.get('q')
|
||||
counter, result = ServerRoomManager.get_racks(_id, q)
|
||||
|
||||
return self.jsonify(counter=counter, result=result)
|
||||
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
@args_required("parent_id")
|
||||
def post(self):
|
||||
parent_id = request.values.pop("parent_id")
|
||||
|
||||
return self.jsonify(ci_id=ServerRoomManager().add(parent_id, **request.values))
|
||||
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
def put(self, _id):
|
||||
ServerRoomManager().update(_id, **request.values)
|
||||
|
||||
return self.jsonify(ci_id=_id)
|
||||
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
def delete(self, _id):
|
||||
ServerRoomManager().delete(_id)
|
||||
|
||||
return self.jsonify(ci_id=_id)
|
19
cmdb-api/api/views/cmdb/dcim/tree_view.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
from api.lib.cmdb.dcim.tree_view import TreeViewManager
|
||||
from api.lib.common_setting.decorator import perms_role_required
|
||||
from api.lib.common_setting.role_perm_base import CMDBApp
|
||||
from api.resource import APIView
|
||||
|
||||
app_cli = CMDBApp()
|
||||
|
||||
|
||||
class DCIMTreeView(APIView):
|
||||
url_prefix = "/dcim/tree_view"
|
||||
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
def get(self):
|
||||
result, type2name = TreeViewManager.get()
|
||||
|
||||
return self.jsonify(result=result, type2name=type2name)
|
@@ -1,6 +1,6 @@
|
||||
NODE_ENV=production
|
||||
VUE_APP_PREVIEW=false
|
||||
VUE_APP_API_BASE_URL=http://127.0.0.1:5000/api
|
||||
VUE_APP_API_BASE_URL=/api
|
||||
VUE_APP_BUILD_PACKAGES="ticket,calendar,acl"
|
||||
VUE_APP_IS_OUTER=true
|
||||
VUE_APP_IS_OPEN_SOURCE=true
|
||||
|
@@ -54,6 +54,66 @@
|
||||
<div class="content unicode" style="display: block;">
|
||||
<ul class="icon_lists dib-box">
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">veops-rear</div>
|
||||
<div class="code-name">&#xea02;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">veops-front</div>
|
||||
<div class="code-name">&#xea03;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">veops-xianggang</div>
|
||||
<div class="code-name">&#xea01;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">veops-device (2)</div>
|
||||
<div class="code-name">&#xea00;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">veops-room (1)</div>
|
||||
<div class="code-name">&#xe9ff;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">veops-IDC</div>
|
||||
<div class="code-name">&#xe9fe;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">veops-region</div>
|
||||
<div class="code-name">&#xe9fd;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">veops-device</div>
|
||||
<div class="code-name">&#xe9fb;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">veops-cabinet</div>
|
||||
<div class="code-name">&#xe9fc;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">veops-data_center</div>
|
||||
<div class="code-name">&#xe9f9;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">ops-setting-holiday_management-copy</div>
|
||||
@@ -6102,9 +6162,9 @@
|
||||
<pre><code class="language-css"
|
||||
>@font-face {
|
||||
font-family: 'iconfont';
|
||||
src: url('iconfont.woff2?t=1731312848138') format('woff2'),
|
||||
url('iconfont.woff?t=1731312848138') format('woff'),
|
||||
url('iconfont.ttf?t=1731312848138') format('truetype');
|
||||
src: url('iconfont.woff2?t=1732673294759') format('woff2'),
|
||||
url('iconfont.woff?t=1732673294759') format('woff'),
|
||||
url('iconfont.ttf?t=1732673294759') format('truetype');
|
||||
}
|
||||
</code></pre>
|
||||
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
|
||||
@@ -6130,6 +6190,96 @@
|
||||
<div class="content font-class">
|
||||
<ul class="icon_lists dib-box">
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont veops-rear"></span>
|
||||
<div class="name">
|
||||
veops-rear
|
||||
</div>
|
||||
<div class="code-name">.veops-rear
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont veops-front"></span>
|
||||
<div class="name">
|
||||
veops-front
|
||||
</div>
|
||||
<div class="code-name">.veops-front
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont veops-xianggang"></span>
|
||||
<div class="name">
|
||||
veops-xianggang
|
||||
</div>
|
||||
<div class="code-name">.veops-xianggang
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont a-veops-device2"></span>
|
||||
<div class="name">
|
||||
veops-device (2)
|
||||
</div>
|
||||
<div class="code-name">.a-veops-device2
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont a-veops-room1"></span>
|
||||
<div class="name">
|
||||
veops-room (1)
|
||||
</div>
|
||||
<div class="code-name">.a-veops-room1
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont veops-IDC"></span>
|
||||
<div class="name">
|
||||
veops-IDC
|
||||
</div>
|
||||
<div class="code-name">.veops-IDC
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont veops-region"></span>
|
||||
<div class="name">
|
||||
veops-region
|
||||
</div>
|
||||
<div class="code-name">.veops-region
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont veops-device"></span>
|
||||
<div class="name">
|
||||
veops-device
|
||||
</div>
|
||||
<div class="code-name">.veops-device
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont veops-cabinet"></span>
|
||||
<div class="name">
|
||||
veops-cabinet
|
||||
</div>
|
||||
<div class="code-name">.veops-cabinet
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont veops-data_center"></span>
|
||||
<div class="name">
|
||||
veops-data_center
|
||||
</div>
|
||||
<div class="code-name">.veops-data_center
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont ops-setting-holidays"></span>
|
||||
<div class="name">
|
||||
@@ -15202,6 +15352,86 @@
|
||||
<div class="content symbol">
|
||||
<ul class="icon_lists dib-box">
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#veops-rear"></use>
|
||||
</svg>
|
||||
<div class="name">veops-rear</div>
|
||||
<div class="code-name">#veops-rear</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#veops-front"></use>
|
||||
</svg>
|
||||
<div class="name">veops-front</div>
|
||||
<div class="code-name">#veops-front</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#veops-xianggang"></use>
|
||||
</svg>
|
||||
<div class="name">veops-xianggang</div>
|
||||
<div class="code-name">#veops-xianggang</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#a-veops-device2"></use>
|
||||
</svg>
|
||||
<div class="name">veops-device (2)</div>
|
||||
<div class="code-name">#a-veops-device2</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#a-veops-room1"></use>
|
||||
</svg>
|
||||
<div class="name">veops-room (1)</div>
|
||||
<div class="code-name">#a-veops-room1</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#veops-IDC"></use>
|
||||
</svg>
|
||||
<div class="name">veops-IDC</div>
|
||||
<div class="code-name">#veops-IDC</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#veops-region"></use>
|
||||
</svg>
|
||||
<div class="name">veops-region</div>
|
||||
<div class="code-name">#veops-region</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#veops-device"></use>
|
||||
</svg>
|
||||
<div class="name">veops-device</div>
|
||||
<div class="code-name">#veops-device</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#veops-cabinet"></use>
|
||||
</svg>
|
||||
<div class="name">veops-cabinet</div>
|
||||
<div class="code-name">#veops-cabinet</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#veops-data_center"></use>
|
||||
</svg>
|
||||
<div class="name">veops-data_center</div>
|
||||
<div class="code-name">#veops-data_center</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#ops-setting-holidays"></use>
|
||||
|
@@ -1,8 +1,8 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 3857903 */
|
||||
src: url('iconfont.woff2?t=1731312848138') format('woff2'),
|
||||
url('iconfont.woff?t=1731312848138') format('woff'),
|
||||
url('iconfont.ttf?t=1731312848138') format('truetype');
|
||||
src: url('iconfont.woff2?t=1732673294759') format('woff2'),
|
||||
url('iconfont.woff?t=1732673294759') format('woff'),
|
||||
url('iconfont.ttf?t=1732673294759') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
@@ -13,6 +13,46 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.veops-rear:before {
|
||||
content: "\ea02";
|
||||
}
|
||||
|
||||
.veops-front:before {
|
||||
content: "\ea03";
|
||||
}
|
||||
|
||||
.veops-xianggang:before {
|
||||
content: "\ea01";
|
||||
}
|
||||
|
||||
.a-veops-device2:before {
|
||||
content: "\ea00";
|
||||
}
|
||||
|
||||
.a-veops-room1:before {
|
||||
content: "\e9ff";
|
||||
}
|
||||
|
||||
.veops-IDC:before {
|
||||
content: "\e9fe";
|
||||
}
|
||||
|
||||
.veops-region:before {
|
||||
content: "\e9fd";
|
||||
}
|
||||
|
||||
.veops-device:before {
|
||||
content: "\e9fb";
|
||||
}
|
||||
|
||||
.veops-cabinet:before {
|
||||
content: "\e9fc";
|
||||
}
|
||||
|
||||
.veops-data_center:before {
|
||||
content: "\e9f9";
|
||||
}
|
||||
|
||||
.ops-setting-holidays:before {
|
||||
content: "\e9fa";
|
||||
}
|
||||
|
@@ -5,6 +5,76 @@
|
||||
"css_prefix_text": "",
|
||||
"description": "",
|
||||
"glyphs": [
|
||||
{
|
||||
"icon_id": "42510712",
|
||||
"name": "veops-rear",
|
||||
"font_class": "veops-rear",
|
||||
"unicode": "ea02",
|
||||
"unicode_decimal": 59906
|
||||
},
|
||||
{
|
||||
"icon_id": "42510708",
|
||||
"name": "veops-front",
|
||||
"font_class": "veops-front",
|
||||
"unicode": "ea03",
|
||||
"unicode_decimal": 59907
|
||||
},
|
||||
{
|
||||
"icon_id": "42497603",
|
||||
"name": "veops-xianggang",
|
||||
"font_class": "veops-xianggang",
|
||||
"unicode": "ea01",
|
||||
"unicode_decimal": 59905
|
||||
},
|
||||
{
|
||||
"icon_id": "42485038",
|
||||
"name": "veops-device (2)",
|
||||
"font_class": "a-veops-device2",
|
||||
"unicode": "ea00",
|
||||
"unicode_decimal": 59904
|
||||
},
|
||||
{
|
||||
"icon_id": "42455620",
|
||||
"name": "veops-room (1)",
|
||||
"font_class": "a-veops-room1",
|
||||
"unicode": "e9ff",
|
||||
"unicode_decimal": 59903
|
||||
},
|
||||
{
|
||||
"icon_id": "42455607",
|
||||
"name": "veops-IDC",
|
||||
"font_class": "veops-IDC",
|
||||
"unicode": "e9fe",
|
||||
"unicode_decimal": 59902
|
||||
},
|
||||
{
|
||||
"icon_id": "42455609",
|
||||
"name": "veops-region",
|
||||
"font_class": "veops-region",
|
||||
"unicode": "e9fd",
|
||||
"unicode_decimal": 59901
|
||||
},
|
||||
{
|
||||
"icon_id": "42448953",
|
||||
"name": "veops-device",
|
||||
"font_class": "veops-device",
|
||||
"unicode": "e9fb",
|
||||
"unicode_decimal": 59899
|
||||
},
|
||||
{
|
||||
"icon_id": "42448948",
|
||||
"name": "veops-cabinet",
|
||||
"font_class": "veops-cabinet",
|
||||
"unicode": "e9fc",
|
||||
"unicode_decimal": 59900
|
||||
},
|
||||
{
|
||||
"icon_id": "42433324",
|
||||
"name": "veops-data_center",
|
||||
"font_class": "veops-data_center",
|
||||
"unicode": "e9f9",
|
||||
"unicode_decimal": 59897
|
||||
},
|
||||
{
|
||||
"icon_id": "42337844",
|
||||
"name": "ops-setting-holiday_management-copy",
|
||||
|
@@ -139,9 +139,9 @@
|
||||
:normalizer="
|
||||
(node) => {
|
||||
return {
|
||||
id: String(node[0] || ''),
|
||||
label: node[1] ? node[1].label || node[0] : node[0],
|
||||
children: node.children && node.children.length ? node.children : undefined,
|
||||
id: node.id,
|
||||
label: node.label,
|
||||
children: node.children,
|
||||
}
|
||||
}
|
||||
"
|
||||
@@ -352,8 +352,12 @@ export default {
|
||||
},
|
||||
getChoiceValueByProperty(property) {
|
||||
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
|
||||
if (_find) {
|
||||
return _find.choice_value
|
||||
if (_find?.choice_value?.length) {
|
||||
return _find.choice_value.map((node) => ({
|
||||
id: String(node?.[0] ?? ''),
|
||||
label: node?.[1]?.label || node?.[0] || '',
|
||||
children: node?.children?.length ? node.children : undefined
|
||||
}))
|
||||
}
|
||||
return []
|
||||
},
|
||||
|
@@ -207,6 +207,13 @@ export function deleteTrigger(type_id, id) {
|
||||
})
|
||||
}
|
||||
|
||||
export function testTrigger(type_id, id) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${type_id}/triggers/${id}/test_notify`,
|
||||
method: 'post',
|
||||
})
|
||||
}
|
||||
|
||||
// CMDB的模型和实例的授权接口
|
||||
export function grantCiType(type_id, rid, data) {
|
||||
return axios({
|
||||
|
93
cmdb-ui/src/modules/cmdb/api/dcim.js
Normal file
@@ -0,0 +1,93 @@
|
||||
import { axios } from '@/utils/request'
|
||||
|
||||
export function getDCIMTreeView(params) {
|
||||
return axios({
|
||||
url: '/v0.1/dcim/tree_view ',
|
||||
method: 'GET',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getDCIMById(type, id) {
|
||||
return axios({
|
||||
url: `/v0.1/dcim/${type}/${id}`,
|
||||
method: 'GET'
|
||||
})
|
||||
}
|
||||
|
||||
export function postDCIM(type, data) {
|
||||
return axios({
|
||||
url: `/v0.1/dcim/${type}`,
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function putDCIM(type, id, data) {
|
||||
return axios({
|
||||
url: `/v0.1/dcim/${type}/${id}`,
|
||||
method: 'PUT',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteDCIM(type, id) {
|
||||
return axios({
|
||||
url: `/v0.1/dcim/${type}/${id}`,
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
|
||||
export function getDCIMRacks(id, params) {
|
||||
return axios({
|
||||
url: `/v0.1/dcim/server_room/${id}/racks`,
|
||||
method: 'GET',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function postDevice(rackId, deviceId, data) {
|
||||
return axios({
|
||||
url: `/v0.1/dcim/rack/${rackId}/device/${deviceId}`,
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteDevice(rackId, deviceId) {
|
||||
return axios({
|
||||
url: `/v0.1/dcim/rack/${rackId}/device/${deviceId}`,
|
||||
method: 'DELETE'
|
||||
})
|
||||
}
|
||||
|
||||
export function putDevice(rackId, deviceId, data) {
|
||||
return axios({
|
||||
url: `/v0.1/dcim/rack/${rackId}/device/${deviceId}`,
|
||||
method: 'PUT',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function migrateDevice(rackId, deviceId, data) {
|
||||
return axios({
|
||||
url: `/v0.1/dcim/rack/${rackId}/device/${deviceId}/migrate`,
|
||||
method: 'PUT',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function getDCIMHistoryOperate(params) {
|
||||
return axios({
|
||||
url: `/v0.1/dcim/history/operate`,
|
||||
method: 'GET',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function calcUnitFreeCount() {
|
||||
return axios({
|
||||
url: `/v0.1/dcim/rack/calc_u_free_count`,
|
||||
method: 'POST'
|
||||
})
|
||||
}
|
BIN
cmdb-ui/src/modules/cmdb/assets/dcim/dcim_null.png
Normal file
After Width: | Height: | Size: 136 KiB |
BIN
cmdb-ui/src/modules/cmdb/assets/dcim/device/firewall_front.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
cmdb-ui/src/modules/cmdb/assets/dcim/device/firewall_rear.png
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
cmdb-ui/src/modules/cmdb/assets/dcim/device/raid_front.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
cmdb-ui/src/modules/cmdb/assets/dcim/device/raid_rear.png
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
cmdb-ui/src/modules/cmdb/assets/dcim/device/router_front.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
cmdb-ui/src/modules/cmdb/assets/dcim/device/router_rear.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
cmdb-ui/src/modules/cmdb/assets/dcim/device/server_front.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
cmdb-ui/src/modules/cmdb/assets/dcim/device/server_rear.png
Normal file
After Width: | Height: | Size: 70 KiB |
BIN
cmdb-ui/src/modules/cmdb/assets/dcim/device/switch_front.png
Normal file
After Width: | Height: | Size: 7.9 KiB |
BIN
cmdb-ui/src/modules/cmdb/assets/dcim/device/switch_rear.png
Normal file
After Width: | Height: | Size: 55 KiB |
BIN
cmdb-ui/src/modules/cmdb/assets/dcim/device_2.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
cmdb-ui/src/modules/cmdb/assets/dcim/rack.png
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
cmdb-ui/src/modules/cmdb/assets/dcim/rack_front_part.png
Normal file
After Width: | Height: | Size: 554 KiB |
BIN
cmdb-ui/src/modules/cmdb/assets/dcim/rack_rear_part.png
Normal file
After Width: | Height: | Size: 13 KiB |
@@ -15,7 +15,7 @@
|
||||
highlight-hover-row
|
||||
:checkbox-config="{ reserve: true, highlight: true, range: true }"
|
||||
:edit-config="{ trigger: 'dblclick', mode: 'row', showIcon: false }"
|
||||
:sort-config="{ remote: true, trigger: 'cell' }"
|
||||
:sort-config="sortConfig"
|
||||
:row-key="true"
|
||||
:column-key="true"
|
||||
:cell-style="getCellStyle"
|
||||
@@ -170,7 +170,7 @@
|
||||
</template>
|
||||
</template>
|
||||
</vxe-table-column>
|
||||
<vxe-column align="left" field="operate" fixed="right" width="80">
|
||||
<vxe-column v-if="showOperation" align="left" field="operate" fixed="right" width="80">
|
||||
<template #header>
|
||||
<span>{{ $t('operation') }}</span>
|
||||
</template>
|
||||
@@ -272,6 +272,18 @@ export default {
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
sortConfig: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
remote: true,
|
||||
trigger: 'cell'
|
||||
})
|
||||
},
|
||||
// 是否展示操作列
|
||||
showOperation: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -25,7 +25,8 @@ const cmdb_en = {
|
||||
relationType: 'Relation Type',
|
||||
ad: 'AutoDiscovery',
|
||||
cidetail: 'CI Detail',
|
||||
scene: 'Scene'
|
||||
scene: 'Scene',
|
||||
dcim: 'DCIM'
|
||||
},
|
||||
ciType: {
|
||||
ciType: 'CIType',
|
||||
@@ -185,6 +186,9 @@ const cmdb_en = {
|
||||
botSelect: 'Please select a robot',
|
||||
refAttributeTips: 'The title and content can reference the attribute value of the CIType. The reference method is: {{ attr_name }}',
|
||||
webhookRefAttributeTips: 'Request parameters can reference the attribute value of the model. The reference method is: {{ attr_name }}',
|
||||
testSend: 'Test Send',
|
||||
testSendTip: 'Please save the trigger first',
|
||||
testSendSuccess: 'Send Success',
|
||||
newTrigger: 'Add trigger',
|
||||
editTriggerTitle: 'Edit trigger {name}',
|
||||
newTriggerTitle: 'Add trigger {name}',
|
||||
@@ -837,7 +841,67 @@ if __name__ == "__main__":
|
||||
onlineRatio: 'Online Ratio',
|
||||
scanEnable: 'Scan Enable',
|
||||
lastScanTime: 'Last Scan Time',
|
||||
isSuccess: 'Is Success'
|
||||
isSuccess: 'Is Success',
|
||||
batchAssign: 'Batch Assign',
|
||||
batchAssignInProgress: 'Assign in batches, {total} in total, {successNum} successful, {errorNum} failed',
|
||||
batchAssignCompleted: 'Batch Assign Completed',
|
||||
batchRecycle: 'Batch Recycle',
|
||||
batchRecycleInProgress: 'Recycle in batches, {total} in total, {successNum} successful, {errorNum} failed',
|
||||
batchRecycleCompleted: 'Batch Recycle Completed',
|
||||
},
|
||||
dcim: {
|
||||
addRegion: 'Add Region',
|
||||
addIDC: 'Add IDC',
|
||||
addServerRoom: 'Add Server Room',
|
||||
addRack: 'Add Rack',
|
||||
editRegion: 'Edit Region',
|
||||
editIDC: 'Edit IDC',
|
||||
editServerRoom: 'Edit Server Room',
|
||||
editRack: 'Edit Rack',
|
||||
rackCount: 'Rack Count',
|
||||
total: 'Total',
|
||||
deviceCount: 'Device Count',
|
||||
utilizationRation: 'Utilization Ration',
|
||||
used: 'Used',
|
||||
unused: 'Unused',
|
||||
rackSearchTip: 'Please search for rack name',
|
||||
viewDetail: 'View Detail',
|
||||
deleteNode: 'Delete Node',
|
||||
editNode: 'Edit Node',
|
||||
roomNullTip: 'Please select the server room on the left first',
|
||||
unitAbnormal: 'Unit Abnormal',
|
||||
rack: 'Rack',
|
||||
unitCount: 'Unit Count',
|
||||
rackView: 'Rack View',
|
||||
deviceList: 'Device List',
|
||||
operationLog: 'Operation Log',
|
||||
frontView: 'Front View',
|
||||
rearView: 'Rear View',
|
||||
addDevice: 'Add Device',
|
||||
device: 'Device',
|
||||
ciType: 'CIType',
|
||||
unitStart: 'Unit Start',
|
||||
toChange: 'To Change',
|
||||
abnormalModalTip1: 'and',
|
||||
abnormalModalTip2: ' location duplication',
|
||||
abnormalModalTip3: 'Please select one of the devices to change',
|
||||
remove: 'Remove',
|
||||
migrate: 'Migrate',
|
||||
deviceMigrate: 'Device Migrate',
|
||||
migrationSuccess: 'Migration Success',
|
||||
removeDeviceTip: 'Confirmed to remove {deviceName} device?',
|
||||
operationTime: 'Operation Time',
|
||||
operationUser: 'Operation User',
|
||||
operationType: 'Operation Type',
|
||||
deviceType: 'Device Type',
|
||||
deviceName: 'Device Name',
|
||||
removeDevice: 'Remove Device',
|
||||
moveDevice: 'Move Device',
|
||||
rackDetail: 'Rack Detail',
|
||||
calcUnitFreeCount: 'Calculate Rack Free Unit Count',
|
||||
calcUnitFreeCountTip: 'Calculating in the background, refresh the page later to see the result',
|
||||
calcUnitFreeCountTip1: 'Calculate Trigger Success, refresh the page later to see the result',
|
||||
calcUnitFreeCountTip2: `Confirm that you want to calculate the number of free Units for all rack?`
|
||||
}
|
||||
}
|
||||
export default cmdb_en
|
||||
|
@@ -25,7 +25,8 @@ const cmdb_zh = {
|
||||
relationType: '关系类型',
|
||||
ad: '自动发现',
|
||||
cidetail: 'CI 详情',
|
||||
scene: '场景'
|
||||
scene: '场景',
|
||||
dcim: '数据中心'
|
||||
},
|
||||
ciType: {
|
||||
ciType: '模型',
|
||||
@@ -185,6 +186,9 @@ const cmdb_zh = {
|
||||
botSelect: '请选择机器人',
|
||||
refAttributeTips: '标题、内容可以引用该模型的属性值,引用方法为: {{ attr_name }}',
|
||||
webhookRefAttributeTips: '请求参数可以引用该模型的属性值,引用方法为: {{ attr_name }}',
|
||||
testSend: '测试发送',
|
||||
testSendTip: '请先保存触发器',
|
||||
testSendSuccess: '发送成功',
|
||||
newTrigger: '新增触发器',
|
||||
editTriggerTitle: '编辑触发器 {name}',
|
||||
newTriggerTitle: '新增触发器 {name}',
|
||||
@@ -836,7 +840,67 @@ if __name__ == "__main__":
|
||||
onlineRatio: '在线率',
|
||||
scanEnable: '是否扫描',
|
||||
lastScanTime: '最后扫描时间',
|
||||
isSuccess: '是否成功'
|
||||
isSuccess: '是否成功',
|
||||
batchAssign: '批量分配',
|
||||
batchAssignInProgress: '正在批量分配,共{total}个,成功{successNum}个,失败{errorNum}个',
|
||||
batchAssignCompleted: '批量分配已完成',
|
||||
batchRecycle: '批量回收',
|
||||
batchRecycleInProgress: '正在批量回收,共{total}个,成功{successNum}个,失败{errorNum}个',
|
||||
batchRecycleCompleted: '批量回收已完成',
|
||||
},
|
||||
dcim: {
|
||||
addRegion: '新增区域',
|
||||
addIDC: '新增数据中心',
|
||||
addServerRoom: '新增机房',
|
||||
addRack: '添加机柜',
|
||||
editRegion: '编辑区域',
|
||||
editIDC: '编辑数据中心',
|
||||
editServerRoom: '编辑机房',
|
||||
editRack: '编辑机柜',
|
||||
rackCount: '机柜数',
|
||||
total: '总数',
|
||||
deviceCount: '设备数',
|
||||
utilizationRation: '利用率',
|
||||
used: '已使用',
|
||||
unused: '未使用',
|
||||
rackSearchTip: '请搜索机柜名称',
|
||||
viewDetail: '查看详情',
|
||||
deleteNode: '删除节点',
|
||||
editNode: '编辑节点',
|
||||
roomNullTip: '请先选择左侧的机房',
|
||||
unitAbnormal: 'U位异常',
|
||||
rack: '机柜',
|
||||
unitCount: 'U位数',
|
||||
rackView: '机柜视图',
|
||||
deviceList: '设备列表',
|
||||
operationLog: '操作记录',
|
||||
frontView: '前视图',
|
||||
rearView: '后视图',
|
||||
addDevice: '添加设备',
|
||||
device: '设备',
|
||||
ciType: '模型',
|
||||
unitStart: '起始U位',
|
||||
toChange: '去修改',
|
||||
abnormalModalTip1: '和',
|
||||
abnormalModalTip2: ' 位置重复',
|
||||
abnormalModalTip3: '请选择其中一台设备进行修改',
|
||||
remove: '移除',
|
||||
migrate: '迁移',
|
||||
deviceMigrate: '设备迁移',
|
||||
migrationSuccess: '迁移成功',
|
||||
removeDeviceTip: '确认要移除 {deviceName} 设备吗?',
|
||||
operationTime: '操作时间',
|
||||
operationUser: '操作人',
|
||||
operationType: '操作类型',
|
||||
deviceType: '设备类型',
|
||||
deviceName: '设备名',
|
||||
removeDevice: '删除设备',
|
||||
moveDevice: '移动设备',
|
||||
rackDetail: '机柜详情',
|
||||
calcUnitFreeCount: '计算机柜空闲U数',
|
||||
calcUnitFreeCountTip: '后台计算中,稍后刷新页面查看结果',
|
||||
calcUnitFreeCountTip1: '计算触发成功,稍后刷新页面查看结果',
|
||||
calcUnitFreeCountTip2: '确认要计算所有机柜的空闲U数?'
|
||||
}
|
||||
}
|
||||
export default cmdb_zh
|
||||
|
@@ -81,6 +81,12 @@ const genCmdbRoutes = async () => {
|
||||
name: 'cmdb_ipam',
|
||||
meta: { title: 'IPAM', appName: 'cmdb', icon: 'veops-ipam', selectedIcon: 'veops-ipam', keepAlive: false, permission: ['admin', 'cmdb_admin'] }
|
||||
},
|
||||
{
|
||||
path: '/cmdb/dcim',
|
||||
component: () => import('../views/dcim'),
|
||||
name: 'cmdb_dcim',
|
||||
meta: { title: 'cmdb.menu.dcim', appName: 'cmdb', icon: 'veops-data_center', selectedIcon: 'veops-data_center', keepAlive: false, permission: ['cmdb_admin', 'admin'] }
|
||||
},
|
||||
{
|
||||
path: '/cmdb/disabled2',
|
||||
name: 'cmdb_disabled2',
|
||||
|
@@ -178,7 +178,7 @@
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
<a v-if="!isEdit && !attr.is_computed && !attr.sys_computed" @click="handleEdit" :style="{ opacity: 0 }"><a-icon type="edit"/></a>
|
||||
<a v-if="!isEdit && !attr.is_computed && !attr.sys_computed && showEdit" @click="handleEdit" :style="{ opacity: 0 }"><a-icon type="edit"/></a>
|
||||
<JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" />
|
||||
</span>
|
||||
</template>
|
||||
@@ -204,6 +204,10 @@ export default {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
showEdit: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@@ -45,7 +45,7 @@
|
||||
:href="`/cmdb/cidetail/${column.params.attr.reference_type_id}/${id}`"
|
||||
target="_blank"
|
||||
>
|
||||
{{ id }}
|
||||
{{ getReferenceName(id, column) }}
|
||||
</a>
|
||||
</template>
|
||||
<template #operation_default="{ row }">
|
||||
@@ -102,7 +102,7 @@
|
||||
:href="`/cmdb/cidetail/${column.params.attr.reference_type_id}/${id}`"
|
||||
target="_blank"
|
||||
>
|
||||
{{ id }}
|
||||
{{ getReferenceName(id, column) }}
|
||||
</a>
|
||||
</template>
|
||||
<template #operation_default="{ row }">
|
||||
@@ -133,9 +133,11 @@
|
||||
import _ from 'lodash'
|
||||
import { getCITypeChildren, getCITypeParent, getCanEditByParentIdChildId } from '@/modules/cmdb/api/CITypeRelation'
|
||||
import { searchCIRelation, deleteCIRelationView } from '@/modules/cmdb/api/CIRelation'
|
||||
import { searchCI } from '@/modules/cmdb/api/ci'
|
||||
import CiDetailRelationTopo from './ciDetailRelationTopo/index.vue'
|
||||
import Node from './ciDetailRelationTopo/node.js'
|
||||
import AddTableModal from '../../relation_views/modules/AddTableModal.vue'
|
||||
|
||||
export default {
|
||||
name: 'CiDetailRelation',
|
||||
components: { CiDetailRelationTopo, AddTableModal },
|
||||
@@ -172,7 +174,8 @@ export default {
|
||||
topoData: {
|
||||
nodes: {},
|
||||
edges: []
|
||||
}
|
||||
},
|
||||
referenceCINameMap: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -211,9 +214,14 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
async init(isFirst) {
|
||||
const ci_types_list = this.ci_types()
|
||||
const _findCiType = ci_types_list.find((item) => item.id === this.typeId)
|
||||
if (!_findCiType) {
|
||||
return
|
||||
}
|
||||
|
||||
await Promise.all([this.getParentCITypes(), this.getChildCITypes()])
|
||||
Promise.all([this.getFirstCIs(), this.getSecondCIs()]).then(() => {
|
||||
const ci_types_list = this.ci_types()
|
||||
this.handleTopoData()
|
||||
if (
|
||||
isFirst &&
|
||||
@@ -223,6 +231,8 @@ export default {
|
||||
this.$refs.ciDetailRelationTopo.exsited_ci = this.exsited_ci
|
||||
this.$refs.ciDetailRelationTopo.setTopoData(this.topoData)
|
||||
}
|
||||
|
||||
this.handleReferenceCINameMap()
|
||||
})
|
||||
},
|
||||
async getFirstCIs() {
|
||||
@@ -394,6 +404,98 @@ export default {
|
||||
this.secondCIColumns = secondCIColumns
|
||||
this.secondCIJsonAttr = secondCIJsonAttr
|
||||
},
|
||||
|
||||
async handleReferenceCINameMap() {
|
||||
const CITypes = _.unionBy(
|
||||
[
|
||||
...this.parentCITypes,
|
||||
...this.childCITypes
|
||||
],
|
||||
'id'
|
||||
)
|
||||
const CIList = _.unionBy(
|
||||
_.flatten(
|
||||
[
|
||||
...Object.values(this.firstCIs),
|
||||
...Object.values(this.secondCIs)
|
||||
]
|
||||
),
|
||||
'_id'
|
||||
)
|
||||
|
||||
const CIMap = {}
|
||||
CIList.forEach((ci) => {
|
||||
if (!CIMap[ci._type]) {
|
||||
CIMap[ci._type] = []
|
||||
}
|
||||
CIMap[ci._type].push(ci)
|
||||
})
|
||||
|
||||
const referenceCINameMap = {}
|
||||
CITypes.forEach((CIType) => {
|
||||
CIType.attributes.forEach((attr) => {
|
||||
if (attr?.is_reference && attr?.reference_type_id) {
|
||||
const currentCIList = CIMap[CIType.id]
|
||||
if (currentCIList?.length) {
|
||||
currentCIList.forEach((ci) => {
|
||||
const ids = Array.isArray(ci[attr.name]) ? ci[attr.name] : ci[attr.name] ? [ci[attr.name]] : []
|
||||
|
||||
if (ids.length) {
|
||||
if (!referenceCINameMap?.[attr.reference_type_id]) {
|
||||
referenceCINameMap[attr.reference_type_id] = {}
|
||||
}
|
||||
ids.forEach((id) => {
|
||||
referenceCINameMap[attr.reference_type_id][id] = ''
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (!Object.keys(referenceCINameMap).length) {
|
||||
return
|
||||
}
|
||||
|
||||
const allRes = await Promise.all(
|
||||
Object.keys(referenceCINameMap).map((key) => {
|
||||
return searchCI({
|
||||
q: `_type:${key},_id:(${Object.keys(referenceCINameMap[key]).join(';')})`,
|
||||
count: 9999
|
||||
})
|
||||
})
|
||||
)
|
||||
const CITypeList = this.ci_types()
|
||||
const showNameMap = {}
|
||||
|
||||
Object.keys(referenceCINameMap).forEach((id) => {
|
||||
const CIType = CITypeList.find((CIType) => Number(CIType.id) === Number(id))
|
||||
|
||||
showNameMap[id] = {
|
||||
show_name: CIType?.show_name,
|
||||
unique_key: CIType?.unique_key
|
||||
}
|
||||
})
|
||||
|
||||
allRes.forEach((res) => {
|
||||
res.result.forEach((item) => {
|
||||
if (referenceCINameMap?.[item._type]?.[item._id] === '') {
|
||||
const showName = showNameMap?.[item._type]
|
||||
|
||||
referenceCINameMap[item._type][item._id] = item?.[showName?.show_name] ?? item?.[showName?.unique_key] ?? ''
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
this.referenceCINameMap = referenceCINameMap
|
||||
},
|
||||
|
||||
getReferenceName(id, column) {
|
||||
const typeId = column?.params?.attr?.reference_type_id
|
||||
return this.referenceCINameMap?.[typeId]?.[id] || id
|
||||
},
|
||||
|
||||
reload() {
|
||||
this.init()
|
||||
},
|
||||
|
@@ -688,19 +688,19 @@ export default {
|
||||
font-size: 12px;
|
||||
color: #A5A9BC;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.ops-stripe-table .vxe-body--row.row--stripe.relation-table-divider {
|
||||
background-color: #b1b8d3 !important;
|
||||
}
|
||||
.ops-stripe-table .vxe-body--row.relation-table-parent {
|
||||
background-color: #f5f8ff !important;
|
||||
}
|
||||
.relation-table-divider {
|
||||
td {
|
||||
height: 1px !important;
|
||||
line-height: 1px !important;
|
||||
.ops-stripe-table {
|
||||
/deep/ .relation-table-divider {
|
||||
background-color: #b1b8d3 !important;
|
||||
|
||||
td {
|
||||
height: 2px !important;
|
||||
line-height: 2px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/deep/ .relation-table-parent {
|
||||
background-color: #f5f8ff !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -242,6 +242,18 @@
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-checkbox-group>
|
||||
|
||||
<a-row v-if="category === 2">
|
||||
<a-button
|
||||
@click="clickTestSend"
|
||||
:disabled="!dateForm.attr_id"
|
||||
type="primary"
|
||||
ghost
|
||||
class="ops-button-ghost"
|
||||
>
|
||||
{{ $t('cmdb.ciType.testSend') }}
|
||||
</a-button>
|
||||
</a-row>
|
||||
</a-form-model-item>
|
||||
</a-form-model>
|
||||
<div class="auto-complete-wrapper" v-if="triggerAction === '3'">
|
||||
@@ -273,7 +285,7 @@
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { addTrigger, updateTrigger, deleteTrigger } from '../../api/CIType'
|
||||
import { addTrigger, updateTrigger, deleteTrigger, testTrigger } from '../../api/CIType'
|
||||
import FilterComp from '@/components/CMDBFilterComp'
|
||||
import EmployeeTreeSelect from '@/views/setting/components/employeeTreeSelect.vue'
|
||||
import Webhook from '../../components/webhook'
|
||||
@@ -569,10 +581,13 @@ export default {
|
||||
}
|
||||
if (this.triggerId) {
|
||||
await updateTrigger(this.CITypeId, this.triggerId, params)
|
||||
this.$message.success(this.$t('editSuccess'))
|
||||
} else {
|
||||
await addTrigger(this.CITypeId, params)
|
||||
const res = await addTrigger(this.CITypeId, params)
|
||||
this.triggerId = res.id
|
||||
this.$message.success(this.$t('createSuccess'))
|
||||
}
|
||||
this.handleCancel()
|
||||
|
||||
if (this.refresh) {
|
||||
this.refresh()
|
||||
}
|
||||
@@ -614,6 +629,15 @@ export default {
|
||||
this.searchValue = item.label
|
||||
this.dag_id = item.id
|
||||
},
|
||||
async clickTestSend() {
|
||||
if (!this.triggerId) {
|
||||
this.$message.warning(this.$t('cmdb.ciType.testSendTip'))
|
||||
return
|
||||
}
|
||||
|
||||
await testTrigger(this.CITypeId, this.triggerId)
|
||||
this.$message.success(this.$t('cmdb.ciType.testSendSuccess'))
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
331
cmdb-ui/src/modules/cmdb/views/dcim/components/dcimForm.vue
Normal file
@@ -0,0 +1,331 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
:width="700"
|
||||
:title="$t(modalTitle)"
|
||||
:confirmLoading="confirmLoading"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form-model
|
||||
ref="dcimFormRef"
|
||||
:model="form"
|
||||
:rules="formRules"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
class="dcim-form"
|
||||
>
|
||||
<a-form-model-item
|
||||
v-for="(item) in formList"
|
||||
:key="item.name"
|
||||
:label="item.alias || item.name"
|
||||
:prop="item.name"
|
||||
>
|
||||
<CIReferenceAttr
|
||||
v-if="item.is_reference"
|
||||
:referenceTypeId="item.reference_type_id"
|
||||
:isList="item.is_list"
|
||||
:referenceShowAttrName="item.showAttrName"
|
||||
:initSelectOption="getInitReferenceSelectOption(item)"
|
||||
v-model="form[item.name]"
|
||||
/>
|
||||
<a-select
|
||||
v-else-if="item.is_choice"
|
||||
:mode="item.is_list ? 'multiple' : 'default'"
|
||||
showSearch
|
||||
allowClear
|
||||
v-model="form[item.name]"
|
||||
>
|
||||
<a-icon slot="suffixIcon" type="caret-down" />
|
||||
<a-select-option
|
||||
v-for="(choiceItem, choiceIndex) in item.selectOption"
|
||||
:key="choiceIndex"
|
||||
:value="choiceItem.value"
|
||||
>
|
||||
{{ choiceItem.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<a-switch
|
||||
v-else-if="item.is_bool"
|
||||
v-model="form[item.name]"
|
||||
/>
|
||||
|
||||
<a-input-number
|
||||
v-model="form[item.name]"
|
||||
class="dcim-form-input"
|
||||
v-else-if="(item.value_type === '0' || item.value_type === '1') && !item.is_list"
|
||||
/>
|
||||
|
||||
<a-date-picker
|
||||
v-else-if="(item.value_type === '4' || item.value_type === '3') && !item.is_list"
|
||||
v-model="form[item.name]"
|
||||
class="dcim-form-input"
|
||||
:format="item.value_type === '4' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'"
|
||||
:valueFormat="item.value_type === '4' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'"
|
||||
:showTime="item.value_type === '4' ? false : { format: 'HH:mm:ss' }"
|
||||
/>
|
||||
|
||||
<a-input
|
||||
v-else
|
||||
:placeholder="$t('placeholder1')"
|
||||
v-model="form[item.name]"
|
||||
/>
|
||||
</a-form-model-item>
|
||||
</a-form-model>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { postDCIM, putDCIM } from '@/modules/cmdb/api/dcim.js'
|
||||
import { getCITypes } from '@/modules/cmdb/api/CIType'
|
||||
import { searchCI } from '@/modules/cmdb/api/ci'
|
||||
import { DCIM_TYPE } from '../constants'
|
||||
|
||||
import CIReferenceAttr from '@/components/ciReferenceAttr/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'DCIMForm',
|
||||
components: {
|
||||
CIReferenceAttr
|
||||
},
|
||||
props: {
|
||||
allAttrList: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
nodeId: null,
|
||||
parentId: null,
|
||||
dcimType: '',
|
||||
|
||||
formList: [],
|
||||
form: {},
|
||||
formRules: {},
|
||||
|
||||
confirmLoading: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
modalTitle() {
|
||||
switch (this.dcimType) {
|
||||
case DCIM_TYPE.REGION:
|
||||
return this.nodeId ? 'cmdb.dcim.editRegion' : 'cmdb.dcim.addRegion'
|
||||
case DCIM_TYPE.IDC:
|
||||
return this.nodeId ? 'cmdb.dcim.editIDC' : 'cmdb.dcim.addIDC'
|
||||
case DCIM_TYPE.SERVER_ROOM:
|
||||
return this.nodeId ? 'cmdb.dcim.editServerRoom' : 'cmdb.dcim.addServerRoom'
|
||||
case DCIM_TYPE.RACK:
|
||||
return this.nodeId ? 'cmdb.dcim.editRack' : 'cmdb.dcim.addRack'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async open({
|
||||
nodeId = null,
|
||||
parentId = null,
|
||||
dcimType = ''
|
||||
}) {
|
||||
this.nodeId = nodeId
|
||||
|
||||
let nodeData = {}
|
||||
if (nodeId) {
|
||||
const res = await searchCI({
|
||||
q: `_id:${nodeId}`,
|
||||
count: 9999
|
||||
})
|
||||
nodeData = res?.result?.[0] || {}
|
||||
}
|
||||
|
||||
this.parentId = parentId
|
||||
this.dcimType = dcimType
|
||||
this.visible = true
|
||||
|
||||
const form = {}
|
||||
const formRules = {}
|
||||
let formList = []
|
||||
|
||||
let attrList = _.cloneDeep(this.allAttrList?.[dcimType]?.attributes)
|
||||
attrList = attrList?.filter?.((attr) => !attr.sys_computed && !attr.is_computed) || []
|
||||
|
||||
if (attrList.length) {
|
||||
attrList.forEach((attr) => {
|
||||
let value = nodeData?.[attr.name] ?? attr?.default?.default ?? undefined
|
||||
|
||||
if (
|
||||
Array.isArray(value) &&
|
||||
['0', '1', '2', '9'].includes(attr.value_type)
|
||||
) {
|
||||
value = value.join(',')
|
||||
}
|
||||
form[attr.name] = value
|
||||
|
||||
if (attr?.is_choice) {
|
||||
const choice_value = attr?.choice_value || []
|
||||
attr.selectOption = choice_value.map((item) => {
|
||||
return {
|
||||
label: item?.[1]?.label || item?.[0] || '',
|
||||
value: item?.[0]
|
||||
}
|
||||
})
|
||||
}
|
||||
formList.push(attr)
|
||||
|
||||
if (attr.is_required) {
|
||||
formRules[attr.name] = [
|
||||
{
|
||||
required: true, message: attr?.is_choice ? this.$t('placeholder2') : this.$t('placeholder1')
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
formList = await this.handleReferenceAttr(formList, form)
|
||||
|
||||
this.form = form
|
||||
this.formList = formList
|
||||
this.formRules = formRules
|
||||
},
|
||||
|
||||
async handleReferenceAttr(formList, ci) {
|
||||
const map = {}
|
||||
formList.forEach((attr) => {
|
||||
if (attr?.is_reference && attr?.reference_type_id && ci[attr.name]) {
|
||||
const ids = Array.isArray(ci[attr.name]) ? ci[attr.name] : ci[attr.name] ? [ci[attr.name]] : []
|
||||
if (ids.length) {
|
||||
if (!map?.[attr.reference_type_id]) {
|
||||
map[attr.reference_type_id] = {}
|
||||
}
|
||||
ids.forEach((id) => {
|
||||
map[attr.reference_type_id][id] = {}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
if (!Object.keys(map).length) {
|
||||
return formList
|
||||
}
|
||||
|
||||
const ciTypesRes = await getCITypes({
|
||||
type_ids: Object.keys(map).join(',')
|
||||
})
|
||||
const showAttrNameMap = {}
|
||||
ciTypesRes.ci_types.forEach((ciType) => {
|
||||
showAttrNameMap[ciType.id] = ciType?.show_name || ciType?.unique_name || ''
|
||||
})
|
||||
|
||||
const allRes = await Promise.all(
|
||||
Object.keys(map).map((key) => {
|
||||
return searchCI({
|
||||
q: `_type:${key},_id:(${Object.keys(map[key]).join(';')})`,
|
||||
count: 9999
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
const ciNameMap = {}
|
||||
allRes.forEach((res) => {
|
||||
res.result.forEach((item) => {
|
||||
ciNameMap[item._id] = item
|
||||
})
|
||||
})
|
||||
|
||||
formList.forEach((attr) => {
|
||||
if (attr?.is_reference && attr?.reference_type_id) {
|
||||
attr.showAttrName = showAttrNameMap?.[attr?.reference_type_id] || ''
|
||||
|
||||
const referenceShowAttrNameMap = {}
|
||||
const referenceCIIds = ci[attr.name];
|
||||
(Array.isArray(referenceCIIds) ? referenceCIIds : referenceCIIds ? [referenceCIIds] : []).forEach((id) => {
|
||||
referenceShowAttrNameMap[id] = ciNameMap?.[id]?.[attr.showAttrName] ?? id
|
||||
})
|
||||
attr.referenceShowAttrNameMap = referenceShowAttrNameMap
|
||||
}
|
||||
})
|
||||
|
||||
return formList
|
||||
},
|
||||
|
||||
handleCancel() {
|
||||
this.visible = false
|
||||
this.nodeId = null
|
||||
this.parentId = null
|
||||
this.dcimType = ''
|
||||
this.form = {}
|
||||
this.formRules = {}
|
||||
this.formList = []
|
||||
this.confirmLoading = false
|
||||
|
||||
this.$refs.dcimFormRef.clearValidate()
|
||||
},
|
||||
|
||||
handleOk() {
|
||||
this.$refs.dcimFormRef.validate(async (valid) => {
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
this.confirmLoading = true
|
||||
|
||||
try {
|
||||
if (this.nodeId) {
|
||||
await putDCIM(
|
||||
this.dcimType,
|
||||
this.nodeId,
|
||||
{
|
||||
...this.form,
|
||||
parent_id: Number(this.parentId)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
await postDCIM(
|
||||
this.dcimType,
|
||||
{
|
||||
...this.form,
|
||||
parent_id: Number(this.parentId)
|
||||
}
|
||||
)
|
||||
}
|
||||
this.$emit('ok', {
|
||||
dcimType: this.dcimType,
|
||||
editType: this.nodeId ? 'edit' : 'create'
|
||||
})
|
||||
this.handleCancel()
|
||||
} catch (error) {
|
||||
console.log('submit fail', error)
|
||||
}
|
||||
|
||||
this.confirmLoading = false
|
||||
})
|
||||
},
|
||||
|
||||
getInitReferenceSelectOption(attr) {
|
||||
const option = Object.keys(attr?.referenceShowAttrNameMap || {}).map((key) => {
|
||||
return {
|
||||
key: Number(key),
|
||||
title: attr?.referenceShowAttrNameMap?.[Number(key)] ?? ''
|
||||
}
|
||||
})
|
||||
return option
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.dcim-form {
|
||||
padding-right: 12px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
&-input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,184 @@
|
||||
<template>
|
||||
<div class="dcim-stats">
|
||||
<div
|
||||
v-for="(item, index) in statsList"
|
||||
:key="index"
|
||||
class="dcim-stats-card"
|
||||
>
|
||||
<div class="dcim-stats-card-left">
|
||||
<div class="dcim-stat-card-title">{{ $t(item.title) }}</div>
|
||||
|
||||
<div class="dcim-stats-card-row">
|
||||
<div
|
||||
v-for="(data, dataIndex) in item.countList"
|
||||
:key="dataIndex"
|
||||
class="dcim-stats-card-count"
|
||||
>
|
||||
<span class="dcim-stats-card-count-label">{{ $t(data.label) }}:</span>
|
||||
<span class="dcim-stats-card-count-value">{{ data.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="item.icon"
|
||||
class="dcim-stats-card-icon"
|
||||
>
|
||||
<ops-icon
|
||||
:type="item.icon"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DCIMStatsChart
|
||||
v-else-if="item.chartData"
|
||||
:chartData="item.chartData"
|
||||
:chartRatio="item.chartRatio"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DCIMStatsChart from './dcimStatsChart.vue'
|
||||
|
||||
export default {
|
||||
name: 'DCIMStats',
|
||||
props: {
|
||||
statsData: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
components: {
|
||||
DCIMStatsChart
|
||||
},
|
||||
computed: {
|
||||
statsList() {
|
||||
const {
|
||||
device_count = 0,
|
||||
rack_count = 0,
|
||||
u_count = 0,
|
||||
u_used_count = 0,
|
||||
} = this.statsData || {}
|
||||
|
||||
return [
|
||||
{
|
||||
title: 'cmdb.dcim.rackCount',
|
||||
icon: 'veops-cabinet',
|
||||
countList: [
|
||||
{
|
||||
label: 'cmdb.dcim.total',
|
||||
value: rack_count
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'cmdb.dcim.deviceCount',
|
||||
icon: 'veops-device',
|
||||
countList: [
|
||||
{
|
||||
label: 'cmdb.dcim.total',
|
||||
value: device_count
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'cmdb.dcim.utilizationRation',
|
||||
countList: [
|
||||
{
|
||||
label: 'cmdb.dcim.used',
|
||||
value: `${u_used_count}u`
|
||||
},
|
||||
{
|
||||
label: 'cmdb.dcim.unused',
|
||||
value: `${u_count - u_used_count}u`
|
||||
}
|
||||
],
|
||||
chartRatio: u_used_count > 0 && u_count > 0 ? Math.round((u_used_count / u_count) * 100) : 0,
|
||||
chartData: [
|
||||
{
|
||||
label: 'cmdb.dcim.used',
|
||||
value: u_used_count,
|
||||
color: '#009FA9'
|
||||
},
|
||||
{
|
||||
label: 'cmdb.dcim.unused',
|
||||
value: u_count - u_used_count,
|
||||
color: '#17D4B0'
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.dcim-stats {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
column-gap: 16px;
|
||||
flex-shrink: 0;
|
||||
|
||||
&-card {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 24px;
|
||||
background-color: #F7F8FA;
|
||||
filter: drop-shadow(0px 0px 12px rgba(231, 236, 239, 0.10));
|
||||
|
||||
&-left {
|
||||
width: 100%;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
&-title {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #4E5969;
|
||||
}
|
||||
|
||||
&-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 12px;
|
||||
column-gap: 12px;
|
||||
}
|
||||
|
||||
&-count {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
&-label {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #1D2129;
|
||||
}
|
||||
|
||||
&-value {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #1D2129;
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
&-icon {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 52px;
|
||||
background-color: #FFFFFF;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<div class="stats-chart">
|
||||
<div
|
||||
class="stats-chart-pie"
|
||||
ref="statsChartRef"
|
||||
></div>
|
||||
<div class="stats-chart-ratio">
|
||||
{{ chartRatio }}%
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
export default {
|
||||
name: 'DCIMStatsChart',
|
||||
props: {
|
||||
chartRatio: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
chartData: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
chartData: {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
handler(data) {
|
||||
this.updateChart(data)
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.chart) {
|
||||
this.chart.dispose()
|
||||
this.chart = null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateChart(data) {
|
||||
const option = {
|
||||
color: data?.map?.((item) => item.color) || [],
|
||||
tooltip: {
|
||||
show: false
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: ['60%', '85%'],
|
||||
data: data?.map((item) => {
|
||||
return {
|
||||
name: this.$t(item?.label),
|
||||
value: item?.value || 0
|
||||
}
|
||||
}) || [],
|
||||
itemStyle: {
|
||||
borderColor: '#fff',
|
||||
borderWidth: 1
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
if (!this.chart) {
|
||||
const el = this.$refs.statsChartRef
|
||||
this.chart = echarts.init(el)
|
||||
}
|
||||
this.chart.setOption(option)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.stats-chart {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
|
||||
&-pie {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&-ratio {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 2;
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #1D2129;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,376 @@
|
||||
<template>
|
||||
<div class="dcim-main" ref="rackMainRef">
|
||||
<div v-if="!roomId" class="dcim-main-null">
|
||||
<img class="dcim-main-null-img" :src="require(`@/modules/cmdb/assets/dcim/dcim_null.png`)"></img>
|
||||
<div class="dcim-main-null-tip">{{ $t('noData') }}</div>
|
||||
<div class="dcim-main-null-tip2">{{ $t('cmdb.dcim.roomNullTip') }}</div>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<DCIMStats :statsData="statsData" />
|
||||
|
||||
<div class="dcim-main-row">
|
||||
<div class="dcim-main-filter">
|
||||
<a-input-search
|
||||
v-model="searchValue"
|
||||
:placeholder="$t('cmdb.dcim.rackSearchTip')"
|
||||
class="dcim-main-row-search"
|
||||
/>
|
||||
|
||||
<a-select
|
||||
class="dcim-main-row-select"
|
||||
:getPopupContainer="(trigger) => trigger.parentElement"
|
||||
v-model="currentRackType"
|
||||
>
|
||||
<a-icon slot="suffixIcon" type="caret-down" />
|
||||
<a-select-option
|
||||
v-for="(item) in rackTypeSelectOption"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
:class="item.value === 'unitAbnormal' ? 'dcim-main-row-select-unitAbnormal' : ''"
|
||||
>
|
||||
{{ item.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
|
||||
<div class="dcim-main-row-right">
|
||||
<div class="dcim-main-layout">
|
||||
<div
|
||||
v-for="(item) in layoutList"
|
||||
:key="item.value"
|
||||
:class="['dcim-main-layout-item', currentLayout === item.value ?'dcim-main-layout-item-active' : '']"
|
||||
@click="handleChangeLayout(item.value)"
|
||||
>
|
||||
<ops-icon :type="item.icon" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-button
|
||||
type="primary"
|
||||
class="ops-button-ghost"
|
||||
ghost
|
||||
@click="addRack"
|
||||
>
|
||||
<a-icon type="plus-circle" />
|
||||
{{ $t('cmdb.dcim.addRack') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="rack-wrap"
|
||||
>
|
||||
<RackGrid
|
||||
v-if="currentLayout === 'grid'"
|
||||
:rackList="filterRackList"
|
||||
@openRackDetail="openRackDetail"
|
||||
/>
|
||||
|
||||
<RackTable
|
||||
v-if="currentLayout === 'table'"
|
||||
:rackList="filterRackList"
|
||||
:columns="columns"
|
||||
:preferenceAttrList="preferenceAttrList"
|
||||
:CITypeId="rackCITYpe.id"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<RackDetail
|
||||
ref="rackDetailRef"
|
||||
:roomId="roomId"
|
||||
:rackCITYpe="rackCITYpe"
|
||||
:rackList="rackList"
|
||||
@openForm="(data) => $emit('openForm', data)"
|
||||
@refreshRackList="getRackList"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { DCIM_TYPE } from '../../constants.js'
|
||||
import { getDCIMRacks } from '@/modules/cmdb/api/dcim.js'
|
||||
import { getCITableColumns } from '@/modules/cmdb/utils/helper'
|
||||
|
||||
import DCIMStats from './dcimStats.vue'
|
||||
import RackGrid from './rackGrid.vue'
|
||||
import RackTable from './rackTable.vue'
|
||||
import RackDetail from '../rackDetail/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'DCIMMain',
|
||||
components: {
|
||||
DCIMStats,
|
||||
RackGrid,
|
||||
RackTable,
|
||||
RackDetail
|
||||
},
|
||||
props: {
|
||||
roomId: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
attrObj: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
rackCITYpe: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
preferenceAttrList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchValue: '',
|
||||
currentRackType: 'all',
|
||||
rackList: [],
|
||||
columns: [],
|
||||
|
||||
statsData: {},
|
||||
|
||||
currentLayout: 'grid',
|
||||
layoutList: [
|
||||
{
|
||||
value: 'grid',
|
||||
icon: 'veops-map_view'
|
||||
},
|
||||
{
|
||||
value: 'table',
|
||||
icon: 'monitor-list_view'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
rackTypeSelectOption() {
|
||||
const selectOption = [
|
||||
{
|
||||
value: 'all',
|
||||
label: this.$t('all')
|
||||
}
|
||||
]
|
||||
|
||||
const rackTypeAttr = this.attrObj?.attributes?.find?.((item) => item.name === 'rack_type')
|
||||
if (rackTypeAttr?.choice_value?.length) {
|
||||
rackTypeAttr.choice_value.map((item) => {
|
||||
selectOption.push({
|
||||
value: item?.[0] || '',
|
||||
label: item?.[1]?.label || item?.[0] || ''
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
selectOption.push({
|
||||
value: 'unitAbnormal',
|
||||
label: this.$t('cmdb.dcim.unitAbnormal')
|
||||
})
|
||||
|
||||
return selectOption
|
||||
},
|
||||
filterRackList() {
|
||||
let rackList = _.cloneDeep(this.rackList)
|
||||
|
||||
if (this.searchValue) {
|
||||
rackList = rackList.filter((item) => item.name.indexOf(this.searchValue) !== -1)
|
||||
}
|
||||
|
||||
if (this.currentRackType !== 'all') {
|
||||
if (this.currentRackType === 'unitAbnormal') {
|
||||
rackList = rackList.filter((item) => item.u_slot_abnormal)
|
||||
} else {
|
||||
rackList = rackList.filter((item) => item.rack_type === this.currentRackType)
|
||||
}
|
||||
}
|
||||
|
||||
return rackList
|
||||
}
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
getRackList: this.getRackList,
|
||||
handleSearch: this.getRackList,
|
||||
attrList: () => {
|
||||
return this?.attrObj?.attributes || []
|
||||
},
|
||||
attributes: () => {
|
||||
return this.attrObj
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
roomId: {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
handler(id) {
|
||||
if (id) {
|
||||
this.initData()
|
||||
} else {
|
||||
this.rackList = []
|
||||
this.statsData = {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async initData() {
|
||||
try {
|
||||
await this.getRackList()
|
||||
} catch (error) {
|
||||
console.log('initData error', error)
|
||||
}
|
||||
},
|
||||
|
||||
async getRackList() {
|
||||
const res = await getDCIMRacks(this.roomId)
|
||||
const rackList = res?.result || []
|
||||
|
||||
const jsonAttrList = this.preferenceAttrList.filter((attr) => attr.value_type === '6')
|
||||
rackList.forEach((item) => {
|
||||
item.free_u_count = item.free_u_count ?? 0
|
||||
item.u_count = item.u_count ?? 0
|
||||
item.u_used_count = item.u_count - item.free_u_count
|
||||
item.u_used_ratio = item.u_used_count > 0 && item.u_count > 0 ? Math.round((item.u_used_count / item.u_count) * 100) : 0
|
||||
|
||||
jsonAttrList.forEach(
|
||||
(jsonAttr) => (item[jsonAttr.name] = item[jsonAttr.name] ? JSON.stringify(item[jsonAttr.name]) : '')
|
||||
)
|
||||
})
|
||||
|
||||
this.getColumns(rackList)
|
||||
|
||||
this.rackList = rackList
|
||||
this.statsData = res?.counter || {}
|
||||
},
|
||||
|
||||
getColumns(data) {
|
||||
const width = this.$refs.rackMainRef.clientWidth - 50
|
||||
const columns = getCITableColumns(data, this.preferenceAttrList, width)
|
||||
columns.forEach((item) => {
|
||||
if (item.editRender) {
|
||||
item.editRender.enabled = false
|
||||
}
|
||||
})
|
||||
this.columns = columns
|
||||
},
|
||||
|
||||
handleChangeLayout(value) {
|
||||
if (this.currentLayout !== value) {
|
||||
this.currentLayout = value
|
||||
}
|
||||
},
|
||||
|
||||
addRack() {
|
||||
this.$emit('openForm', {
|
||||
dcimType: DCIM_TYPE.RACK,
|
||||
parentId: this.roomId
|
||||
})
|
||||
},
|
||||
|
||||
openRackDetail(data) {
|
||||
this.$refs.rackDetailRef.open(data._id)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.dcim-main {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&-null {
|
||||
width: 100%;
|
||||
padding-top: 95px;
|
||||
text-align: center;
|
||||
|
||||
&-img {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
&-tip {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #86909C;
|
||||
}
|
||||
|
||||
&-tip2 {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #2F54EB;
|
||||
}
|
||||
}
|
||||
|
||||
&-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-shrink: 0;
|
||||
margin-top: 20px;
|
||||
|
||||
&-search {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
&-select {
|
||||
width: 120px;
|
||||
margin-left: 22px;
|
||||
flex-shrink: 0;
|
||||
|
||||
/deep/ &-unitAbnormal {
|
||||
border-top: dashed 1px #e8e8e8;
|
||||
}
|
||||
}
|
||||
|
||||
&-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 21px;
|
||||
}
|
||||
}
|
||||
|
||||
&-layout {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 32px;
|
||||
border: solid 1px #E4E7ED;
|
||||
|
||||
&-item {
|
||||
height: 100%;
|
||||
width: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-right: solid 1px #E4E7ED;
|
||||
}
|
||||
|
||||
&-active {
|
||||
color: #2F54EB;
|
||||
background-color: #F0F5FF;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #2F54EB;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rack-wrap {
|
||||
margin-top: 22px;
|
||||
margin-bottom: 22px;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,326 @@
|
||||
<template>
|
||||
<div class="rack-grid">
|
||||
<template v-if="rackList.length">
|
||||
<div
|
||||
v-for="(item, index) in rackList"
|
||||
:key="index"
|
||||
class="rack-grid-item"
|
||||
>
|
||||
<div
|
||||
v-if="item.u_slot_abnormal"
|
||||
class="rack-grid-item-warning"
|
||||
>
|
||||
<a-icon
|
||||
type="warning"
|
||||
theme="filled"
|
||||
class="rack-grid-item-warning-icon"
|
||||
/>
|
||||
<span
|
||||
class="rack-grid-item-warning-text"
|
||||
>
|
||||
{{ $t('cmdb.dcim.unitAbnormal') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="rack-grid-item-header">
|
||||
<a-tooltip :title="item.name">
|
||||
<div class="rack-grid-item-name">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</a-tooltip>
|
||||
<div class="rack-grid-item-store">
|
||||
{{ `${item.u_count || 0}U` }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<img
|
||||
class="rack-grid-item-img"
|
||||
:src="require(`@/modules/cmdb/assets/dcim/rack.png`)"
|
||||
/>
|
||||
|
||||
<div class="rack-grid-item-data">
|
||||
<ops-icon
|
||||
type="a-veops-device2"
|
||||
class="rack-grid-item-data-icon"
|
||||
/>
|
||||
<span class="rack-grid-item-data-value">
|
||||
{{ item.u_used_count }}/{{ item.u_count }}
|
||||
</span>
|
||||
|
||||
<div class="rack-grid-item-data-progress">
|
||||
<div
|
||||
class="rack-grid-item-data-progress-line"
|
||||
:style="{
|
||||
width: item.u_used_ratio + '%'
|
||||
}"
|
||||
></div>
|
||||
<div
|
||||
class="rack-grid-item-data-progress-end"
|
||||
:style="{
|
||||
left: item.u_used_ratio + '%'
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a
|
||||
class="rack-grid-item-btn"
|
||||
@click="openRackDetail(item)"
|
||||
>
|
||||
<span class="rack-grid-item-btn-text">{{ $t('cmdb.dcim.viewDetail') }}</span>
|
||||
<a-icon type="right" class="rack-grid-item-btn-icon" />
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-else class="rack-grid-null">
|
||||
<img class="rack-grid-null-img" :src="require(`@/assets/data_empty.png`)"></img>
|
||||
<div class="rack-grid-null-text">{{ $t('noData') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'RackGrid',
|
||||
props: {
|
||||
rackList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openRackDetail(data) {
|
||||
this.$emit('openRackDetail', data)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.rack-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
column-gap: 27px;
|
||||
row-gap: 27px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
max-height: 100%;
|
||||
padding-bottom: 57px;
|
||||
|
||||
&-item {
|
||||
width: 205px;
|
||||
height: 219px;
|
||||
flex-shrink: 0;
|
||||
background-color: #F9FBFF;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
transition: all 0.1s;
|
||||
|
||||
&-warning {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px 6px;
|
||||
background-color: #FFDEBF;
|
||||
border-radius: 2px;
|
||||
width: max-content;
|
||||
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
|
||||
&-icon {
|
||||
font-size: 12px;
|
||||
color: #FF7D00;
|
||||
margin-right: 2.5px;
|
||||
}
|
||||
|
||||
&-text {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #FF7D00;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -6px;
|
||||
left: 50%;
|
||||
margin-left: -7px;
|
||||
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 7px solid transparent;
|
||||
border-right: 7px solid transparent;
|
||||
border-top: 6px solid #FFDEBF;
|
||||
}
|
||||
}
|
||||
|
||||
&-header {
|
||||
width: 100%;
|
||||
height: 25px;
|
||||
background-color: #8FB9F712;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&-name {
|
||||
height: 25px;
|
||||
line-height: 25px;
|
||||
border-bottom-right-radius: 25px;
|
||||
padding-left: 7px;
|
||||
padding-right: 17px;
|
||||
background-color: #4E5969;
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #FFFFFF;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
|
||||
&-store {
|
||||
padding-right: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #2F54EB;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
&-img {
|
||||
height: 112px;
|
||||
margin-top: 16px;
|
||||
transition: all 0.1s;
|
||||
}
|
||||
|
||||
&-data {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&-value {
|
||||
margin-left: 5px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #4E5969;
|
||||
}
|
||||
|
||||
&-progress {
|
||||
margin-left: 6px;
|
||||
width: 97px;
|
||||
height: 2px;
|
||||
border-radius: 2px;
|
||||
background-color: #C3D0EB;
|
||||
position: relative;
|
||||
|
||||
&-line {
|
||||
height: 2px;
|
||||
border-radius: 2px;
|
||||
background-color: #7F97FA;
|
||||
}
|
||||
|
||||
&-end {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
margin-top: -8px;
|
||||
margin-left: -8px;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
border-radius: 16px;
|
||||
background-color: #3044F112;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-top: -3px;
|
||||
margin-left: -3px;
|
||||
background-color: #2F54EB;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-btn {
|
||||
position: absolute;
|
||||
right: 17px;
|
||||
bottom: 10px;
|
||||
align-items: center;
|
||||
display: none;
|
||||
|
||||
&-text {
|
||||
margin-right: 2px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #3F75FF;
|
||||
}
|
||||
|
||||
&-icon {
|
||||
font-size: 12px;
|
||||
color: #3F75FF;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #FFFFFF;
|
||||
box-shadow: 0px 22px 33px 0px rgba(41, 65, 126, 0.25);
|
||||
z-index: 2;
|
||||
|
||||
.rack-grid-item-name {
|
||||
background-color: #2F54EB;
|
||||
}
|
||||
|
||||
.rack-grid-item-img {
|
||||
margin-top: 7px;
|
||||
height: 128px;
|
||||
}
|
||||
|
||||
.rack-grid-item-data {
|
||||
margin-top: 9px;
|
||||
|
||||
&-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&-progress {
|
||||
width: 112px;
|
||||
}
|
||||
}
|
||||
|
||||
.rack-grid-item-btn {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-null {
|
||||
padding-top: 150px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
|
||||
&-img {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
&-text {
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div class="rack-table">
|
||||
<CITable
|
||||
ref="xTable"
|
||||
:attrList="preferenceAttrList"
|
||||
:columns="columns"
|
||||
:data="rackList"
|
||||
:height="tableHeight"
|
||||
:sortConfig="{ remote: false, trigger: 'default' }"
|
||||
:showCheckbox="false"
|
||||
:showOperation="false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
import CITable from '@/modules/cmdb/components/ciTable/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'RackTable',
|
||||
components: {
|
||||
CITable
|
||||
},
|
||||
props: {
|
||||
rackList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
columns: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
preferenceAttrList: {
|
||||
type: Array,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
windowHeight: (state) => state.windowHeight,
|
||||
}),
|
||||
tableHeight() {
|
||||
return `${this.windowHeight - 295}px`
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
</style>
|
419
cmdb-ui/src/modules/cmdb/views/dcim/components/dcimTree.vue
Normal file
@@ -0,0 +1,419 @@
|
||||
<template>
|
||||
<div class="dcim-tree">
|
||||
<div class="dcim-tree-header">
|
||||
<a-input
|
||||
v-model="searchValue"
|
||||
class="dcim-tree-header-search"
|
||||
:placeholder="$t('placeholder1')"
|
||||
/>
|
||||
<a-dropdown>
|
||||
<a-button class="dcim-tree-header-more">
|
||||
<ops-icon type="veops-more" />
|
||||
</a-button>
|
||||
<a-menu slot="overlay">
|
||||
<a-menu-item
|
||||
v-for="(type) in rootAction"
|
||||
:key="type"
|
||||
@click="openForm({
|
||||
dcimType: type
|
||||
})"
|
||||
>
|
||||
<a>
|
||||
<a-icon
|
||||
type="plus-circle"
|
||||
class="dcim-tree-header-menu-icon"
|
||||
/>
|
||||
{{ $t(addActionTitle[type]) }}
|
||||
</a>
|
||||
</a-menu-item>
|
||||
|
||||
<a-menu-item
|
||||
class="dcim-tree-header-calc"
|
||||
@click="calcUnitFreeCount"
|
||||
>
|
||||
<a>
|
||||
<ops-icon
|
||||
type="veops-refresh"
|
||||
class="dcim-tree-header-menu-icon"
|
||||
/>
|
||||
{{ $t('cmdb.dcim.calcUnitFreeCount') }}
|
||||
</a>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
|
||||
<div class="dcim-tree-main">
|
||||
<a-tree
|
||||
v-if="treeData.length"
|
||||
autoExpandParent
|
||||
:treeData="filterTreeData"
|
||||
:selectedKeys="treeKey ? [treeKey] : []"
|
||||
:defaultExpandedKeys="treeKey ? [treeKey] : []"
|
||||
>
|
||||
<template #title="treeNodeData">
|
||||
<div
|
||||
class="dcim-tree-node"
|
||||
@click="clickTreeNode(treeNodeData)"
|
||||
>
|
||||
<ops-icon
|
||||
:type="treeNodeData.icon"
|
||||
class="dcim-tree-node-icon"
|
||||
:style="{ color: treeNodeData.iconColor }"
|
||||
/>
|
||||
<a-tooltip :title="treeNodeData.title">
|
||||
<span
|
||||
class="dcim-tree-node-title"
|
||||
:style="{
|
||||
color: treeKey === treeNodeData.key ? '#2F54EB' : ''
|
||||
}"
|
||||
>
|
||||
{{ treeNodeData.title }}
|
||||
</span>
|
||||
</a-tooltip>
|
||||
|
||||
<div class="dcim-tree-node-right">
|
||||
<span
|
||||
v-if="treeNodeData.count"
|
||||
class="dcim-tree-node-count"
|
||||
>
|
||||
{{ treeNodeData.count }}
|
||||
</span>
|
||||
|
||||
<a-dropdown>
|
||||
<a class="dcim-tree-node-action">
|
||||
<ops-icon type="veops-more" />
|
||||
</a>
|
||||
|
||||
<a-menu slot="overlay">
|
||||
<a-menu-item
|
||||
v-if="treeNodeData.addType"
|
||||
@click="openForm({
|
||||
dcimType: treeNodeData.addType,
|
||||
parentId: treeNodeData._id
|
||||
})"
|
||||
>
|
||||
<a-icon type="plus-circle" />
|
||||
{{ $t(addActionTitle[treeNodeData.addType]) }}
|
||||
</a-menu-item>
|
||||
<a-menu-item
|
||||
@click="openDetail(treeNodeData)"
|
||||
>
|
||||
<a-icon type="unordered-list" />
|
||||
{{ $t('cmdb.dcim.viewDetail') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item
|
||||
@click="openForm({
|
||||
dcimType: treeNodeData.dcimType,
|
||||
parentId: treeNodeData.parentId,
|
||||
nodeId: treeNodeData._id
|
||||
})"
|
||||
>
|
||||
<ops-icon type="veops-edit" />
|
||||
{{ $t('cmdb.dcim.editNode') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="deleteNode(treeNodeData)">
|
||||
<ops-icon type="veops-delete" />
|
||||
{{ $t('cmdb.dcim.deleteNode') }}
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-tree>
|
||||
</div>
|
||||
|
||||
<CIDetailDrawer
|
||||
ref="CIdetailRef"
|
||||
:typeId="viewDetailCITypeId"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { DCIM_TYPE, DCIM_TYPE_NAME_MAP } from '../constants.js'
|
||||
import { deleteDCIM, calcUnitFreeCount } from '@/modules/cmdb/api/dcim.js'
|
||||
import CIDetailDrawer from '@/modules/cmdb/views/ci/modules/ciDetailDrawer.vue'
|
||||
|
||||
export default {
|
||||
name: 'DCIMTree',
|
||||
components: {
|
||||
CIDetailDrawer
|
||||
},
|
||||
props: {
|
||||
treeData: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
treeKey: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchValue: '',
|
||||
addActionTitle: {
|
||||
[DCIM_TYPE.REGION]: 'cmdb.dcim.addRegion',
|
||||
[DCIM_TYPE.IDC]: 'cmdb.dcim.addIDC',
|
||||
[DCIM_TYPE.SERVER_ROOM]: 'cmdb.dcim.addServerRoom',
|
||||
},
|
||||
rootAction: [
|
||||
DCIM_TYPE.REGION,
|
||||
DCIM_TYPE.IDC
|
||||
],
|
||||
|
||||
viewDetailCITypeId: 0,
|
||||
viewDetailAttrObj: {},
|
||||
|
||||
calculatedFreeUnitCount: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filterTreeData() {
|
||||
if (this.searchValue) {
|
||||
const treeData = _.cloneDeep(this.treeData)
|
||||
|
||||
// 过滤筛选
|
||||
const filterTreeData = treeData.filter((data) => {
|
||||
return this.handleTreeDataBySearch(data)
|
||||
})
|
||||
|
||||
// 处理同级父节点
|
||||
const newTreeData = []
|
||||
treeData.forEach((item) => {
|
||||
const filterNodeData = filterTreeData.find((data) => data.key === item.key)
|
||||
if (filterNodeData) {
|
||||
newTreeData.push(filterNodeData)
|
||||
} else if (
|
||||
filterTreeData.some((data) => data.parentId === item.key)
|
||||
) {
|
||||
newTreeData.push(item)
|
||||
}
|
||||
})
|
||||
|
||||
return newTreeData
|
||||
}
|
||||
|
||||
return this.treeData
|
||||
}
|
||||
},
|
||||
inject: ['getTreeData'],
|
||||
provide() {
|
||||
return {
|
||||
handleSearch: this.refreshTreeData,
|
||||
attrList: () => {
|
||||
return this.viewDetailAttrObj?.attributes || []
|
||||
},
|
||||
attributes: () => {
|
||||
return this.viewDetailAttrObj
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleTreeDataBySearch(data) {
|
||||
const isMatch = data?.title?.indexOf?.(this.searchValue) !== -1
|
||||
if (!data?.children?.length) {
|
||||
return isMatch ? data : null
|
||||
}
|
||||
|
||||
data.children = data.children.filter((data) => {
|
||||
return this.handleTreeDataBySearch(data)
|
||||
})
|
||||
return isMatch || data.children.length ? data : null
|
||||
},
|
||||
|
||||
openForm({
|
||||
dcimType,
|
||||
nodeId = undefined,
|
||||
parentId = ''
|
||||
}) {
|
||||
this.$emit('openForm', {
|
||||
dcimType,
|
||||
nodeId,
|
||||
parentId
|
||||
})
|
||||
},
|
||||
|
||||
deleteNode(node) {
|
||||
this.$confirm({
|
||||
title: this.$t('warning'),
|
||||
content: this.$t('confirmDelete'),
|
||||
onOk: async () => {
|
||||
await deleteDCIM(node.dcimType, node._id)
|
||||
|
||||
if (node.key === this.treeKey) {
|
||||
this.$emit('updateTreeKey', '')
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.refreshTreeData()
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
refreshTreeData() {
|
||||
this.getTreeData()
|
||||
},
|
||||
|
||||
clickTreeNode(node) {
|
||||
if (node.dcimType === DCIM_TYPE.SERVER_ROOM) {
|
||||
this.$emit('updateTreeKey', node.key)
|
||||
}
|
||||
},
|
||||
|
||||
async openDetail(node) {
|
||||
this.$emit('getAttrList', DCIM_TYPE_NAME_MAP[node.dcimType], node.dcimType, (allAttrList) => {
|
||||
this.viewDetailCITypeId = node._type
|
||||
this.viewDetailAttrObj = allAttrList[node.dcimType]
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.$refs.CIdetailRef.create(node._id)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
calcUnitFreeCount() {
|
||||
if (this.calculatedFreeUnitCount) {
|
||||
this.$message.info(this.$t('cmdb.dcim.calcUnitFreeCountTip'))
|
||||
} else {
|
||||
this.$confirm({
|
||||
title: this.$t('tip'),
|
||||
content: this.$t('cmdb.dcim.calcUnitFreeCountTip2'),
|
||||
onOk: () => {
|
||||
calcUnitFreeCount().then(() => {
|
||||
this.calculatedFreeUnitCount = true
|
||||
this.$message.success(this.$t('cmdb.dcim.calcUnitFreeCountTip1'))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.dcim-tree {
|
||||
&-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 14px;
|
||||
|
||||
&-search {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&-more {
|
||||
flex-shrink: 0;
|
||||
width: 32px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
&-calc {
|
||||
border-top: dashed 1px #e8e8e8;
|
||||
}
|
||||
|
||||
&-menu-icon {
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
&-main {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
/deep/ .ant-tree {
|
||||
.ant-tree-node-content-wrapper {
|
||||
width: calc(100% - 24px);
|
||||
padding: 0px;
|
||||
display: inline-block;
|
||||
height: fit-content;
|
||||
|
||||
.ant-tree-title {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
padding: 0 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.ipam-tree-node_hide_expand {
|
||||
.ant-tree-switcher {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ant-tree-node-content-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tree-switcher-icon {
|
||||
color: #CACDD9;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 32px;
|
||||
cursor: pointer;
|
||||
|
||||
&-icon {
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&-title {
|
||||
margin-left: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
|
||||
&-right {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&-count {
|
||||
font-size: 10px;
|
||||
font-weight: 400;
|
||||
color: #A5A9BC;
|
||||
}
|
||||
|
||||
&-action {
|
||||
display: none;
|
||||
margin-left: 3px;
|
||||
font-size: 12px;
|
||||
|
||||
&:hover {
|
||||
color: #2F54EB;
|
||||
}
|
||||
|
||||
/deep/ .ant-dropdown-menu {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
/deep/ .ant-dropdown-menu-item {
|
||||
padding: 5px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.dcim-tree-node-action {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,228 @@
|
||||
<template>
|
||||
<div
|
||||
ref="deviceListRef"
|
||||
class="device-list"
|
||||
>
|
||||
<div class="device-list-tabs">
|
||||
<div
|
||||
v-for="(item) in tabs"
|
||||
:key="item.id"
|
||||
:class="[
|
||||
'device-list-tabs-item',
|
||||
item.id === tabActive ? 'device-list-tabs-item_active' : ''
|
||||
]"
|
||||
@click="clickTab(item.id)"
|
||||
>
|
||||
<CIIcon :icon="item.icon" />
|
||||
<span class="device-list-tabs-item-name" >{{ item.alias || item.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CITable
|
||||
ref="xTable"
|
||||
:attrList="preferenceAttrList"
|
||||
:columns="columns"
|
||||
:data="deviceList"
|
||||
:height="tableHeight"
|
||||
:showCheckbox="false"
|
||||
:showDelete="false"
|
||||
:sortConfig="{ remote: false, trigger: 'default' }"
|
||||
@openDetail="openDetail"
|
||||
/>
|
||||
|
||||
<CIDetailDrawer
|
||||
v-if="tabActive"
|
||||
ref="CIdetailRef"
|
||||
:typeId="tabActive"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { mapState } from 'vuex'
|
||||
import { getSubscribeAttributes } from '@/modules/cmdb/api/preference'
|
||||
import { getCITableColumns } from '@/modules/cmdb/utils/helper'
|
||||
|
||||
import CIIcon from '@/modules/cmdb/components/ciIcon/index.vue'
|
||||
import CITable from '@/modules/cmdb/components/ciTable/index.vue'
|
||||
import CIDetailDrawer from '@/modules/cmdb/views/ci/modules/ciDetailDrawer.vue'
|
||||
|
||||
export default {
|
||||
name: 'DeviceList',
|
||||
components: {
|
||||
CIIcon,
|
||||
CITable,
|
||||
CIDetailDrawer
|
||||
},
|
||||
props: {
|
||||
allDeviceList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
CITypeRelations: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tabActive: '',
|
||||
tabs: [],
|
||||
|
||||
preferenceAttrList: [],
|
||||
deviceList: [],
|
||||
columns: [],
|
||||
deviceCIType: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
windowHeight: (state) => state.windowHeight,
|
||||
}),
|
||||
tableHeight() {
|
||||
return `${this.windowHeight - 210}px`
|
||||
},
|
||||
},
|
||||
inject: [
|
||||
'getDeviceList',
|
||||
'getRackList'
|
||||
],
|
||||
provide() {
|
||||
return {
|
||||
handleSearch: this.refreshData,
|
||||
attrList: () => {
|
||||
return this?.deviceCIType?.attributes || []
|
||||
},
|
||||
attributes: () => {
|
||||
return {
|
||||
attributes: this?.deviceCIType?.attributes || [],
|
||||
unique_id: this?.deviceCIType?.unique_id || 0,
|
||||
unique: this?.deviceCIType?.show_key || ''
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
allDeviceList: {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
handler() {
|
||||
this.initData()
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initData() {
|
||||
const tabs = []
|
||||
this.allDeviceList.forEach((item) => {
|
||||
const CIType = this.CITypeRelations.find((CIType) => CIType.id === item._type)
|
||||
|
||||
tabs.push({
|
||||
icon: CIType.icon,
|
||||
name: item.ci_type,
|
||||
alias: item.ci_type_alias,
|
||||
id: item._type
|
||||
})
|
||||
})
|
||||
|
||||
this.clickTab(tabs?.[0]?.id ?? '')
|
||||
this.tabs = _.uniqBy(tabs, 'id')
|
||||
},
|
||||
|
||||
clickTab(id) {
|
||||
if (id !== this.tabActive) {
|
||||
this.tabActive = id
|
||||
|
||||
if (this.tabActive) {
|
||||
this.initTableData()
|
||||
} else {
|
||||
this.columns = []
|
||||
this.deviceList = []
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async initTableData() {
|
||||
const subscribed = await getSubscribeAttributes(this.tabActive)
|
||||
this.preferenceAttrList = subscribed.attributes
|
||||
|
||||
const deviceList = this.allDeviceList.filter((item) => item._type === this.tabActive)
|
||||
|
||||
const deviceCIType = this.CITypeRelations.find((item) => item.id === this.tabActive)
|
||||
this.deviceCIType = deviceCIType || {}
|
||||
|
||||
this.getColumns(deviceList)
|
||||
this.deviceList = deviceList
|
||||
},
|
||||
|
||||
getColumns(data) {
|
||||
const width = this.$refs.deviceListRef.clientWidth - 50
|
||||
const columns = getCITableColumns(data, this.preferenceAttrList, width)
|
||||
columns.forEach((item) => {
|
||||
if (item.editRender) {
|
||||
item.editRender.enabled = false
|
||||
}
|
||||
})
|
||||
this.columns = columns
|
||||
},
|
||||
|
||||
refreshData() {
|
||||
this.getDeviceList()
|
||||
this.getRackList()
|
||||
},
|
||||
|
||||
openDetail(id, activeTabKey, ciDetailRelationKey) {
|
||||
this.$refs.CIdetailRef.create(id, activeTabKey, ciDetailRelationKey)
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.device-list {
|
||||
width: 100%;
|
||||
|
||||
&-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
column-gap: 9px;
|
||||
row-gap: 5px;
|
||||
margin-bottom: 18px;
|
||||
|
||||
&-item {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
padding: 4px 12px;
|
||||
background-color: #F7F8FA;
|
||||
border-radius: 1px;
|
||||
border: solid 1px transparent;
|
||||
max-width: 100%;
|
||||
|
||||
&-name {
|
||||
margin-left: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #1D2129;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
|
||||
&_active {
|
||||
border-color: #B1C9FF;
|
||||
background-color: #F9FBFF;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.device-list-tabs-item-name {
|
||||
color: #3F75FF;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,327 @@
|
||||
<template>
|
||||
<CustomDrawer
|
||||
width="825px"
|
||||
:visible="visible"
|
||||
:bodyStyle="{ height: '100vh', padding: '0px' }"
|
||||
:hasTitle="false"
|
||||
destroyOnClose
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="rack-detail">
|
||||
<div class="rack-header">
|
||||
<div class="rack-header-left">
|
||||
<div class="rack-header-name">
|
||||
<span class="rack-header-name-label">
|
||||
{{ $t('cmdb.dcim.rack') }}
|
||||
</span>
|
||||
<a-tooltip :title="rackData.name">
|
||||
<span class="rack-header-name-value">
|
||||
{{ rackData.name }}
|
||||
</span>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<ops-icon
|
||||
type="veops-edit"
|
||||
class="rack-header-edit"
|
||||
@click="clickEdit"
|
||||
/>
|
||||
<ops-icon
|
||||
type="veops-delete"
|
||||
class="rack-header-delete"
|
||||
@click="clickDelete"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="rack-header-right">
|
||||
<div
|
||||
v-for="(item, index) in countList"
|
||||
:key="index"
|
||||
class="rack-header-count"
|
||||
>
|
||||
<span class="rack-header-count-name">{{ $t(item.name) }}:</span>
|
||||
<span class="rack-header-count-value">{{ item.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-tabs
|
||||
class="rack-detail-tabs"
|
||||
v-model="tabActive"
|
||||
>
|
||||
<a-tab-pane
|
||||
key="rackView"
|
||||
:tab="$t('cmdb.dcim.rackView')"
|
||||
>
|
||||
<RackView
|
||||
:CITypeRelations="CITypeRelations"
|
||||
:rackData="rackData"
|
||||
:deviceList="deviceList"
|
||||
:rackList="rackList"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane
|
||||
key="rackDetail"
|
||||
:tab="$t('cmdb.dcim.rackDetail')"
|
||||
>
|
||||
<RackGroupAttr
|
||||
:ci="rackData"
|
||||
:rackCITYpeId="rackCITYpe.id"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane
|
||||
key="deviceList"
|
||||
:tab="$t('cmdb.dcim.deviceList')"
|
||||
>
|
||||
<DeviceList
|
||||
:allDeviceList="deviceList"
|
||||
:CITypeRelations="CITypeRelations"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane
|
||||
key="operationLog"
|
||||
:tab="$t('cmdb.dcim.operationLog')"
|
||||
>
|
||||
<OperationLog
|
||||
v-if="tabActive === 'operationLog'"
|
||||
:rackId="rackId"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</CustomDrawer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DCIM_TYPE } from '../../constants.js'
|
||||
import { deleteDCIM } from '@/modules/cmdb/api/dcim.js'
|
||||
import { getCITypeChildren } from '@/modules/cmdb/api/CITypeRelation'
|
||||
import { searchCIRelation } from '@/modules/cmdb/api/CIRelation'
|
||||
|
||||
import RackView from './rackView/index.vue'
|
||||
import RackGroupAttr from './rackGroupAttr/index.vue'
|
||||
import DeviceList from './deviceList/index.vue'
|
||||
import OperationLog from './operationLog/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'RackDetail',
|
||||
components: {
|
||||
RackView,
|
||||
RackGroupAttr,
|
||||
DeviceList,
|
||||
OperationLog
|
||||
},
|
||||
props: {
|
||||
roomId: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
rackCITYpe: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
rackList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
rackId: 0,
|
||||
tabActive: 'rackView',
|
||||
|
||||
CITypeRelations: [],
|
||||
deviceList: [],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
rackData() {
|
||||
return this.rackList.find((item) => item._id === this.rackId) || {}
|
||||
},
|
||||
countList() {
|
||||
const {
|
||||
u_count = 0,
|
||||
u_used_ratio = 0,
|
||||
u_slot_abnormal = false
|
||||
} = this.rackData
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'cmdb.dcim.deviceCount',
|
||||
value: this.deviceList?.length || 0
|
||||
},
|
||||
{
|
||||
name: 'cmdb.dcim.unitCount',
|
||||
value: u_count
|
||||
},
|
||||
{
|
||||
name: 'cmdb.dcim.unitAbnormal',
|
||||
value: u_slot_abnormal ? this.$t('yes') : this.$t('no')
|
||||
},
|
||||
{
|
||||
name: 'cmdb.dcim.utilizationRation',
|
||||
value: `${u_used_ratio}%`
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
inject: [
|
||||
'getTreeData',
|
||||
'getRackList'
|
||||
],
|
||||
provide() {
|
||||
return {
|
||||
getDeviceList: this.getDeviceList
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async open(rackId) {
|
||||
this.rackId = rackId
|
||||
this.visible = true
|
||||
|
||||
if (!this.CITypeRelations.length) {
|
||||
const res = await getCITypeChildren(this.rackCITYpe.id)
|
||||
this.CITypeRelations = res?.children || []
|
||||
}
|
||||
|
||||
await this.getDeviceList()
|
||||
},
|
||||
|
||||
async getDeviceList() {
|
||||
if (!this.rackId) {
|
||||
return
|
||||
}
|
||||
|
||||
const res = await searchCIRelation(`root_id=${this.rackId}&level=1&count=10000`)
|
||||
const deviceList = res?.result || []
|
||||
deviceList.sort((a, b) => a.u_start - b.u_start)
|
||||
this.deviceList = deviceList
|
||||
},
|
||||
|
||||
handleClose() {
|
||||
this.rackId = 0
|
||||
this.tabActive = 'rackView'
|
||||
this.visible = false
|
||||
},
|
||||
|
||||
clickEdit() {
|
||||
this.$emit('openForm', {
|
||||
dcimType: DCIM_TYPE.RACK,
|
||||
parentId: this.roomId,
|
||||
nodeId: this.rackId
|
||||
})
|
||||
this.handleClose()
|
||||
},
|
||||
|
||||
clickDelete() {
|
||||
this.$confirm({
|
||||
title: this.$t('warning'),
|
||||
content: this.$t('confirmDelete'),
|
||||
onOk: () => {
|
||||
deleteDCIM(DCIM_TYPE.RACK, this.rackId).then(() => {
|
||||
this.$message.success(this.$t('deleteSuccess'))
|
||||
this.handleClose()
|
||||
this.getRackList()
|
||||
this.getTreeData()
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
refreshRackList() {
|
||||
this.$emit('refreshRackList')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.rack-detail {
|
||||
.rack-header {
|
||||
height: 44px;
|
||||
padding: 0px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: #F7F8FA;
|
||||
|
||||
&-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
font-weight: 900;
|
||||
color: #1D2129;
|
||||
max-width: calc(100% - 48px);
|
||||
|
||||
&-label {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&-value {
|
||||
color: #2F54EB;
|
||||
margin-left: 2px;
|
||||
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
&-edit {
|
||||
margin-left: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&-delete {
|
||||
margin-left: 12px;
|
||||
font-size: 12px;
|
||||
color: #FD4C6A;
|
||||
}
|
||||
|
||||
&-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 30px;
|
||||
flex-shrink: 0;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
&-count {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&-name {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #4E5969;
|
||||
}
|
||||
|
||||
&-value {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #1D2129;
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-tabs {
|
||||
margin-left: 19px;
|
||||
margin-right: 19px;
|
||||
|
||||
/deep/ .ant-tabs-bar {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<div class="operation-log">
|
||||
<ops-table
|
||||
ref="xTable"
|
||||
size="small"
|
||||
show-overflow
|
||||
show-header-overflow
|
||||
highlight-hover-row
|
||||
:data="tableData"
|
||||
:height="tableHeight"
|
||||
:sort-config="{ remote: true }"
|
||||
@sort-change="handleSortChange"
|
||||
>
|
||||
<vxe-table-column
|
||||
:title="$t('cmdb.dcim.operationTime')"
|
||||
field="created_at"
|
||||
sortable
|
||||
></vxe-table-column>
|
||||
<vxe-table-column
|
||||
:title="$t('cmdb.dcim.operationUser')"
|
||||
field="operationUser"
|
||||
></vxe-table-column>
|
||||
<vxe-table-column
|
||||
:title="$t('cmdb.dcim.operationType')"
|
||||
field="operate_type"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div
|
||||
class="operation-log-device-type"
|
||||
:style="{
|
||||
backgroundColor: row.deviceTypeData.backgroundColor,
|
||||
color: row.deviceTypeData.textColor
|
||||
}"
|
||||
>
|
||||
{{ $t(row.deviceTypeData.name) }}
|
||||
</div>
|
||||
</template>
|
||||
</vxe-table-column>
|
||||
<vxe-table-column
|
||||
:title="$t('cmdb.dcim.deviceType')"
|
||||
field="deviceType"
|
||||
></vxe-table-column>
|
||||
<vxe-table-column
|
||||
:title="$t('cmdb.dcim.deviceName')"
|
||||
field="deviceName"
|
||||
></vxe-table-column>
|
||||
</ops-table>
|
||||
|
||||
<div class="operation-log-pagination">
|
||||
<a-pagination
|
||||
:showSizeChanger="true"
|
||||
:current="page"
|
||||
size="small"
|
||||
:total="totalNumber"
|
||||
show-quick-jumper
|
||||
:page-size="pageSize"
|
||||
:page-size-options="pageSizeOptions"
|
||||
:show-total="
|
||||
(total, range) =>
|
||||
$t('pagination.total', {
|
||||
range0: range[0],
|
||||
range1: range[1],
|
||||
total,
|
||||
})
|
||||
"
|
||||
@change="handleChangePage"
|
||||
@showSizeChange="onShowSizeChange"
|
||||
>
|
||||
<template slot="buildOptionText" slot-scope="props">
|
||||
<span v-if="props.value !== '100000'">{{ props.value }}{{ $t('itemsPerPage') }}</span>
|
||||
<span v-if="props.value === '100000'">{{ $t('cmdb.ci.all') }}</span>
|
||||
</template>
|
||||
</a-pagination>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import { getDCIMHistoryOperate } from '@/modules/cmdb/api/dcim.js'
|
||||
|
||||
export default {
|
||||
name: 'OperationLog',
|
||||
props: {
|
||||
rackId: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
pageSizeOptions: ['50', '100', '200'],
|
||||
totalNumber: 0,
|
||||
tableData: [],
|
||||
getTableDataParams: {
|
||||
reverse: 1
|
||||
},
|
||||
|
||||
deviceTypeMap: {
|
||||
0: {
|
||||
textColor: '#00B42A',
|
||||
backgroundColor: '#F6FFED',
|
||||
name: 'cmdb.dcim.addDevice'
|
||||
},
|
||||
1: {
|
||||
textColor: '#FD4C6A',
|
||||
backgroundColor: '#FFECE8',
|
||||
name: 'cmdb.dcim.removeDevice'
|
||||
},
|
||||
2: {
|
||||
textColor: '#FF7D00',
|
||||
backgroundColor: '#FFECCF',
|
||||
name: 'cmdb.dcim.moveDevice'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
windowHeight: (state) => state.windowHeight,
|
||||
allEmployees: (state) => state.user.allEmployees,
|
||||
}),
|
||||
tableHeight() {
|
||||
return `${this.windowHeight - 187}px`
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.getTableData()
|
||||
},
|
||||
methods: {
|
||||
async getTableData() {
|
||||
const res = await getDCIMHistoryOperate({
|
||||
rack_id: this.rackId,
|
||||
count: this.pageSize,
|
||||
page: this.page,
|
||||
...this.getTableDataParams
|
||||
})
|
||||
|
||||
const tableData = res?.result || []
|
||||
tableData.forEach((item) => {
|
||||
const ci = res?.id2ci?.[item?.ci_id] || {}
|
||||
const showKey = res?.type2show_key?.[ci?._type] || ''
|
||||
const user = this.allEmployees.find((emp) => item.uid === emp.acl_uid)
|
||||
|
||||
item.operationUser = user?.nickname || ''
|
||||
item.deviceType = ci?.ci_type_alias || ''
|
||||
item.deviceName = ci?.[showKey] || item?.ci_id || ''
|
||||
item.deviceTypeData = this.deviceTypeMap?.[item?.operate_type] || {}
|
||||
})
|
||||
|
||||
this.tableData = tableData
|
||||
this.totalNumber = res?.numfound || 0
|
||||
},
|
||||
handleChangePage(page) {
|
||||
this.page = page
|
||||
this.getTableData()
|
||||
},
|
||||
onShowSizeChange(_, pageSize) {
|
||||
this.page = 1
|
||||
this.pageSize = pageSize
|
||||
this.getTableData()
|
||||
},
|
||||
handleSortChange(data) {
|
||||
if (data?.order === 'asc') {
|
||||
this.getTableDataParams.reverse = 0
|
||||
} else {
|
||||
this.getTableDataParams.reverse = 1
|
||||
}
|
||||
this.page = 1
|
||||
this.getTableData()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.operation-log {
|
||||
&-device-type {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
height: 22px;
|
||||
padding: 0 9px;
|
||||
border-radius: 1px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&-pagination {
|
||||
text-align: right;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<div class="rack-group-attr">
|
||||
<el-descriptions
|
||||
v-for="group in attributeGroups"
|
||||
class="rack-group-attr-desc"
|
||||
:title="group.name || $t('other')"
|
||||
:key="group.name"
|
||||
border
|
||||
:column="3"
|
||||
>
|
||||
<el-descriptions-item
|
||||
v-for="attr in group.attributes"
|
||||
:label="`${attr.alias || attr.name}`"
|
||||
:key="attr.name"
|
||||
>
|
||||
<ci-detail-attr-content
|
||||
:ci="ci"
|
||||
:attr="attr"
|
||||
:attributeGroups="attributeGroups"
|
||||
:showEdit="false"
|
||||
/>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { getCITypeGroupById, getCITypes } from '@/modules/cmdb/api/CIType'
|
||||
import { searchCI } from '@/modules/cmdb/api/ci'
|
||||
|
||||
import { Descriptions, DescriptionsItem } from 'element-ui'
|
||||
import CiDetailAttrContent from '@/modules/cmdb/views/ci/modules/ciDetailAttrContent.vue'
|
||||
|
||||
export default {
|
||||
name: 'RackGroupAttr',
|
||||
components: {
|
||||
ElDescriptions: Descriptions,
|
||||
ElDescriptionsItem: DescriptionsItem,
|
||||
CiDetailAttrContent
|
||||
},
|
||||
props: {
|
||||
ci: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
rackCITYpeId: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
attributeGroups: []
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getAttributes()
|
||||
},
|
||||
methods: {
|
||||
getAttributes() {
|
||||
getCITypeGroupById(this.rackCITYpeId, { need_other: 1 })
|
||||
.then((res) => {
|
||||
this.attributeGroups = res
|
||||
|
||||
this.handleReferenceAttr()
|
||||
})
|
||||
.catch((e) => {})
|
||||
},
|
||||
|
||||
async handleReferenceAttr() {
|
||||
const map = {}
|
||||
this.attributeGroups.forEach((group) => {
|
||||
group.attributes.forEach((attr) => {
|
||||
if (attr?.is_reference && attr?.reference_type_id && this.ci[attr.name]) {
|
||||
const ids = Array.isArray(this.ci[attr.name]) ? this.ci[attr.name] : this.ci[attr.name] ? [this.ci[attr.name]] : []
|
||||
if (ids.length) {
|
||||
if (!map?.[attr.reference_type_id]) {
|
||||
map[attr.reference_type_id] = {}
|
||||
}
|
||||
ids.forEach((id) => {
|
||||
map[attr.reference_type_id][id] = {}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (!Object.keys(map).length) {
|
||||
return
|
||||
}
|
||||
|
||||
const ciTypesRes = await getCITypes({
|
||||
type_ids: Object.keys(map).join(',')
|
||||
})
|
||||
const showAttrNameMap = {}
|
||||
ciTypesRes.ci_types.forEach((ciType) => {
|
||||
showAttrNameMap[ciType.id] = ciType?.show_name || ciType?.unique_name || ''
|
||||
})
|
||||
|
||||
const allRes = await Promise.all(
|
||||
Object.keys(map).map((key) => {
|
||||
return searchCI({
|
||||
q: `_type:${key},_id:(${Object.keys(map[key]).join(';')})`,
|
||||
count: 9999
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
const ciNameMap = {}
|
||||
allRes.forEach((res) => {
|
||||
res.result.forEach((item) => {
|
||||
ciNameMap[item._id] = item
|
||||
})
|
||||
})
|
||||
|
||||
const newAttrGroups = _.cloneDeep(this.attributeGroups)
|
||||
|
||||
newAttrGroups.forEach((group) => {
|
||||
group.attributes.forEach((attr) => {
|
||||
if (attr?.is_reference && attr?.reference_type_id) {
|
||||
attr.showAttrName = showAttrNameMap?.[attr?.reference_type_id] || ''
|
||||
|
||||
const referenceShowAttrNameMap = {}
|
||||
const referenceCIIds = this.ci[attr.name];
|
||||
(Array.isArray(referenceCIIds) ? referenceCIIds : referenceCIIds ? [referenceCIIds] : []).forEach((id) => {
|
||||
referenceShowAttrNameMap[id] = ciNameMap?.[id]?.[attr.showAttrName] ?? id
|
||||
})
|
||||
attr.referenceShowAttrNameMap = referenceShowAttrNameMap
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
this.$set(this, 'attributeGroups', newAttrGroups)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.rack-group-attr {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
&-desc {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
:okText="$t('cmdb.dcim.toChange')"
|
||||
:width="350"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<div class="abnormal-modal-title">
|
||||
<a-icon
|
||||
type="info-circle"
|
||||
theme="filled"
|
||||
class="abnormal-modal-title-icon"
|
||||
/>
|
||||
<span class="abnormal-modal-title-text">
|
||||
{{ $t('cmdb.dcim.unitAbnormal') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="abnormal-modal-content">
|
||||
<div class="abnormal-modal-content-row">
|
||||
<span
|
||||
v-for="(item, index) in abnormalList"
|
||||
:key="item.id"
|
||||
>
|
||||
{{ item.CITypeName }}
|
||||
<span class="abnormal-modal-content-name" >
|
||||
{{ item.name }}
|
||||
</span>
|
||||
<span
|
||||
v-if="index !== abnormalList.length - 1"
|
||||
>
|
||||
{{ $t('cmdb.dcim.abnormalModalTip1') }}
|
||||
</span>
|
||||
</span>
|
||||
<span>{{ $t('cmdb.dcim.abnormalModalTip2') }}</span>
|
||||
</div>
|
||||
<div class="abnormal-modal-content-row">
|
||||
{{ $t('cmdb.dcim.abnormalModalTip3') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-radio-group
|
||||
v-model="currentSelect"
|
||||
>
|
||||
<a-radio
|
||||
v-for="(item) in abnormalList"
|
||||
:value="item.id"
|
||||
:key="item.id"
|
||||
>
|
||||
{{ item.name }}
|
||||
</a-radio>
|
||||
</a-radio-group>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AbnormalModal',
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
abnormalList: [],
|
||||
currentSelect: undefined,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(data) {
|
||||
this.visible = true
|
||||
|
||||
const abnormalList = [data]
|
||||
if (data?.abnormalList?.length) {
|
||||
abnormalList.push(...data.abnormalList)
|
||||
}
|
||||
this.abnormalList = abnormalList
|
||||
|
||||
this.currentSelect = abnormalList?.[0]?.id ?? undefined
|
||||
},
|
||||
|
||||
handleCancel() {
|
||||
this.currentSelect = undefined
|
||||
this.abnormalList = []
|
||||
this.visible = false
|
||||
},
|
||||
|
||||
handleOk() {
|
||||
if (!this.currentSelect) {
|
||||
return
|
||||
}
|
||||
|
||||
const device = this.abnormalList.find((item) => item.id === this.currentSelect)
|
||||
this.$emit('ok', device)
|
||||
|
||||
this.handleCancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped >
|
||||
.abnormal-modal-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&-icon {
|
||||
font-size: 18px;
|
||||
color: #FF7D00;
|
||||
}
|
||||
|
||||
&-text {
|
||||
margin-left: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #1D2129;
|
||||
}
|
||||
}
|
||||
|
||||
.abnormal-modal-content {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
margin: 9px 0px;
|
||||
color: #1D2129;
|
||||
|
||||
&-name {
|
||||
color: #2F54EB;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<div class="device-select">
|
||||
<a-input-search
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<a-radio-group
|
||||
v-if="CIList.length"
|
||||
:value="currentSelect"
|
||||
class="device-select-group"
|
||||
@change="handleCIChange"
|
||||
>
|
||||
<a-radio
|
||||
v-for="(item) in CIList"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
class="device-select-item"
|
||||
>
|
||||
<a-tooltip :title="item.name" placement="topLeft">
|
||||
{{ item.name }}
|
||||
</a-tooltip>
|
||||
</a-radio>
|
||||
</a-radio-group>
|
||||
|
||||
<div v-else class="device-select-null">
|
||||
<img class="device-select-null-img" :src="require(`@/assets/data_empty.png`)"></img>
|
||||
<div class="device-select-null-text">{{ $t('noData') }}</div>
|
||||
</div>
|
||||
|
||||
<div class="device-select-pagination">
|
||||
<a-pagination
|
||||
:showSizeChanger="true"
|
||||
:current="page"
|
||||
size="small"
|
||||
:total="totalNumber"
|
||||
show-quick-jumper
|
||||
:page-size="pageSize"
|
||||
:page-size-options="pageSizeOptions"
|
||||
:show-total="
|
||||
(total, range) =>
|
||||
$t('pagination.total', {
|
||||
range0: range[0],
|
||||
range1: range[1],
|
||||
total,
|
||||
})
|
||||
"
|
||||
@change="handleChangePage"
|
||||
@showSizeChange="onShowSizeChange"
|
||||
>
|
||||
<template slot="buildOptionText" slot-scope="props">
|
||||
<span v-if="props.value !== '100000'">{{ props.value }}{{ $t('itemsPerPage') }}</span>
|
||||
<span v-if="props.value === '100000'">{{ $t('cmdb.ci.all') }}</span>
|
||||
</template>
|
||||
</a-pagination>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { searchCI } from '@/modules/cmdb/api/ci'
|
||||
|
||||
export default {
|
||||
name: 'DeviceSelect',
|
||||
props: {
|
||||
currentSelect: {
|
||||
type: [Number, undefined],
|
||||
default: undefined
|
||||
},
|
||||
CITypeId: {
|
||||
type: [Number, undefined],
|
||||
default: undefined
|
||||
},
|
||||
currentCITYpe: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
pageSizeOptions: ['20', '50', '100'],
|
||||
totalNumber: 0,
|
||||
CIList: [],
|
||||
|
||||
searchValue: ''
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
CITypeId: {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
handler(newVal, oldVal) {
|
||||
this.page = 1
|
||||
this.searchValue = ''
|
||||
|
||||
if (newVal && newVal !== oldVal) {
|
||||
this.getCIList()
|
||||
} else {
|
||||
this.CIList = []
|
||||
this.totalNumber = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async getCIList() {
|
||||
const res = await searchCI({
|
||||
q: `_type:${this.CITypeId}${this.searchValue ? `,*${this.searchValue}*` : ''}`,
|
||||
count: this.pageSize,
|
||||
page: this.page
|
||||
})
|
||||
let CIList = res?.result || []
|
||||
|
||||
if (CIList.length) {
|
||||
CIList = CIList.map((item) => {
|
||||
return {
|
||||
value: item?._id,
|
||||
name: item?.[this?.currentCITYpe?.show_key] || item?._id || '',
|
||||
unitCount: item?.u_count ?? 0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
this.CIList = CIList
|
||||
this.totalNumber = res?.numfound || 0
|
||||
},
|
||||
|
||||
handleSearch(value) {
|
||||
this.searchValue = value
|
||||
this.page = 1
|
||||
this.getCIList()
|
||||
},
|
||||
|
||||
handleChangePage(page) {
|
||||
this.page = page
|
||||
this.getCIList()
|
||||
},
|
||||
|
||||
onShowSizeChange(_, pageSize) {
|
||||
this.page = 1
|
||||
this.pageSize = pageSize
|
||||
this.getCIList()
|
||||
},
|
||||
|
||||
handleCIChange(e) {
|
||||
const value = e.target.value
|
||||
const findCI = this.CIList.find((item) => item.value === value)
|
||||
|
||||
this.$emit('change', findCI)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.device-select {
|
||||
width: 650px;
|
||||
|
||||
&-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
row-gap: 20px;
|
||||
margin: 12px 0px;
|
||||
max-height: 40vh;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
&-item {
|
||||
width: 48%;
|
||||
flex-shrink: 0;
|
||||
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
|
||||
&-null {
|
||||
margin: 30px 0px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
|
||||
&-img {
|
||||
width: 130px;
|
||||
}
|
||||
|
||||
&-text {
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&-pagination {
|
||||
text-align: right;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,243 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
:width="500"
|
||||
:title="$t('cmdb.dcim.addDevice')"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form-model
|
||||
ref="deviceFormRef"
|
||||
:model="form"
|
||||
:rules="formRules"
|
||||
:label-col="{ span: 5 }"
|
||||
:wrapper-col="{ span: 19 }"
|
||||
class="device-form"
|
||||
>
|
||||
<a-form-model-item
|
||||
:label="$t('cmdb.dcim.ciType')"
|
||||
prop="CITypeId"
|
||||
>
|
||||
<a-select
|
||||
v-model="form.CITypeId"
|
||||
showSearch
|
||||
allowClear
|
||||
optionFilterProp="title"
|
||||
@change="handleCITypeChange"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="(item) in CITypeRelations"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
:title="item.alias || item.name"
|
||||
>
|
||||
{{ item.alias || item.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-model-item>
|
||||
|
||||
<a-form-model-item
|
||||
:label="$t('cmdb.dcim.device')"
|
||||
prop="deviceId"
|
||||
>
|
||||
<a-popover trigger="click" placement="bottom">
|
||||
<DeviceSelect
|
||||
slot="content"
|
||||
:CITypeId="form.CITypeId"
|
||||
:currentCITYpe="currentCITYpe"
|
||||
:currentSelect="form.deviceId"
|
||||
@change="handleDeviceChange"
|
||||
/>
|
||||
<div
|
||||
class="device-form-select"
|
||||
>
|
||||
{{ deviceName }}
|
||||
</div>
|
||||
</a-popover>
|
||||
</a-form-model-item>
|
||||
|
||||
<a-form-model-item
|
||||
:label="$t('cmdb.dcim.unitStart')"
|
||||
prop="unitStart"
|
||||
>
|
||||
<a-input-number
|
||||
v-model="form.unitStart"
|
||||
:min="1"
|
||||
:precision="0"
|
||||
class="device-form-input"
|
||||
/>
|
||||
</a-form-model-item>
|
||||
|
||||
<a-form-model-item
|
||||
v-if="showUnitCount"
|
||||
:label="$t('cmdb.dcim.unitCount')"
|
||||
prop="unitCount"
|
||||
>
|
||||
<a-input-number
|
||||
v-model="form.unitCount"
|
||||
:min="1"
|
||||
:precision="0"
|
||||
class="device-form-input"
|
||||
/>
|
||||
</a-form-model-item>
|
||||
</a-form-model>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { postDevice } from '@/modules/cmdb/api/dcim.js'
|
||||
|
||||
import DeviceSelect from './deviceSelect.vue'
|
||||
|
||||
export default {
|
||||
name: 'DeviceForm',
|
||||
components: {
|
||||
DeviceSelect
|
||||
},
|
||||
props: {
|
||||
CITypeRelations: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
rackId: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
form: {
|
||||
CITypeId: undefined,
|
||||
deviceId: undefined,
|
||||
unitStart: undefined,
|
||||
unitCount: undefined
|
||||
},
|
||||
|
||||
deviceName: '',
|
||||
showUnitCount: true,
|
||||
formRules: {
|
||||
CITypeId: [
|
||||
{
|
||||
required: true, message: this.$t('placeholder2')
|
||||
}
|
||||
],
|
||||
deviceId: [
|
||||
{
|
||||
required: true, message: this.$t('placeholder2')
|
||||
}
|
||||
],
|
||||
unitStart: [
|
||||
{
|
||||
required: true, message: this.$t('placeholder1')
|
||||
}
|
||||
],
|
||||
unitCount: [
|
||||
{
|
||||
required: true, message: this.$t('placeholder1')
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentCITYpe() {
|
||||
return this.CITypeRelations.find((CIType) => CIType?.id === this.form.CITypeId) || {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(deviceData) {
|
||||
this.visible = true
|
||||
|
||||
if (deviceData) {
|
||||
this.form = {
|
||||
CITypeId: deviceData?.CITypeId ?? undefined,
|
||||
deviceId: deviceData?.deviceId ?? undefined,
|
||||
unitStart: deviceData?.unitStart ?? undefined,
|
||||
unitCount: deviceData?.unitCount ?? undefined,
|
||||
}
|
||||
|
||||
if (this.form.unitCount) {
|
||||
this.showUnitCount = false
|
||||
}
|
||||
this.deviceName = deviceData?.name || ''
|
||||
}
|
||||
},
|
||||
|
||||
handleCancel() {
|
||||
this.form = {
|
||||
CITypeId: undefined,
|
||||
deviceId: undefined,
|
||||
unitStart: undefined,
|
||||
unitCount: undefined
|
||||
}
|
||||
this.deviceName = ''
|
||||
this.showUnitCount = true
|
||||
this.$refs.deviceFormRef.clearValidate()
|
||||
|
||||
this.visible = false
|
||||
},
|
||||
|
||||
handleOk() {
|
||||
this.$refs.deviceFormRef.validate(async (valid) => {
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
await postDevice(
|
||||
this.rackId,
|
||||
this.form.deviceId,
|
||||
{
|
||||
u_start: this.form.unitStart,
|
||||
u_count: this.form.unitCount
|
||||
}
|
||||
)
|
||||
|
||||
this.handleCancel()
|
||||
this.$message.success(this.$t('addSuccess'))
|
||||
this.$emit('ok')
|
||||
})
|
||||
},
|
||||
|
||||
handleDeviceChange({
|
||||
name,
|
||||
value,
|
||||
unitCount
|
||||
}) {
|
||||
this.form.deviceId = value
|
||||
this.deviceName = name
|
||||
|
||||
this.form.unitCount = unitCount || undefined
|
||||
this.showUnitCount = !unitCount
|
||||
},
|
||||
|
||||
handleCITypeChange() {
|
||||
this.form.deviceId = undefined
|
||||
this.deviceName = ''
|
||||
this.showUnitCount = true
|
||||
this.form.unitCount = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.device-form {
|
||||
&-select {
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 2px;
|
||||
line-height: 32px;
|
||||
min-height: 32px;
|
||||
padding: 0 12px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
border-color: #597ef7;
|
||||
}
|
||||
}
|
||||
|
||||
&-input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,296 @@
|
||||
<template>
|
||||
<div v-if="unitList.length" class="rack-view">
|
||||
<div class="rack-view-col">
|
||||
<RackUnitView
|
||||
viewType="front"
|
||||
:countList="countList"
|
||||
:unitList="unitList"
|
||||
:rackId="rackData._id"
|
||||
@migrateDevice="migrateDevice"
|
||||
@openDeviceForm="openDeviceForm"
|
||||
@draggable="handleDraggable"
|
||||
@refreshRackAllData="refreshRackAllData"
|
||||
@openDeviceDetail="openDeviceDetail"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="rack-view-col">
|
||||
<RackUnitView
|
||||
viewType="rear"
|
||||
:countList="countList"
|
||||
:unitList="unitList"
|
||||
:rackId="rackData._id"
|
||||
@migrateDevice="migrateDevice"
|
||||
@openDeviceForm="openDeviceForm"
|
||||
@draggable="handleDraggable"
|
||||
@refreshRackAllData="refreshRackAllData"
|
||||
@openDeviceDetail="openDeviceDetail"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DeviceForm
|
||||
ref="deviceFormRef"
|
||||
:CITypeRelations="CITypeRelations"
|
||||
:rackId="rackData._id"
|
||||
@ok="refreshRackAllData"
|
||||
/>
|
||||
|
||||
<MigrateModal
|
||||
ref="migrateModalRef"
|
||||
:rackList="rackList"
|
||||
@ok="refreshRackAllData"
|
||||
/>
|
||||
|
||||
<CIDetailDrawer
|
||||
ref="CIdetailRef"
|
||||
:typeId="deviceCITypeId"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { putDevice } from '@/modules/cmdb/api/dcim.js'
|
||||
import { DEVICE_CITYPE_NAME } from '../../../constants.js'
|
||||
|
||||
import RackUnitView from './rackUnitView.vue'
|
||||
import DeviceForm from './deviceForm/index.vue'
|
||||
import MigrateModal from './migrateModal.vue'
|
||||
import CIDetailDrawer from '@/modules/cmdb/views/ci/modules/ciDetailDrawer.vue'
|
||||
|
||||
export default {
|
||||
name: 'RackView',
|
||||
components: {
|
||||
RackUnitView,
|
||||
DeviceForm,
|
||||
MigrateModal,
|
||||
CIDetailDrawer
|
||||
},
|
||||
props: {
|
||||
CITypeRelations: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
rackData: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
deviceList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
rackList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
unitList: [],
|
||||
countList: [],
|
||||
|
||||
deviceAttrList: [],
|
||||
deviceCITypeId: 0,
|
||||
}
|
||||
},
|
||||
inject: [
|
||||
'getRackList',
|
||||
'getDeviceList'
|
||||
],
|
||||
provide() {
|
||||
return {
|
||||
handleSearch: this.refreshRackAllData,
|
||||
attrList: () => {
|
||||
return this.deviceAttrList
|
||||
},
|
||||
attributes: () => {
|
||||
return {
|
||||
attributes: this.deviceAttrList
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
deviceList: {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
handler(deviceList) {
|
||||
this.initData(deviceList)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async initData(deviceList) {
|
||||
const CITypeMap = this.CITypeRelations.reduce((map, cur) => {
|
||||
map[cur.id] = cur
|
||||
return map
|
||||
}, {})
|
||||
|
||||
const _deviceList = _.cloneDeep(deviceList)
|
||||
|
||||
// 建立设备map, 并处理U位异常情况
|
||||
const deviceMap = {}
|
||||
_deviceList.forEach((device, index) => {
|
||||
const CITYpe = CITypeMap?.[device?._type] || {}
|
||||
|
||||
device.deviceImage = this.getDeviceViewImage(CITYpe?.name)
|
||||
device.name = device?.[CITYpe?.show_key] || device._id || ''
|
||||
device.icon = CITYpe?.icon || ''
|
||||
device.CITypeName = CITYpe?.alias || CITYpe?.name || ''
|
||||
device.id = device._id
|
||||
|
||||
if (index > 0) {
|
||||
const abnormalDevice = _deviceList.slice(0, index).find((item) => {
|
||||
const unitCount = item.abnormal ? item.abnormalUnitcount : item.u_count
|
||||
|
||||
return item.u_start <= device.u_start && device.u_start <= (item.u_start + unitCount - 1)
|
||||
})
|
||||
|
||||
if (abnormalDevice) {
|
||||
abnormalDevice.abnormal = true
|
||||
const endCount = Math.max(abnormalDevice.u_start + abnormalDevice.u_count, device.u_start + device.u_count)
|
||||
abnormalDevice.abnormalUnitcount = endCount - abnormalDevice.u_start
|
||||
|
||||
if (abnormalDevice?.abnormalList?.length) {
|
||||
abnormalDevice.abnormalList.push(device)
|
||||
} else {
|
||||
abnormalDevice.abnormalList = [device]
|
||||
}
|
||||
} else {
|
||||
deviceMap[device.u_start] = device
|
||||
}
|
||||
} else {
|
||||
deviceMap[device.u_start] = device
|
||||
}
|
||||
})
|
||||
|
||||
let unitIndex = 1
|
||||
const unitList = []
|
||||
|
||||
while (unitIndex <= this.rackData.u_count) {
|
||||
if (deviceMap[unitIndex]) {
|
||||
const device = deviceMap[unitIndex]
|
||||
const unitCount = device?.abnormal ? device.abnormalUnitcount : device.u_count
|
||||
|
||||
unitList.push({
|
||||
...device,
|
||||
unitCount,
|
||||
type: 'device',
|
||||
key: uuidv4(),
|
||||
abnormal: device?.abnormal ?? false,
|
||||
abnormalList: device.abnormalList
|
||||
})
|
||||
|
||||
unitIndex += unitCount
|
||||
device.assign = true
|
||||
} else {
|
||||
unitList.push({
|
||||
type: 'gap',
|
||||
unitCount: 1,
|
||||
key: uuidv4()
|
||||
})
|
||||
unitIndex += 1
|
||||
}
|
||||
}
|
||||
|
||||
this.unitList = _.reverse(unitList)
|
||||
this.countList = Array.from({ length: this.rackData.u_count }, (_, i) => this.rackData.u_count - i)
|
||||
},
|
||||
|
||||
getDeviceViewImage(name) {
|
||||
const image = {
|
||||
front: require('@/modules/cmdb/assets/dcim/device/server_front.png'),
|
||||
rear: require('@/modules/cmdb/assets/dcim/device/server_rear.png')
|
||||
}
|
||||
|
||||
switch (name) {
|
||||
case DEVICE_CITYPE_NAME.ROUTER:
|
||||
image.front = require('@/modules/cmdb/assets/dcim/device/router_front.png')
|
||||
image.rear = require('@/modules/cmdb/assets/dcim/device/router_rear.png')
|
||||
break
|
||||
case DEVICE_CITYPE_NAME.FIRE_WALL:
|
||||
image.front = require('@/modules/cmdb/assets/dcim/device/firewall_front.png')
|
||||
image.rear = require('@/modules/cmdb/assets/dcim/device/firewall_rear.png')
|
||||
break
|
||||
case DEVICE_CITYPE_NAME.SERVER:
|
||||
image.front = require('@/modules/cmdb/assets/dcim/device/server_front.png')
|
||||
image.rear = require('@/modules/cmdb/assets/dcim/device/server_rear.png')
|
||||
break
|
||||
case DEVICE_CITYPE_NAME.RAID:
|
||||
image.front = require('@/modules/cmdb/assets/dcim/device/raid_front.png')
|
||||
image.rear = require('@/modules/cmdb/assets/dcim/device/raid_rear.png')
|
||||
break
|
||||
case DEVICE_CITYPE_NAME.SWITCH:
|
||||
case DEVICE_CITYPE_NAME.FC_SWITCH:
|
||||
case DEVICE_CITYPE_NAME.F5:
|
||||
image.front = require('@/modules/cmdb/assets/dcim/device/switch_front.png')
|
||||
image.rear = require('@/modules/cmdb/assets/dcim/device/switch_rear.png')
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
return image
|
||||
},
|
||||
|
||||
openDeviceForm(deviceData) {
|
||||
this.$refs.deviceFormRef.open(deviceData)
|
||||
},
|
||||
|
||||
handleDraggable({
|
||||
startUnit,
|
||||
deviceId,
|
||||
oldUnitList
|
||||
}) {
|
||||
putDevice(
|
||||
this.rackData._id,
|
||||
deviceId,
|
||||
{
|
||||
to_u_start: startUnit
|
||||
}
|
||||
).then(() => {
|
||||
this.getDeviceList()
|
||||
}).catch((error) => {
|
||||
console.log('putDevice fail', error)
|
||||
this.unitList = oldUnitList
|
||||
})
|
||||
},
|
||||
|
||||
migrateDevice(deviceId) {
|
||||
this.$refs.migrateModalRef.open({
|
||||
deviceId,
|
||||
rackId: this.rackData._id
|
||||
})
|
||||
},
|
||||
|
||||
refreshRackAllData() {
|
||||
this.getRackList()
|
||||
this.getDeviceList()
|
||||
},
|
||||
|
||||
async openDeviceDetail(data) {
|
||||
const deviceCIType = this.CITypeRelations.find((item) => item.id === data._type)
|
||||
this.deviceAttrList = deviceCIType?.attributes || []
|
||||
this.deviceCITypeId = data?._type
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.$refs.CIdetailRef.create(data._id)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.rack-view {
|
||||
display: flex;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
max-height: calc(100vh - 160px);
|
||||
|
||||
&-col {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:title="$t('cmdb.dcim.deviceMigrate')"
|
||||
:visible="visible"
|
||||
:width="500"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form-model
|
||||
ref="deviceMigrateFormRef"
|
||||
:model="form"
|
||||
:rules="formRules"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
class="device-migrate"
|
||||
>
|
||||
<a-form-model-item
|
||||
:label="$t('cmdb.dcim.rack')"
|
||||
prop="to_rack_id"
|
||||
>
|
||||
<a-select
|
||||
v-model="form.to_rack_id"
|
||||
showSearch
|
||||
allowClear
|
||||
optionFilterProp="title"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="(rack) in rackList"
|
||||
:key="rack._id"
|
||||
:value="rack._id"
|
||||
:title="rack.name"
|
||||
>
|
||||
{{ rack.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-model-item>
|
||||
|
||||
<a-form-model-item
|
||||
:label="$t('cmdb.dcim.unitStart')"
|
||||
prop="to_u_start"
|
||||
>
|
||||
<a-input-number
|
||||
v-model="form.to_u_start"
|
||||
:min="1"
|
||||
:precision="0"
|
||||
class="device-migrate-input"
|
||||
/>
|
||||
</a-form-model-item>
|
||||
</a-form-model>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { migrateDevice } from '@/modules/cmdb/api/dcim.js'
|
||||
|
||||
export default {
|
||||
name: 'MigrateModal',
|
||||
props: {
|
||||
rackList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
form: {
|
||||
to_rack_id: undefined,
|
||||
to_u_start: undefined,
|
||||
},
|
||||
formRules: {
|
||||
to_rack_id: [
|
||||
{
|
||||
required: true, message: this.$t('placeholder2')
|
||||
}
|
||||
],
|
||||
to_u_start: [
|
||||
{
|
||||
required: true, message: this.$t('placeholder1')
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
deviceId: '',
|
||||
rackId: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(data) {
|
||||
this.visible = true
|
||||
this.deviceId = data?.deviceId || ''
|
||||
this.rackId = data?.rackId || ''
|
||||
},
|
||||
|
||||
handleCancel() {
|
||||
this.deviceId = ''
|
||||
this.rackId = ''
|
||||
this.form = {
|
||||
to_rack_id: undefined,
|
||||
to_u_start: undefined,
|
||||
}
|
||||
|
||||
this.$refs.deviceMigrateFormRef.clearValidate()
|
||||
this.visible = false
|
||||
},
|
||||
|
||||
handleOk() {
|
||||
this.$refs.deviceMigrateFormRef.validate(async (valid) => {
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
migrateDevice(
|
||||
this.rackId,
|
||||
this.deviceId,
|
||||
{
|
||||
...this.form
|
||||
}
|
||||
).then(() => {
|
||||
this.$message.success(this.$t('cmdb.dcim.migrationSuccess'))
|
||||
this.handleCancel()
|
||||
this.$emit('ok')
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.device-migrate {
|
||||
&-input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<div class="rack-header">
|
||||
<div class="rack-header-part-1">
|
||||
<div
|
||||
class="rack-header-part-1-line"
|
||||
:style="{
|
||||
backgroundColor: viewType === 'front' ? '#A4FFF8' : '#FFFFFF'
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="rack-header-part-2"
|
||||
:style="{
|
||||
padding: viewType === 'front' ? '0 8px' : '0 22px'
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-if="viewType === 'front'"
|
||||
class="rack-header-part-2-left"
|
||||
>
|
||||
<RackHeaderCircle/>
|
||||
<RackHeaderCircle/>
|
||||
<RackHeaderCircle/>
|
||||
</div>
|
||||
|
||||
<div class="rack-header-part-2-right">
|
||||
<div
|
||||
v-for="(item) in 300"
|
||||
:key="item"
|
||||
class="rack-header-part-2-right-item"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import RackHeaderCircle from './rackHeaderCircle.vue'
|
||||
|
||||
export default {
|
||||
name: 'RackHeader',
|
||||
components: {
|
||||
RackHeaderCircle
|
||||
},
|
||||
props: {
|
||||
viewType: {
|
||||
type: String,
|
||||
default: 'front'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.rack-header {
|
||||
width: 100%;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
&-part-1 {
|
||||
height: 5px;
|
||||
width: 100%;
|
||||
background-color: #3D4151;
|
||||
padding-top: 3px;
|
||||
|
||||
&-line {
|
||||
width: 100%;
|
||||
height: 0.5px;
|
||||
}
|
||||
}
|
||||
|
||||
&-part-2 {
|
||||
height: 21px;
|
||||
width: 100%;
|
||||
background-color: #86909C;
|
||||
border-bottom: solid 1px #FFFFFF;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
|
||||
&-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 5px;
|
||||
margin-right: 7px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&-right {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px;
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
overflow: hidden;
|
||||
|
||||
&-item {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
background-color: #C8CDD2;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div class="circle-container">
|
||||
<div class="circle shadow-1"></div>
|
||||
<div class="circle shadow-2"></div>
|
||||
<div class="circle shadow-3"></div>
|
||||
<div class="circle inner-circle"></div>
|
||||
<div class="circle inner-shadow"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'RackHeaderCircle'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.circle-container {
|
||||
position: relative;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
|
||||
.circle {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
background-color: #20e757;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.shadow-1 {
|
||||
filter: blur(4px);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.shadow-2 {
|
||||
filter: blur(2px);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.shadow-3 {
|
||||
filter: blur(1px);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.inner-circle {
|
||||
background-color: #6cffe5;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.inner-shadow {
|
||||
background-color: #6affe4;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
filter: blur(1px);
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,575 @@
|
||||
<template>
|
||||
<div class="rack-container">
|
||||
<div class="rack-title">
|
||||
<ops-icon
|
||||
:type="titleData.icon"
|
||||
class="rack-title-icon"
|
||||
/>
|
||||
<span
|
||||
class="rack-title-text"
|
||||
>
|
||||
{{ $t(titleData.text) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<RackHeader :viewType="viewType" />
|
||||
|
||||
<div
|
||||
class="rack-container-main"
|
||||
:style="{
|
||||
flexDirection: viewType === 'front' ? 'row' : 'row-reverse'
|
||||
}"
|
||||
>
|
||||
<div class="rack-container-main-left">
|
||||
<div
|
||||
v-for="(item, index) in countList"
|
||||
:key="index"
|
||||
class="rack-container-main-left-count"
|
||||
:style="{
|
||||
backgroundColor: item % 2 === 0 ? '#3D4151' : '#5E6772',
|
||||
height: unitHeight + 'px',
|
||||
lineHeight: unitHeight + 'px'
|
||||
}"
|
||||
>
|
||||
{{ item }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rack-container-main-list">
|
||||
<draggable
|
||||
filter=".undraggable"
|
||||
:list="unitList"
|
||||
@start="handleDraggableStart"
|
||||
@end="handleDraggableEnd"
|
||||
>
|
||||
<div
|
||||
v-for="(item, index) in unitList"
|
||||
:key="item.key"
|
||||
:class="[item.type === 'gap' || item.abnormal ? 'undraggable' : '']"
|
||||
>
|
||||
<div
|
||||
v-if="item.type === 'device'"
|
||||
:class="['rack-container-main-list-device', item.abnormal ? '' : 'rack-container-main-list-device_normal']"
|
||||
:style="{
|
||||
height: unitHeight * item.unitCount + 'px'
|
||||
}"
|
||||
@click="clickDevice(item)"
|
||||
>
|
||||
<div class="rack-container-main-list-device-action">
|
||||
<div
|
||||
class="rack-container-main-list-device-action-btn"
|
||||
@click.stop="removeDevice(item)"
|
||||
>
|
||||
{{ $t('cmdb.dcim.remove') }}
|
||||
</div>
|
||||
<div
|
||||
class="rack-container-main-list-device-action-btn"
|
||||
@click.stop="migrateDevice(item)"
|
||||
>
|
||||
{{ $t('cmdb.dcim.migrate') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="item.abnormal"
|
||||
class="rack-container-main-list-device-abnormal"
|
||||
>
|
||||
<span
|
||||
class="rack-container-main-list-device-abnormal-text"
|
||||
>
|
||||
{{ $t('cmdb.dcim.unitAbnormal') }}
|
||||
</span>
|
||||
<a-icon
|
||||
type="right"
|
||||
class="rack-container-main-list-device-abnormal-icon"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="rack-container-main-list-device-header"></div>
|
||||
<img
|
||||
v-for="(unitIndex) in item.unitCount"
|
||||
:key="unitIndex"
|
||||
:src="item.deviceImage[viewType]"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="rack-container-main-list-device-sider"
|
||||
:style="{
|
||||
right: viewType === 'front' ? '-154px' : '-157px'
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-for="(nameItem, nameIndex) in getNameList(item)"
|
||||
:key="nameIndex"
|
||||
class="rack-container-main-list-device-name"
|
||||
@click.stop="openDeviceDetail(nameItem)"
|
||||
>
|
||||
<CIIcon size="14" :icon="nameItem.icon" />
|
||||
<span class="rack-container-main-list-device-name-text">{{ nameItem.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="item.type === 'gap'"
|
||||
:class="['rack-container-main-list-gap', viewType === 'rear' ? 'rack-container-main-list-gap_rear' : '']"
|
||||
:style="{
|
||||
height: unitHeight + 'px'
|
||||
}"
|
||||
@click="addDevice(index)"
|
||||
>
|
||||
<ops-icon
|
||||
type="monitor-add"
|
||||
class="rack-container-main-list-gap-icon"
|
||||
/>
|
||||
<span
|
||||
class="rack-container-main-list-gap-text"
|
||||
>
|
||||
{{ $t('cmdb.dcim.addDevice') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</draggable>
|
||||
</div>
|
||||
|
||||
<div class="rack-container-main-right">
|
||||
<div class="rack-container-main-right-part-1"></div>
|
||||
<div class="rack-container-main-right-part-2"></div>
|
||||
|
||||
<img
|
||||
v-if="viewType === 'front'"
|
||||
:src="require(`@/modules/cmdb/assets/dcim/rack_front_part.png`)"
|
||||
class="rack-container-main-right-part-3"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rack-container-footer">
|
||||
<template v-if="viewType === 'front'">
|
||||
<div class="rack-container-footer-dot"></div>
|
||||
<div class="rack-container-footer-dot"></div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<AbnormalModal
|
||||
ref="abnormalModalRef"
|
||||
@ok="editDevice"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { deleteDevice } from '@/modules/cmdb/api/dcim.js'
|
||||
|
||||
import RackHeader from './rackHeader/index.vue'
|
||||
import draggable from 'vuedraggable'
|
||||
import CIIcon from '@/modules/cmdb/components/ciIcon/index.vue'
|
||||
import AbnormalModal from './abnormalModal.vue'
|
||||
|
||||
export default {
|
||||
name: 'RackUnitView',
|
||||
components: {
|
||||
RackHeader,
|
||||
draggable,
|
||||
CIIcon,
|
||||
AbnormalModal
|
||||
},
|
||||
props: {
|
||||
viewType: {
|
||||
type: String,
|
||||
default: 'front'
|
||||
},
|
||||
countList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
unitList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
rackId: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
oldDraggableList: [],
|
||||
draggableDevice: {},
|
||||
|
||||
unitHeight: 24
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
titleData() {
|
||||
return {
|
||||
icon: this.viewType === 'front' ? 'veops-front' : 'veops-rear',
|
||||
text: this.viewType === 'front' ? 'cmdb.dcim.frontView' : 'cmdb.dcim.rearView'
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addDevice(index) {
|
||||
const sliceUnitList = this.unitList.slice(0, index)
|
||||
const unitCount = sliceUnitList.reduce((acc, cur) => acc + cur.unitCount, 0)
|
||||
|
||||
this.$emit('openDeviceForm', {
|
||||
unitStart: this.countList.length - unitCount
|
||||
})
|
||||
},
|
||||
|
||||
editDevice(data) {
|
||||
this.$emit('openDeviceForm', {
|
||||
CITypeId: data?._type,
|
||||
deviceId: data?.id,
|
||||
unitStart: data?.u_start,
|
||||
unitCount: data?.u_count,
|
||||
name: data?.name
|
||||
})
|
||||
},
|
||||
|
||||
handleDraggableStart(e) {
|
||||
this.oldDraggableList = _.cloneDeep(this.unitList)
|
||||
this.draggableDevice = this.oldDraggableList?.[e.oldIndex] || {}
|
||||
},
|
||||
|
||||
handleDraggableEnd(e) {
|
||||
if (e.newIndex === e.oldIndex) {
|
||||
return
|
||||
}
|
||||
|
||||
const sliceUnitList = this.unitList.slice(0, e.newIndex)
|
||||
const unitCount = sliceUnitList.reduce((acc, cur) => acc + cur.unitCount, 0)
|
||||
|
||||
/**
|
||||
* 拖拽后的起始U位 = 总U数 - 该设备以上的U数 - 该设备U数 + 1
|
||||
*/
|
||||
const startUnit = this.countList.length - unitCount - this.draggableDevice.unitCount + 1
|
||||
|
||||
if (this?.draggableDevice?.id) {
|
||||
this.$emit('draggable', {
|
||||
startUnit,
|
||||
deviceId: this.draggableDevice.id,
|
||||
oldUnitList: this.oldDraggableList
|
||||
})
|
||||
}
|
||||
|
||||
this.draggableDevice = {}
|
||||
this.oldDraggableList = []
|
||||
},
|
||||
|
||||
getNameList(item) {
|
||||
const nameList = [item]
|
||||
|
||||
if (item?.abnormalList?.length) {
|
||||
nameList.push(...item.abnormalList)
|
||||
}
|
||||
|
||||
return nameList
|
||||
},
|
||||
|
||||
clickDevice(data) {
|
||||
if (data.abnormal) {
|
||||
this.$refs.abnormalModalRef.open(data)
|
||||
}
|
||||
},
|
||||
|
||||
removeDevice(data) {
|
||||
const content = this.$t('cmdb.dcim.removeDeviceTip', {
|
||||
deviceName: `${data.CITypeName} ${data.name}`
|
||||
})
|
||||
|
||||
this.$confirm({
|
||||
title: this.$t('warning'),
|
||||
content,
|
||||
onOk: () => {
|
||||
deleteDevice(
|
||||
this.rackId,
|
||||
data.id
|
||||
).then(() => {
|
||||
this.$message.success(this.$t('deleteSuccess'))
|
||||
this.$emit('refreshRackAllData')
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
migrateDevice(data) {
|
||||
this.$emit('migrateDevice', data.id)
|
||||
},
|
||||
|
||||
openDeviceDetail(deviceData) {
|
||||
this.$emit('openDeviceDetail', deviceData)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.rack-container {
|
||||
width: 236px;
|
||||
|
||||
.rack-title {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 14px;
|
||||
|
||||
&-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&-text {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #4E5969;
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
&-main {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
||||
&-left {
|
||||
min-width: 17px;
|
||||
flex-shrink: 0;
|
||||
z-index: 2;
|
||||
|
||||
&-count {
|
||||
width: 100%;
|
||||
border-bottom: solid 1px rgba(116, 138, 171, 0.25);
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
}
|
||||
|
||||
&-list {
|
||||
width: 100%;
|
||||
|
||||
&-device {
|
||||
background-color: #2C2D31;
|
||||
border-bottom: solid 1px rgba(116, 138, 171, 0.25);
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
|
||||
&-header {
|
||||
width: 195px;
|
||||
height: 6px;
|
||||
clip-path: polygon(20px 0, 175px 0, 195px 100%, 0px 100%);
|
||||
background-color: #5D6271;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 195px;
|
||||
height: 17px;
|
||||
}
|
||||
|
||||
&-action {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px solid #10D4FF;
|
||||
background: linear-gradient(90deg, rgba(0, 0, 0, 0.80) 0%, rgba(102, 102, 102, 0.80) 100%);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&-btn {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #FFFFFF;
|
||||
padding: 0 10px;
|
||||
cursor: pointer;
|
||||
|
||||
&:not(:first-child) {
|
||||
border-left: solid 1px rgba(165, 169, 188, 0.44);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #10D4FF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-abnormal {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px solid #F00;
|
||||
background-color: rgba(128, 47, 47, 0.66);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
|
||||
&-text {
|
||||
color: #FFFFFF;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&-icon {
|
||||
color: #FFFFFF;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
&-text {
|
||||
margin-left: 3px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #1D2129;
|
||||
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.rack-container-main-list-device-name-text {
|
||||
color: #3F75FF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-sider {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 140px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
row-gap: 6px;
|
||||
padding-left: 7px;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 5%;
|
||||
left: 0;
|
||||
width: 4px;
|
||||
height: 90%;
|
||||
border: solid 1px #10D4FF;
|
||||
border-left: none;
|
||||
}
|
||||
}
|
||||
|
||||
&_normal:hover {
|
||||
.rack-container-main-list-device-action {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-gap {
|
||||
width: 100%;
|
||||
border-bottom: solid 1px rgba(116, 138, 171, 0.25);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
background-color: #EBEFF8;
|
||||
|
||||
&-icon {
|
||||
font-size: 12px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
&-text {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: rgba(0, 87, 255, 0.80);
|
||||
margin-left: 6px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
&_rear {
|
||||
background-color: #CACDD9;
|
||||
border-bottom: solid 1px #E4E7ED;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #D5DDEE;
|
||||
|
||||
.rack-container-main-list-gap-icon {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.rack-container-main-list-gap-text {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-right {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
background-color: #86909C;
|
||||
position: relative;
|
||||
|
||||
&-part-1 {
|
||||
width: 7px;
|
||||
height: 100%;
|
||||
border-right: solid 1px rgba(255, 255, 255, 0.33);
|
||||
}
|
||||
|
||||
&-part-2 {
|
||||
width: 7px;
|
||||
height: 100%;
|
||||
background: linear-gradient(270deg, rgba(134, 144, 156, 0.00) 0%, rgba(69, 78, 89, 0.88) 100%);
|
||||
filter: blur(0.25px);
|
||||
}
|
||||
|
||||
&-part-3 {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 21px;
|
||||
height: 57.6px;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-footer {
|
||||
height: 12px;
|
||||
width: 100%;
|
||||
background-color: #86909C;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0px 14px;
|
||||
|
||||
&-dot {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 4px;
|
||||
background-color: #E8EBEE;
|
||||
border: solid 1px #FFFFFF;
|
||||
box-shadow: 3px 3px 7px 0px rgba(136, 150, 163, 0.58) inset, -3px -3px 7px 0px #FFF inset;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
36
cmdb-ui/src/modules/cmdb/views/dcim/constants.js
Normal file
@@ -0,0 +1,36 @@
|
||||
export const DCIM_TYPE = {
|
||||
REGION: 'region',
|
||||
IDC: 'idc',
|
||||
SERVER_ROOM: 'server_room',
|
||||
RACK: 'rack'
|
||||
}
|
||||
|
||||
export const DCIM_CITYPE_NAME = {
|
||||
REGION: 'dcim_region',
|
||||
IDC: 'dcim_idc',
|
||||
SERVER_ROOM: 'dcim_server_room',
|
||||
RACK: 'dcim_rack'
|
||||
}
|
||||
|
||||
export const DEVICE_CITYPE_NAME = {
|
||||
SWITCH: 'switch',
|
||||
FC_SWITCH: 'fc_switch',
|
||||
F5: 'bigip',
|
||||
ROUTER: 'router',
|
||||
FIRE_WALL: 'firewall',
|
||||
SERVER: 'server',
|
||||
RAID: 'raid'
|
||||
}
|
||||
|
||||
const createTypeNameMap = (typeObj, typeNameObj) => {
|
||||
const map = {}
|
||||
|
||||
Object.keys(typeObj).forEach(key => {
|
||||
map[typeObj[key]] = typeNameObj[key]
|
||||
map[typeNameObj[key]] = typeObj[key]
|
||||
})
|
||||
|
||||
return map
|
||||
}
|
||||
|
||||
export const DCIM_TYPE_NAME_MAP = createTypeNameMap(DCIM_TYPE, DCIM_CITYPE_NAME)
|
267
cmdb-ui/src/modules/cmdb/views/dcim/index.vue
Normal file
@@ -0,0 +1,267 @@
|
||||
<template>
|
||||
<TwoColumnLayout
|
||||
class="dcim"
|
||||
appName="cmdb-dcim"
|
||||
calcBasedParent
|
||||
>
|
||||
<template #one>
|
||||
<DCIMTree
|
||||
:treeData="treeData"
|
||||
:treeKey="treeKey"
|
||||
@getAttrList="getAttrList"
|
||||
@updateTreeKey="updateTreeKey"
|
||||
@openForm="openForm"
|
||||
/>
|
||||
|
||||
<DCIMForm
|
||||
ref="dcimFormRef"
|
||||
:allAttrList="allAttrList"
|
||||
@ok="handleDCIMFormOk"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #two>
|
||||
<DCIMMain
|
||||
v-if="!initLoading && rackCITYpe.id"
|
||||
ref="dcimMainRef"
|
||||
:roomId="treeKey"
|
||||
:attrObj="allAttrList[DCIM_TYPE.RACK]"
|
||||
:rackCITYpe="rackCITYpe"
|
||||
:preferenceAttrList="rackPreferenceAttrList"
|
||||
@openForm="openForm"
|
||||
@refreshTreeData="getTreeData"
|
||||
/>
|
||||
</template>
|
||||
</TwoColumnLayout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getDCIMTreeView } from '@/modules/cmdb/api/dcim.js'
|
||||
import { DCIM_CITYPE_NAME, DCIM_TYPE, DCIM_TYPE_NAME_MAP } from './constants.js'
|
||||
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
|
||||
import { getCIType } from '@/modules/cmdb/api/CIType.js'
|
||||
import { getSubscribeAttributes } from '@/modules/cmdb/api/preference'
|
||||
|
||||
import TwoColumnLayout from '@/components/TwoColumnLayout'
|
||||
import DCIMTree from './components/dcimTree.vue'
|
||||
import DCIMForm from './components/dcimForm.vue'
|
||||
import DCIMMain from './components/dcimMain/index.vue'
|
||||
|
||||
const TREE_STORAGE_KEY = 'ops_dcim_tree_active'
|
||||
|
||||
export default {
|
||||
name: 'DCIM',
|
||||
components: {
|
||||
TwoColumnLayout,
|
||||
DCIMTree,
|
||||
DCIMForm,
|
||||
DCIMMain
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
DCIM_TYPE,
|
||||
treeKey: localStorage.getItem(TREE_STORAGE_KEY) || '',
|
||||
treeData: [],
|
||||
allAttrList: {
|
||||
[DCIM_TYPE.REGION]: {},
|
||||
[DCIM_TYPE.IDC]: {},
|
||||
[DCIM_TYPE.SERVER_ROOM]: {},
|
||||
[DCIM_TYPE.RACK]: {}
|
||||
},
|
||||
|
||||
initLoading: true,
|
||||
rackCITYpe: {},
|
||||
rackPreferenceAttrList: []
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
this.initLoading = true
|
||||
|
||||
try {
|
||||
await this.getTreeData()
|
||||
await this.getRackData()
|
||||
} catch (error) {
|
||||
console.log('initData fail', error)
|
||||
}
|
||||
|
||||
this.initLoading = false
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
getTreeData: this.getTreeData
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async getTreeData() {
|
||||
const res = await getDCIMTreeView()
|
||||
let treeData = []
|
||||
|
||||
if (res?.result?.length) {
|
||||
treeData = res.result.map((data) => {
|
||||
return this.handleTreeData(data, res.type2name)
|
||||
})
|
||||
}
|
||||
|
||||
const currentNode = this.findNodeById(treeData, this.treeKey)
|
||||
if (!currentNode) {
|
||||
this.updateTreeKey('')
|
||||
}
|
||||
|
||||
const flatRreeData = []
|
||||
treeData.forEach((item) => {
|
||||
flatRreeData.push({
|
||||
...item,
|
||||
class: 'ipam-tree-node_hide_expand',
|
||||
children: []
|
||||
})
|
||||
if (item.children.length) {
|
||||
flatRreeData.push(...item.children)
|
||||
}
|
||||
})
|
||||
|
||||
this.treeData = flatRreeData
|
||||
},
|
||||
|
||||
handleTreeData(data, type2name, parentId = '') {
|
||||
const title = data?.[type2name?.[data?._type]] || ''
|
||||
const dcimType = DCIM_TYPE_NAME_MAP[data.ci_type]
|
||||
let icon = ''
|
||||
let iconColor = '#A5A9BC'
|
||||
let addType = ''
|
||||
|
||||
const key = String(data._id)
|
||||
|
||||
switch (data.ci_type) {
|
||||
case DCIM_CITYPE_NAME.REGION:
|
||||
icon = 'veops-region'
|
||||
iconColor = '#2F54EB'
|
||||
addType = DCIM_TYPE.IDC
|
||||
break
|
||||
case DCIM_CITYPE_NAME.IDC:
|
||||
icon = 'veops-IDC'
|
||||
addType = DCIM_TYPE.SERVER_ROOM
|
||||
break
|
||||
case DCIM_CITYPE_NAME.SERVER_ROOM:
|
||||
icon = 'a-veops-room1'
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if (!data?.children?.length) {
|
||||
return {
|
||||
...data,
|
||||
key,
|
||||
title,
|
||||
icon,
|
||||
iconColor,
|
||||
parentId,
|
||||
addType,
|
||||
dcimType,
|
||||
count: data?.rack_count || 0
|
||||
}
|
||||
}
|
||||
|
||||
const children = data.children.map((item) => {
|
||||
return this.handleTreeData(item, type2name, key)
|
||||
})
|
||||
|
||||
return {
|
||||
...data,
|
||||
key,
|
||||
title,
|
||||
icon,
|
||||
iconColor,
|
||||
addType,
|
||||
parentId,
|
||||
children,
|
||||
dcimType,
|
||||
count: children.reduce((acc, item) => {
|
||||
return acc + item.count
|
||||
}, 0)
|
||||
}
|
||||
},
|
||||
|
||||
findNodeById(nodes, id) {
|
||||
for (const node of nodes) {
|
||||
if (node.key === id) {
|
||||
return node
|
||||
}
|
||||
if (node.children) {
|
||||
const foundNode = this.findNodeById(node.children, id)
|
||||
if (foundNode) {
|
||||
return foundNode
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
async getRackData() {
|
||||
await this.getAttrList(DCIM_CITYPE_NAME.RACK, DCIM_TYPE.RACK)
|
||||
|
||||
const CITypeRes = await getCIType(DCIM_CITYPE_NAME.RACK)
|
||||
this.rackCITYpe = CITypeRes?.ci_types?.[0] || {}
|
||||
|
||||
if (this.rackCITYpe.id) {
|
||||
const subscribed = await getSubscribeAttributes(this.rackCITYpe.id)
|
||||
this.rackPreferenceAttrList = subscribed.attributes
|
||||
}
|
||||
},
|
||||
|
||||
async getAttrList(id, type, cb) {
|
||||
if (Object.keys(this?.allAttrList?.[type] || {})?.length === 0) {
|
||||
const res = await getCITypeAttributesById(id)
|
||||
this.$set(this.allAttrList, type, res || {})
|
||||
}
|
||||
|
||||
if (cb) {
|
||||
cb(this.allAttrList)
|
||||
}
|
||||
},
|
||||
|
||||
async openForm(data) {
|
||||
await this.getAttrList(DCIM_TYPE_NAME_MAP[data.dcimType], data.dcimType)
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.$refs.dcimFormRef.open(data)
|
||||
})
|
||||
},
|
||||
|
||||
updateTreeKey(key) {
|
||||
this.treeKey = key
|
||||
localStorage.setItem(TREE_STORAGE_KEY, key)
|
||||
},
|
||||
|
||||
handleDCIMFormOk({
|
||||
dcimType,
|
||||
editType
|
||||
}) {
|
||||
switch (dcimType) {
|
||||
case DCIM_TYPE.REGION:
|
||||
case DCIM_TYPE.IDC:
|
||||
case DCIM_TYPE.SERVER_ROOM:
|
||||
this.getTreeData()
|
||||
break
|
||||
case DCIM_TYPE.RACK:
|
||||
this.getRackList()
|
||||
if (editType === 'create') {
|
||||
this.getTreeData()
|
||||
}
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
},
|
||||
|
||||
getRackList() {
|
||||
if (this.$refs.dcimMainRef) {
|
||||
this.$refs.dcimMainRef.getRackList()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
</style>
|
@@ -9,6 +9,7 @@
|
||||
<div class="ipam-tree-main">
|
||||
<a-tree
|
||||
v-if="treeData.length"
|
||||
autoExpandParent
|
||||
:treeData="filterTreeData"
|
||||
:selectedKeys="treeKey ? [treeKey] : []"
|
||||
:defaultExpandedKeys="treeKey ? [treeKey] : []"
|
||||
@@ -238,7 +239,7 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
& > li:first-child {
|
||||
.ipam-tree-node-all {
|
||||
.ant-tree-switcher {
|
||||
display: none;
|
||||
}
|
||||
|
@@ -185,6 +185,7 @@ export default {
|
||||
showCatalogBtn: rootShowCatalogBtn,
|
||||
showSubnetBtn: rootShowSubnetBtn,
|
||||
parentId: '',
|
||||
class: 'ipam-tree-node-all'
|
||||
})
|
||||
|
||||
this.treeData = treeData
|
||||
|
@@ -3,6 +3,7 @@
|
||||
:visible="visible"
|
||||
:width="700"
|
||||
:title="$t('cmdb.ipam.addressAssign')"
|
||||
:confirmLoading="confirmLoading"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
@@ -17,7 +18,7 @@
|
||||
<a-form-model-item
|
||||
label="IP"
|
||||
>
|
||||
{{ ipData.ip }}
|
||||
<span class="assign-form-ip" >{{ ipList.join(', ') }}</span>
|
||||
</a-form-model-item>
|
||||
<a-form-model-item
|
||||
v-for="(item) in formList"
|
||||
@@ -80,37 +81,29 @@ export default {
|
||||
attrList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
subnetData: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
ipData: {},
|
||||
ipList: [],
|
||||
nodeId: -1,
|
||||
formList: [],
|
||||
form: {},
|
||||
formRules: {},
|
||||
statusSelectOption: [
|
||||
{
|
||||
value: 0,
|
||||
label: 'cmdb.ipam.assigned'
|
||||
},
|
||||
{
|
||||
value: 2,
|
||||
label: 'cmdb.ipam.reserved'
|
||||
}
|
||||
]
|
||||
confirmLoading: false,
|
||||
isBatch: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async open({
|
||||
ipData,
|
||||
ipList = [],
|
||||
ipData = null,
|
||||
nodeId,
|
||||
}) {
|
||||
this.isBatch = ipList.length !== 0
|
||||
this.ipList = ipList.length ? _.cloneDeep(ipList) : [ipData?.ip ?? '']
|
||||
this.ipData = ipData || {}
|
||||
this.nodeId = nodeId || -1
|
||||
this.visible = true
|
||||
@@ -127,7 +120,7 @@ export default {
|
||||
|
||||
if (attrList.length) {
|
||||
_.remove(attrList, (item) => {
|
||||
return ['subnet_mask', 'gateway', 'name', 'mac_address', 'is_used', 'ip'].includes(item.name)
|
||||
return ['subnet_mask', 'gateway', 'name', 'mac_address', 'is_used', 'ip', 'ipam_address_id'].includes(item.name)
|
||||
})
|
||||
|
||||
const assingStatusIndex = attrList.findIndex((attr) => attr.name === 'assign_status')
|
||||
@@ -237,7 +230,8 @@ export default {
|
||||
this.form = {}
|
||||
this.formRules = {}
|
||||
this.formList = []
|
||||
this.visible = false
|
||||
this.confirmLoading = false
|
||||
this.isBatch = false
|
||||
|
||||
this.$refs.assignFormRef.clearValidate()
|
||||
},
|
||||
@@ -248,16 +242,35 @@ export default {
|
||||
return
|
||||
}
|
||||
|
||||
await postIPAMAddress({
|
||||
ips: [this.ipData.ip],
|
||||
parent_id: this.nodeId,
|
||||
...this.form,
|
||||
subnet_mask: this?.ipData?.subnet_mask ?? undefined,
|
||||
gateway: this?.ipData?.gateway ?? undefined
|
||||
})
|
||||
this.confirmLoading = true
|
||||
|
||||
if (!this.isBatch) {
|
||||
await postIPAMAddress({
|
||||
ips: this.ipList,
|
||||
parent_id: this.nodeId,
|
||||
...this.form,
|
||||
subnet_mask: this?.ipData?.subnet_mask ?? undefined,
|
||||
gateway: this?.ipData?.gateway ?? undefined
|
||||
})
|
||||
|
||||
this.$emit('ok')
|
||||
} else {
|
||||
const ipChunk = _.chunk(this.ipList, 5)
|
||||
const paramsList = ipChunk.map((ips) => ({
|
||||
ips,
|
||||
parent_id: this.nodeId,
|
||||
...this.form,
|
||||
subnet_mask: this?.ipData?.subnet_mask ?? undefined,
|
||||
gateway: this?.ipData?.gateway ?? undefined
|
||||
}))
|
||||
this.$emit('batchAssign', {
|
||||
paramsList,
|
||||
ipList: this.ipList
|
||||
})
|
||||
}
|
||||
|
||||
this.$emit('ok')
|
||||
this.handleCancel()
|
||||
this.confirmLoading = false
|
||||
})
|
||||
},
|
||||
|
||||
@@ -280,5 +293,12 @@ export default {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
&-ip {
|
||||
max-height: 100px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -9,12 +9,11 @@
|
||||
<div class="address-null-tip2">{{ $t(addressNullTip) }}</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="loading" class="address-loading">
|
||||
<a-icon type="loading" class="address-loading-icon" />
|
||||
<span class="address-loading-text">{{ $t('loading') }}</span>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<a-spin
|
||||
v-else
|
||||
:tip="loadTip"
|
||||
:spinning="loading"
|
||||
>
|
||||
<div class="address-header">
|
||||
<div class="address-header-left">
|
||||
<a-input-search
|
||||
@@ -53,6 +52,15 @@
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
|
||||
<div v-if="selectedIPList.length" class="ops-list-batch-action">
|
||||
<span @click="clickBatchAssign">{{ $t('cmdb.ipam.batchAssign') }}</span>
|
||||
<a-divider type="vertical" />
|
||||
<span @click="clickBatchRecycle">{{ $t('cmdb.ipam.batchRecycle') }}</span>
|
||||
<a-divider type="vertical" />
|
||||
<span @click="handleExport">{{ $t('export') }}</span>
|
||||
<span>{{ $t('cmdb.ci.selectRows', { rows: selectedIPList.length }) }}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="currentLayout === 'grid'"
|
||||
class="address-header-status"
|
||||
@@ -85,22 +93,12 @@
|
||||
</div>
|
||||
|
||||
<div class="address-header-right">
|
||||
<a-button
|
||||
type="primary"
|
||||
class="ops-button-ghost"
|
||||
ghost
|
||||
@click="handleExport"
|
||||
>
|
||||
<ops-icon type="veops-export" />
|
||||
{{ $t('export') }}
|
||||
</a-button>
|
||||
|
||||
<div class="address-header-layout">
|
||||
<div
|
||||
v-for="(item) in layoutList"
|
||||
:key="item.value"
|
||||
:class="['address-header-layout-item', currentLayout === item.value ?'address-header-layout-item-active' : '']"
|
||||
@click="currentLayout = item.value"
|
||||
@click="handleChangeLayout(item.value)"
|
||||
>
|
||||
<ops-icon :type="item.icon" />
|
||||
</div>
|
||||
@@ -117,8 +115,10 @@
|
||||
:referenceShowAttrNameMap="referenceShowAttrNameMap"
|
||||
:referenceCIIdMap="referenceCIIdMap"
|
||||
:columnWidth="columnWidth"
|
||||
:addressCITypeId="addressCITypeId"
|
||||
@openAssign="openAssign"
|
||||
@recycle="handleRecycle"
|
||||
@selectChange="handleTableSelectChange"
|
||||
/>
|
||||
|
||||
<GridIP
|
||||
@@ -131,13 +131,13 @@
|
||||
@recycle="handleRecycle"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</a-spin>
|
||||
|
||||
<AssignForm
|
||||
ref="assignFormRef"
|
||||
:attrList="attrList"
|
||||
:subnetData="subnetData"
|
||||
@ok="getIPList"
|
||||
@batchAssign="batchAssign"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -183,11 +183,14 @@ export default {
|
||||
currentSelectScope: '',
|
||||
columns: [],
|
||||
attrList: [],
|
||||
attributes: {},
|
||||
subnetData: {},
|
||||
referenceShowAttrNameMap: {},
|
||||
referenceCIIdMap: {},
|
||||
columnWidth: {},
|
||||
loading: false,
|
||||
selectedIPList: [],
|
||||
loadTip: this.$t('loading'),
|
||||
|
||||
currentStatus: 'all',
|
||||
filterOption: [
|
||||
@@ -211,6 +214,17 @@ export default {
|
||||
],
|
||||
}
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
handleSearch: this.getIPList,
|
||||
attrList: () => {
|
||||
return this.attrList
|
||||
},
|
||||
attributes: () => {
|
||||
return this.attributes
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
addressNullTip() {
|
||||
if (
|
||||
@@ -298,6 +312,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
async initData() {
|
||||
this.loadTip = this.$t('loading')
|
||||
this.loading = true
|
||||
try {
|
||||
await this.getColumns()
|
||||
@@ -312,11 +327,13 @@ export default {
|
||||
|
||||
async getColumns() {
|
||||
const getAttrRes = await getCITypeAttributesById(this.addressCITypeId)
|
||||
this.attributes = _.cloneDeep(getAttrRes)
|
||||
this.attrList = _.cloneDeep(getAttrRes.attributes)
|
||||
|
||||
const attrList = getAttrRes.attributes
|
||||
this.attrList = _.cloneDeep(attrList)
|
||||
|
||||
const filterAttrList = _.remove(attrList, (item) => {
|
||||
return ['ip', 'subnet_mask', 'assign_status', 'is_used', '_updated_by', '_updated_at'].includes(item.name)
|
||||
return ['ip', 'subnet_mask', 'assign_status', 'is_used', '_updated_by', '_updated_at', 'ipam_address_id'].includes(item.name)
|
||||
})
|
||||
|
||||
const columns = []
|
||||
@@ -487,9 +504,14 @@ export default {
|
||||
const totalWidth = Object.values(columnWidth).reduce((acc, cur) => acc + cur, 0)
|
||||
|
||||
if (totalWidth < wrapWidth) {
|
||||
this.columnWidth = {}
|
||||
this.columnWidth = {
|
||||
ip: 130
|
||||
}
|
||||
} else {
|
||||
this.columnWidth = columnWidth
|
||||
this.columnWidth = {
|
||||
...columnWidth,
|
||||
ip: 130
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -497,6 +519,7 @@ export default {
|
||||
let tableData = []
|
||||
if (this.currentLayout === 'table') {
|
||||
tableData = this.$refs.tableIPRef.getCheckedTableData()
|
||||
this.selectedIPList = []
|
||||
} else {
|
||||
tableData = this.filterIPList
|
||||
}
|
||||
@@ -561,6 +584,143 @@ export default {
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
handleChangeLayout(value) {
|
||||
if (this.currentLayout !== value) {
|
||||
if (value === 'grid') {
|
||||
this.selectedIPList = []
|
||||
}
|
||||
this.currentLayout = value
|
||||
}
|
||||
},
|
||||
|
||||
handleTableSelectChange(ips) {
|
||||
this.selectedIPList = ips
|
||||
},
|
||||
|
||||
clickBatchAssign() {
|
||||
this.$refs.assignFormRef.open({
|
||||
nodeId: this?.nodeData?._id,
|
||||
ipData: {
|
||||
subnet_mask: this?.subnetData?.subnet_mask ?? undefined,
|
||||
gateway: this?.subnetData?.gateway ?? undefined
|
||||
},
|
||||
ipList: this.selectedIPList
|
||||
})
|
||||
},
|
||||
|
||||
async batchAssign({
|
||||
paramsList,
|
||||
ipList
|
||||
}) {
|
||||
let successNum = 0
|
||||
let errorNum = 0
|
||||
|
||||
try {
|
||||
this.loading = true
|
||||
|
||||
this.loadTip = this.$t('cmdb.ipam.batchAssignInProgress', {
|
||||
total: ipList.length,
|
||||
successNum: successNum,
|
||||
errorNum: errorNum,
|
||||
})
|
||||
|
||||
await _.reduce(
|
||||
paramsList,
|
||||
(promiseChain, params) => {
|
||||
const ipCount = params?.ips?.length ?? 0
|
||||
|
||||
return promiseChain.then(() => {
|
||||
return postIPAMAddress(params).then(() => {
|
||||
successNum += ipCount
|
||||
}).catch(() => {
|
||||
errorNum += ipCount
|
||||
}).finally(() => {
|
||||
this.loadTip = this.$t('cmdb.ipam.batchAssignInProgress', {
|
||||
total: ipList.length,
|
||||
successNum: successNum,
|
||||
errorNum: errorNum,
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
Promise.resolve()
|
||||
)
|
||||
|
||||
if (this.$refs.tableIPRef) {
|
||||
this.$refs.tableIPRef.clearCheckbox()
|
||||
this.selectedIPList = []
|
||||
}
|
||||
this.$message.success(this.$t('cmdb.ipam.batchAssignCompleted'))
|
||||
this.loading = false
|
||||
this.getIPList()
|
||||
} catch (error) {
|
||||
console.log('error', error)
|
||||
}
|
||||
},
|
||||
|
||||
clickBatchRecycle() {
|
||||
this.$confirm({
|
||||
title: this.$t('warning'),
|
||||
content: this.$t('cmdb.ipam.recycleTip'),
|
||||
onOk: () => {
|
||||
this.handleBatchRecycle()
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
async handleBatchRecycle() {
|
||||
let successNum = 0
|
||||
let errorNum = 0
|
||||
|
||||
try {
|
||||
this.loading = true
|
||||
|
||||
this.loadTip = this.$t('cmdb.ipam.batchRecycleInProgress', {
|
||||
total: this.selectedIPList.length,
|
||||
successNum: successNum,
|
||||
errorNum: errorNum,
|
||||
})
|
||||
|
||||
const ipChunk = _.chunk(this.selectedIPList, 5)
|
||||
|
||||
await _.reduce(
|
||||
ipChunk,
|
||||
(promiseChain, ips) => {
|
||||
const ipCount = ips.length
|
||||
console.log('ipCount', ipCount, successNum, errorNum)
|
||||
return promiseChain.then(() => {
|
||||
return postIPAMAddress({
|
||||
ips,
|
||||
parent_id: this.nodeData._id,
|
||||
assign_status: 1
|
||||
}).then(() => {
|
||||
successNum += ipCount
|
||||
}).catch(() => {
|
||||
errorNum += ipCount
|
||||
}).finally(() => {
|
||||
this.loadTip = this.$t('cmdb.ipam.batchRecycleInProgress', {
|
||||
total: this.selectedIPList.length,
|
||||
successNum: successNum,
|
||||
errorNum: errorNum,
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
Promise.resolve()
|
||||
)
|
||||
|
||||
if (this.$refs.tableIPRef) {
|
||||
this.$refs.tableIPRef.clearCheckbox()
|
||||
this.selectedIPList = []
|
||||
}
|
||||
this.$message.success(this.$t('cmdb.ipam.batchRecycleCompleted'))
|
||||
this.loading = false
|
||||
this.getIPList()
|
||||
} catch (error) {
|
||||
console.log('error', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -570,7 +730,6 @@ export default {
|
||||
.address {
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
position: relative;
|
||||
|
||||
&-header {
|
||||
width: 100%;
|
||||
@@ -695,27 +854,5 @@ export default {
|
||||
color: #2F54EB;
|
||||
}
|
||||
}
|
||||
|
||||
&-loading {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
color: #000000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
|
||||
&-icon {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
&-text {
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -14,7 +14,7 @@
|
||||
class="ops-unstripe-table checkbox-hover-table"
|
||||
@checkbox-change="onSelectChange"
|
||||
@checkbox-all="onSelectChange"
|
||||
@checkbox-range-end="onSelectChange"
|
||||
@checkbox-range-end="onSelectRangeEnd"
|
||||
>
|
||||
<vxe-table-column
|
||||
align="center"
|
||||
@@ -111,6 +111,12 @@
|
||||
<a-tooltip v-else :title="$t('cmdb.ipam.assign')">
|
||||
<a @click="assignAddress(row)"><ops-icon type="monitor-add2" /></a>
|
||||
</a-tooltip>
|
||||
|
||||
<a-tooltip v-if="row._ip_status !== ADDRESS_STATUS.OFFLINE_UNASSIGNED" :title="$t('cmdb.ci.viewRelation')">
|
||||
<a @click="openRelation(row)">
|
||||
<a-icon type="retweet" />
|
||||
</a>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</vxe-table-column>
|
||||
@@ -141,6 +147,8 @@
|
||||
</template>
|
||||
</a-pagination>
|
||||
</div>
|
||||
|
||||
<CIDetailDrawer ref="detail" :typeId="addressCITypeId" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -149,8 +157,13 @@ import _ from 'lodash'
|
||||
import { mapState } from 'vuex'
|
||||
import { STATUS_COLOR, STATUS_LABEL, ADDRESS_STATUS } from './constants.js'
|
||||
|
||||
import CIDetailDrawer from '@/modules/cmdb/views/ci/modules/ciDetailDrawer.vue'
|
||||
|
||||
export default {
|
||||
name: 'TableIP',
|
||||
components: {
|
||||
CIDetailDrawer
|
||||
},
|
||||
props: {
|
||||
columns: {
|
||||
type: Array,
|
||||
@@ -171,6 +184,10 @@ export default {
|
||||
columnWidth: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
addressCITypeId: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -241,13 +258,20 @@ export default {
|
||||
}
|
||||
|
||||
if (clearCheckbox) {
|
||||
tableRef.clearCheckboxRow()
|
||||
tableRef.clearCheckboxReserve()
|
||||
this.clearCheckbox()
|
||||
}
|
||||
|
||||
return tableData
|
||||
},
|
||||
|
||||
clearCheckbox() {
|
||||
const tableRef = this.$refs?.xTable?.getVxetableRef?.()
|
||||
if (tableRef) {
|
||||
tableRef.clearCheckboxRow()
|
||||
tableRef.clearCheckboxReserve()
|
||||
}
|
||||
},
|
||||
|
||||
getReferenceAttrValue(id, col) {
|
||||
const ci = this?.referenceCIIdMap?.[col?.reference_type_id]?.[id]
|
||||
if (!ci) {
|
||||
@@ -267,8 +291,22 @@ export default {
|
||||
},
|
||||
|
||||
onSelectChange() {
|
||||
console.log('onSelectChange')
|
||||
const xTable = this.$refs.xTable.getVxetableRef()
|
||||
const records = [...xTable.getCheckboxRecords(), ...xTable.getCheckboxReserveRecords()]
|
||||
const ips = records.map((item) => item.ip)
|
||||
this.$emit('selectChange', ips)
|
||||
},
|
||||
|
||||
onSelectRangeEnd({ records }) {
|
||||
const ips = records?.map?.((item) => item.ip) || []
|
||||
this.$emit('selectChange', ips)
|
||||
},
|
||||
|
||||
openRelation(row) {
|
||||
if (row._id) {
|
||||
this.$refs.detail.create(row._id, 'tab_2', '2')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@@ -61,7 +61,7 @@
|
||||
:href="`/cmdb/cidetail/${col.reference_type_id}/${id}`"
|
||||
target="_blank"
|
||||
>
|
||||
{{ id }}
|
||||
{{ getReferenceAttrValue(id, col) }}
|
||||
</a>
|
||||
</template>
|
||||
<template #default="{row}" v-else-if="col.is_choice">
|
||||
@@ -118,6 +118,7 @@ import SearchForm from '../../../components/searchForm/SearchForm.vue'
|
||||
import CreateInstanceForm from '../../ci/modules/CreateInstanceForm.vue'
|
||||
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
|
||||
import { SUB_NET_CITYPE_NAME, SCOPE_CITYPE_NAME, ADDRESS_CITYPE_NAME } from '@/modules/cmdb/views/ipam/constants.js'
|
||||
import { getCITypes } from '@/modules/cmdb/api/CIType'
|
||||
|
||||
export default {
|
||||
name: 'AddTableModal',
|
||||
@@ -140,6 +141,9 @@ export default {
|
||||
ancestor_ids: undefined,
|
||||
attrList1: [],
|
||||
showCreateBtn: true, // 是否展示新增按钮
|
||||
|
||||
referenceShowAttrNameMap: {},
|
||||
referenceCIIdMap: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -174,6 +178,7 @@ export default {
|
||||
|
||||
await getSubscribeAttributes(this.addTypeId).then((res) => {
|
||||
this.preferenceAttrList = res.attributes // 已经订阅的全部列
|
||||
this.handleReferenceShowAttrName()
|
||||
})
|
||||
getCITypeAttributesById(this.addTypeId).then((res) => {
|
||||
this.attrList = res.attributes
|
||||
@@ -217,6 +222,8 @@ export default {
|
||||
}
|
||||
this.loading = false
|
||||
})
|
||||
|
||||
this.handleReferenceCIIdMap()
|
||||
})
|
||||
.catch(() => {
|
||||
this.loading = false
|
||||
@@ -230,6 +237,83 @@ export default {
|
||||
}
|
||||
return []
|
||||
},
|
||||
|
||||
async handleReferenceShowAttrName() {
|
||||
const needRequiredCITypeIds = this.preferenceAttrList?.filter((attr) => attr?.is_reference && attr?.reference_type_id).map((attr) => attr.reference_type_id) || []
|
||||
if (!needRequiredCITypeIds.length) {
|
||||
this.referenceShowAttrNameMap = {}
|
||||
return
|
||||
}
|
||||
|
||||
const res = await getCITypes({
|
||||
type_ids: needRequiredCITypeIds.join(',')
|
||||
})
|
||||
|
||||
const map = {}
|
||||
res.ci_types.forEach((ciType) => {
|
||||
map[ciType.id] = ciType?.show_name || ciType?.unique_name || ''
|
||||
})
|
||||
|
||||
this.referenceShowAttrNameMap = map
|
||||
},
|
||||
|
||||
async handleReferenceCIIdMap() {
|
||||
const referenceTypeCol = this.preferenceAttrList.filter((attr) => attr?.is_reference && attr?.reference_type_id) || []
|
||||
if (!this.tableData?.length || !referenceTypeCol?.length) {
|
||||
this.referenceCIIdMap = {}
|
||||
return
|
||||
}
|
||||
|
||||
const map = {}
|
||||
this.tableData.forEach((row) => {
|
||||
referenceTypeCol.forEach((col) => {
|
||||
const ids = Array.isArray(row[col.name]) ? row[col.name] : row[col.name] ? [row[col.name]] : []
|
||||
if (ids.length) {
|
||||
if (!map?.[col.reference_type_id]) {
|
||||
map[col.reference_type_id] = {}
|
||||
}
|
||||
ids.forEach((id) => {
|
||||
map[col.reference_type_id][id] = {}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (!Object.keys(map).length) {
|
||||
this.referenceCIIdMap = {}
|
||||
return
|
||||
}
|
||||
|
||||
const allRes = await Promise.all(
|
||||
Object.keys(map).map((key) => {
|
||||
return searchCI({
|
||||
q: `_type:${key},_id:(${Object.keys(map[key]).join(';')})`,
|
||||
count: 9999
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
allRes.forEach((res) => {
|
||||
res.result.forEach((item) => {
|
||||
if (map?.[item._type]?.[item._id]) {
|
||||
map[item._type][item._id] = item
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
this.referenceCIIdMap = map
|
||||
},
|
||||
|
||||
getReferenceAttrValue(id, col) {
|
||||
const ci = this?.referenceCIIdMap?.[col?.reference_type_id]?.[id]
|
||||
if (!ci) {
|
||||
return id
|
||||
}
|
||||
|
||||
const attrName = this.referenceShowAttrNameMap?.[col.reference_type_id]
|
||||
return ci?.[attrName] || id
|
||||
},
|
||||
|
||||
onSelectChange() {},
|
||||
handleClose() {
|
||||
this.$refs.xTable.clearCheckboxRow()
|
||||
|
@@ -41,7 +41,7 @@ services:
|
||||
- redis
|
||||
|
||||
cmdb-api:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.4.14
|
||||
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.4.16
|
||||
container_name: cmdb-api
|
||||
env_file:
|
||||
- .env
|
||||
@@ -64,14 +64,14 @@ services:
|
||||
gunicorn --workers=4 autoapp:app -b 0.0.0.0:5000 -D
|
||||
|
||||
celery -A celery_worker.celery worker -E -Q one_cmdb_async --autoscale=4,1 --logfile=one_cmdb_async.log -D
|
||||
celery -A celery_worker.celery worker -E -Q acl_async --logfile=one_acl_async.log --autoscale=2,1 -D
|
||||
celery -A celery_worker.celery worker -E -Q acl_async --logfile=one_acl_async.log --autoscale=4,1 -D
|
||||
|
||||
nohup flask cmdb-trigger > trigger.log 2>&1 &
|
||||
flask cmdb-init-cache
|
||||
flask cmdb-init-acl
|
||||
flask init-import-user-from-acl
|
||||
flask init-department
|
||||
nohup flask cmdb-patch -v 2.4.14 &
|
||||
nohup flask cmdb-patch -v 2.4.16 &
|
||||
flask cmdb-counter > counter.log 2>&1
|
||||
networks:
|
||||
new:
|
||||
@@ -84,7 +84,7 @@ services:
|
||||
test: "ps aux|grep -v grep|grep -v '1 root'|grep gunicorn || exit 1"
|
||||
|
||||
cmdb-ui:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.4.14
|
||||
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.4.16
|
||||
container_name: cmdb-ui
|
||||
depends_on:
|
||||
cmdb-api:
|
||||
|
@@ -1,8 +1,5 @@
|
||||
|
||||
<p align="center">
|
||||
<a href="https://veops.cn"><img src="images/logo.png" alt="维易CMDB" width="300"/></a>
|
||||
</p>
|
||||
<h3 align="center">Simple, lightweight, and versatile operational CMDB</h3>
|
||||
<h2 align="center">Simple, lightweight, and versatile operational CMDB</h2>
|
||||
<p align="center">
|
||||
<a href="https://github.com/veops/cmdb/blob/master/LICENSE"><img src="https://img.shields.io/badge/License-AGPLv3-brightgreen" alt="License: GPLv3"></a>
|
||||
<a href="https:https://github.com/sendya/ant-design-pro-vue"><img src="https://img.shields.io/badge/UI-Ant%20Design%20Pro%20Vue-brightgreen" alt="UI"></a>
|
||||
@@ -24,17 +21,6 @@
|
||||
|
||||
## Overview
|
||||
|
||||
### System Overview
|
||||
|
||||
<img src=images/dashboard.png />
|
||||
|
||||
[View more screenshots](screenshot.md)
|
||||
|
||||
### Document
|
||||
|
||||
- <a href="https://zhuanlan.zhihu.com/p/98453732" target="_blank">Design Document</a>
|
||||
- <a href="https://github.com/veops/cmdb/tree/master/docs/cmdb_api.md" target="_blank">API Documentation</a>
|
||||
- <a href="https://mp.weixin.qq.com/s/EflmmJ-qdUkddTx2hRt3pA" target="_blank">Practice of Tree View</a>
|
||||
|
||||
### Features
|
||||
|
||||
@@ -51,12 +37,12 @@
|
||||
|
||||
### Main Features
|
||||
|
||||
- Model attributes support indexing, multiple values, default sorting, font color, and computed properties.
|
||||
- Support automatic discovery, scheduled inspections, and file import.
|
||||
- Support resource, tree view, and relationship view display.
|
||||
- Support configuration and display of relationships between models.
|
||||
- Custom models and model relationships, with model attributes supporting advanced features such as dropdown lists, font colors, and computed attributes.
|
||||
- Support for automatic discovery of computers, network devices, storage devices, databases, middleware, public cloud resources, etc.
|
||||
- Support for displaying resource, hierarchy, and relationship views.
|
||||
- Fine-grained access control and comprehensive operation logs.
|
||||
- Support cross-model search.
|
||||
- General resource and relationship search capabilities.
|
||||
- Support for IP Address Management (IPAM) and Data Center Infrastructure Management (DCIM).
|
||||
|
||||
|
||||
|
||||
@@ -68,7 +54,7 @@
|
||||
|
||||
### One-Click Docker Quick Build
|
||||
|
||||
> Method 1
|
||||
[//]: # (> Method 1)
|
||||
- step 1: **Prepare: install Docker and Docker Compose (v2)**
|
||||
- step 2: copy the repository
|
||||
```shell
|
||||
@@ -78,13 +64,20 @@ git clone https://github.com/veops/cmdb.git
|
||||
```
|
||||
docker compose up -d
|
||||
```
|
||||
> Method 2 Usefull for linux os.
|
||||
- step 1: **Prepare: install Docker and Docker Compose (v2)**
|
||||
- step 2: directly use the install.sh file in the project's root directory to `install`, `start`, `pause`, `status`, `delete`, and `uninstall` the application.
|
||||
```shell
|
||||
curl -so install.sh https://raw.githubusercontent.com/veops/cmdb/master/install.sh
|
||||
sh install.sh install
|
||||
```
|
||||
|
||||
[//]: # (> M**ethod 2 Usefull for linux os.)
|
||||
|
||||
[//]: # (- step 1: **Prepare: install Docker and Docker Compose (v2)**)
|
||||
|
||||
[//]: # (- step 2: directly use the install.sh file in the project's root directory to `install`, `start`, `pause`, `status`, `delete`, and `uninstall` the application. )
|
||||
|
||||
[//]: # (```shell)
|
||||
|
||||
[//]: # (curl -so install.sh https://raw.githubusercontent.com/veops/cmdb/master/install.sh )
|
||||
|
||||
[//]: # (sh install.sh install)
|
||||
|
||||
[//]: # (```**)
|
||||
|
||||
|
||||
### [Local Setup](local_en.md)
|
||||
@@ -100,13 +93,9 @@ sh install.sh install
|
||||
## Contributing
|
||||
|
||||
1. Fork it
|
||||
1. Create your feature branch (`git checkout -b my-feature`)
|
||||
1. Commit your changes (`git commit -am 'Add some feature'`)
|
||||
1. Push to the branch (`git push origin my-feature`)
|
||||
1. Create new Pull Request
|
||||
2. Create your feature branch (`git checkout -b my-feature`)
|
||||
3. Commit your changes (`git commit -am 'Add some feature'`)
|
||||
4. Push to the branch (`git push origin my-feature`)
|
||||
5. Create new Pull Request
|
||||
|
||||
---
|
||||
|
||||
_**Welcome to pay attention to our public account, click to contact us, join WeChat, QQ operation and maintenance group, and get more product and industry related information**_
|
||||
|
||||

|
||||
|
Before Width: | Height: | Size: 260 KiB |
Before Width: | Height: | Size: 98 KiB |