mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-03 12:46:06 +08:00
Compare commits
583
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab3f9ff4ef | ||
|
|
ed2b6f2551 | ||
|
|
72282ae50e | ||
|
|
8fba4e7050 | ||
|
|
fbd4a1108f | ||
|
|
823a88c85b | ||
|
|
3a2c6e3a3d | ||
|
|
be6e9b5f06 | ||
|
|
a88bed9eb4 | ||
|
|
f97f14c811 | ||
|
|
f8a29a1c54 | ||
|
|
9a935434c2 | ||
|
|
afe632b723 | ||
|
|
d050f21f9a | ||
|
|
b87f367583 | ||
|
|
92aa8ec603 | ||
|
|
e013534373 | ||
|
|
d23c3d3b72 | ||
|
|
719c33fa12 | ||
|
|
a6bd7ec00e | ||
|
|
ac26fe2f0e | ||
|
|
790cade04d | ||
|
|
2903e86c02 | ||
|
|
2d747e435a | ||
|
|
eadda2a18b | ||
|
|
a178302f73 | ||
|
|
d376bfb3a5 | ||
|
|
7a84e08f99 | ||
|
|
3d39626b93 | ||
|
|
1c3b40b631 | ||
|
|
87baf75301 | ||
|
|
a10d651827 | ||
|
|
11ff7c8a7c | ||
|
|
f6dffe9816 | ||
|
|
b813bc578e | ||
|
|
7c3b2730ab | ||
|
|
429d9e18ad | ||
|
|
dc166d8fc9 | ||
|
|
3ff4534294 | ||
|
|
d4223ad2ae | ||
|
|
44297ada16 | ||
|
|
aea53fdd65 | ||
|
|
9640c87947 | ||
|
|
2cefd7cb78 | ||
|
|
fb6ffa3c0f | ||
|
|
e0436bff28 | ||
|
|
219f39ae52 | ||
|
|
41cf44d231 | ||
|
|
7d09cb7142 | ||
|
|
560643d299 | ||
|
|
c3bb0ea4be | ||
|
|
3c37f04ef6 | ||
|
|
d98db6ba49 | ||
|
|
531ed03796 | ||
|
|
59f260eab8 | ||
|
|
495e80267d | ||
|
|
479c298f4a | ||
|
|
3fdde25d17 | ||
|
|
5090d8ecad | ||
|
|
b9334bafb7 | ||
|
|
9a675da756 | ||
|
|
8c0c36e022 | ||
|
|
140a85f6a8 | ||
|
|
e62ae7e1d8 | ||
|
|
d03cff645b | ||
|
|
34dfe4027e | ||
|
|
135d520b8d | ||
|
|
62a19ee3fb | ||
|
|
64f0f5fb87 | ||
|
|
93a692ff0c | ||
|
|
5e3f12de35 | ||
|
|
f115261889 | ||
|
|
5985913f5e | ||
|
|
661ab995f6 | ||
|
|
d3f63df883 | ||
|
|
44ef9195eb | ||
|
|
b18aebbbd4 | ||
|
|
d2db23d69a | ||
|
|
55918232a6 | ||
|
|
ce2adbdc3b | ||
|
|
ae65037083 | ||
|
|
b4ea7c8f98 | ||
|
|
836b04f222 | ||
|
|
0908c7fc6b | ||
|
|
b34d618c7c | ||
|
|
8945b9f158 | ||
|
|
c5af2b576b | ||
|
|
45872cef3b | ||
|
|
3e5ee422ce | ||
|
|
75202a9e55 | ||
|
|
5f5ce291fb | ||
|
|
a27e38fc81 | ||
|
|
89e1484884 | ||
|
|
eacca055c7 | ||
|
|
56c1738055 | ||
|
|
8f4b335cae | ||
|
|
91232beffc | ||
|
|
09e8946cc3 | ||
|
|
e70c684382 | ||
|
|
1446aac168 | ||
|
|
8e65274b0c | ||
|
|
e5432210b9 | ||
|
|
af52df0ba0 | ||
|
|
e5a1929ac5 | ||
|
|
9de065a5c9 | ||
|
|
cd933bdbf8 | ||
|
|
5fe642a42d | ||
|
|
18c66083ab | ||
|
|
d291a35754 | ||
|
|
5cc8b4c301 | ||
|
|
032700293c | ||
|
|
8cb793e7ad | ||
|
|
2c45b71d1f | ||
|
|
5a301dcf55 | ||
|
|
1a92265d03 | ||
|
|
8f65915bad | ||
|
|
67bb6dfbd5 | ||
|
|
e74428d951 | ||
|
|
629f7df6b9 | ||
|
|
b7119d5c4c | ||
|
|
6fdd1e3954 | ||
|
|
7d0f33f31d | ||
|
|
9ce95daf76 | ||
|
|
ce72232bc3 | ||
|
|
afc0e0f531 | ||
|
|
e941cc9a90 | ||
|
|
9961523d08 | ||
|
|
c4d1018388 | ||
|
|
be64329776 | ||
|
|
35411ed87a | ||
|
|
15eb0f07f7 | ||
|
|
fb78f24fdd | ||
|
|
81ad827829 | ||
|
|
6dd9b33336 | ||
|
|
1f9e9b336b | ||
|
|
c5b4f6cd1e | ||
|
|
4ce35ce075 | ||
|
|
27c26bd782 | ||
|
|
9d698b1230 | ||
|
|
566e79eda5 | ||
|
|
42ddcfc1fe | ||
|
|
ad3decf71f | ||
|
|
8083a41df0 | ||
|
|
46b860260a | ||
|
|
62801b3b13 | ||
|
|
161e3bbd97 | ||
|
|
97e93dba0b | ||
|
|
9a8a709e56 | ||
|
|
d1949641a7 | ||
|
|
9c9f59c939 | ||
|
|
ccc14c7fb9 | ||
|
|
2ba471907e | ||
|
|
5aea476f64 | ||
|
|
519c7d9f1f | ||
|
|
a9c392c13f | ||
|
|
888ec7a50f | ||
|
|
a2486a7d38 | ||
|
|
df3d249923 | ||
|
|
23344e0c07 | ||
|
|
62b80b3cec | ||
|
|
1cf94ee550 | ||
|
|
dfaa9fbe33 | ||
|
|
94b1c24479 | ||
|
|
e0b2b9ec57 | ||
|
|
06b38a0f99 | ||
|
|
4be3634c82 | ||
|
|
de103ff91e | ||
|
|
37a91601c9 | ||
|
|
9794671ccd | ||
|
|
337f9186f1 | ||
|
|
7c187e894d | ||
|
|
c2df13ad73 | ||
|
|
93f027a57c | ||
|
|
da83f04b6c | ||
|
|
a97cb08e3f | ||
|
|
3fdc9f03a3 | ||
|
|
c8a613874a | ||
|
|
2db7ee7397 | ||
|
|
8b9b5b41b7 | ||
|
|
77599900b5 | ||
|
|
473c1ba355 | ||
|
|
708a95b67b | ||
|
|
fecb3de0cf | ||
|
|
0781bde188 | ||
|
|
0647600e88 | ||
|
|
99dbba7958 | ||
|
|
3dcc50980b | ||
|
|
ee1c3de188 | ||
|
|
915559962a | ||
|
|
b1b1a31c04 | ||
|
|
7f52562d22 | ||
|
|
b140c05697 | ||
|
|
ad9fef63c0 | ||
|
|
786e271ac6 | ||
|
|
c169853e5a | ||
|
|
1352246e1e | ||
|
|
49374bb18a | ||
|
|
fe3a12e28c | ||
|
|
a86302c9d5 | ||
|
|
5fe7e13fcd | ||
|
|
c079984047 | ||
|
|
5769588633 | ||
|
|
b29950f99c | ||
|
|
6c27a46274 | ||
|
|
b03fffbc30 | ||
|
|
5b668a176c | ||
|
|
b4019a2c86 | ||
|
|
416098ffd8 | ||
|
|
3d7b96d86d | ||
|
|
0b37467f76 | ||
|
|
fab7fbec5e | ||
|
|
d94003f8c2 | ||
|
|
dbb5b01cc3 | ||
|
|
467933bda0 | ||
|
|
1b183febdc | ||
|
|
70d3b656f7 | ||
|
|
10d19cd329 | ||
|
|
87e44cb658 | ||
|
|
1715ae754c | ||
|
|
30de2b1a18 | ||
|
|
6fedcc353c | ||
|
|
f9bff21e84 | ||
|
|
6735826e15 | ||
|
|
9e17e239cf | ||
|
|
a1cf41bda9 | ||
|
|
db58024351 | ||
|
|
81bf19419c | ||
|
|
6f90cae482 | ||
|
|
b4980a51e7 | ||
|
|
756ed383ac | ||
|
|
7e0a9dcd80 | ||
|
|
5fae6c3087 | ||
|
|
a994d9861e | ||
|
|
5d996c42dc | ||
|
|
88cd302ce4 | ||
|
|
e6dd8d6771 | ||
|
|
1a3822881e | ||
|
|
ee8bb21a10 | ||
|
|
e3d30a0eec | ||
|
|
532b6c4d50 | ||
|
|
f22644e058 | ||
|
|
cd128d0444 | ||
|
|
0278f51260 | ||
|
|
98efd63110 | ||
|
|
78e78ac8dd | ||
|
|
2aaa0a55a2 | ||
|
|
5c2a23924d | ||
|
|
7af8a82838 | ||
|
|
3b343bcaca | ||
|
|
6ddd35d661 | ||
|
|
bd28ba6f71 | ||
|
|
4bb67153ce | ||
|
|
7dbd442519 | ||
|
|
5a0d7f283d | ||
|
|
9fdbe9c58f | ||
|
|
f652ed9d4b | ||
|
|
21a1cd4e89 | ||
|
|
facc74ca51 | ||
|
|
74c12a0d1b | ||
|
|
3277ab4a2b | ||
|
|
4aabf0b7a3 | ||
|
|
4e8cb31e9d | ||
|
|
1b214b2b6f | ||
|
|
748c88866a | ||
|
|
c3e5c2161e | ||
|
|
f932afee68 | ||
|
|
0bbdf3bf17 | ||
|
|
bdf3ac2803 | ||
|
|
0d183a8174 | ||
|
|
8f72c33f4b | ||
|
|
22c99211f1 | ||
|
|
775330d829 | ||
|
|
18be022a9f | ||
|
|
56b4b46b5a | ||
|
|
db398ecad3 | ||
|
|
d175e3b11a | ||
|
|
905db4b12c | ||
|
|
fc31b4bb9d | ||
|
|
db29a7c31b | ||
|
|
2b3541164d | ||
|
|
ea5e9aa1f4 | ||
|
|
18a5608e72 | ||
|
|
18599fc94c | ||
|
|
e155d46483 | ||
|
|
795e5d178f | ||
|
|
2e03ac3cf7 | ||
|
|
76605fdc07 | ||
|
|
c289b001db | ||
|
|
e8d63da1eb | ||
|
|
4a456ac07d | ||
|
|
57dfddd593 | ||
|
|
66eac96575 | ||
|
|
182d92d80b | ||
|
|
1b362f570b | ||
|
|
cf798048f8 | ||
|
|
93c8399935 | ||
|
|
b72f523ddf | ||
|
|
e5ffb08df3 | ||
|
|
98a6322c11 | ||
|
|
be7e4bd80b | ||
|
|
ed2bc3100b | ||
|
|
b6ffb3e9f1 | ||
|
|
3d8a6bba7e | ||
|
|
be9311dc0c | ||
|
|
e294f4c00f | ||
|
|
a9fa928912 | ||
|
|
ef2b4d1589 | ||
|
|
a73e9f0ac1 | ||
|
|
11b3bf4c1f | ||
|
|
b775ce5f50 | ||
|
|
193d4536e3 | ||
|
|
9ca9fdb521 | ||
|
|
bcabefe9f3 | ||
|
|
c541f81dc3 | ||
|
|
4486f71c8f | ||
|
|
060105ead3 | ||
|
|
3ea73978ee | ||
|
|
f4e363396d | ||
|
|
fa16e24983 | ||
|
|
ce63ee6a19 | ||
|
|
ee28697161 | ||
|
|
85a06feb91 | ||
|
|
17dd46cee9 | ||
|
|
0ef4ff65cc | ||
|
|
cebb302730 | ||
|
|
f12717a8f4 | ||
|
|
03f54643fd | ||
|
|
7fb6d44208 | ||
|
|
dc683a50a4 | ||
|
|
be421054d8 | ||
|
|
fcef0391bb | ||
|
|
1d89aafeab | ||
|
|
7386a2deb6 | ||
|
|
26f5ebc8e2 | ||
|
|
8cd482870b | ||
|
|
57cbd799dd | ||
|
|
3694b316ab | ||
|
|
717258defd | ||
|
|
7aae6264f7 | ||
|
|
c88a083e04 | ||
|
|
52c4556e64 | ||
|
|
90cb147938 | ||
|
|
d9c97de0fd | ||
|
|
fb59b396d1 | ||
|
|
bdcea54eeb | ||
|
|
912ca7883f | ||
|
|
a89678cc81 | ||
|
|
d4d3774c3b | ||
|
|
32ebdc3acd | ||
|
|
703abd7ab9 | ||
|
|
e8f7f0e06b | ||
|
|
feddcd247a | ||
|
|
29b1ab2fb9 | ||
|
|
01528f7429 | ||
|
|
9e015ec3b1 | ||
|
|
7ecc9c320d | ||
|
|
d6da4898b4 | ||
|
|
d023104371 | ||
|
|
cb452b9627 | ||
|
|
dbbad01b0f | ||
|
|
b689e3aacf | ||
|
|
796d98e61a | ||
|
|
fe32b4372b | ||
|
|
3dcec52aff | ||
|
|
6d8f6579c7 | ||
|
|
9e240792e8 | ||
|
|
aa9dd32cce | ||
|
|
4f3e532327 | ||
|
|
cbfcf4dbae | ||
|
|
8877dcf639 | ||
|
|
8d196c4244 | ||
|
|
220e40feaa | ||
|
|
258476876a | ||
|
|
63c4810b99 | ||
|
|
0cd5651c38 | ||
|
|
6f66c80c63 | ||
|
|
0e5a78d8a6 | ||
|
|
d16802365e | ||
|
|
8410f75859 | ||
|
|
f8079c5924 | ||
|
|
1e270268d8 | ||
|
|
43064576d6 | ||
|
|
9dd36bbd87 | ||
|
|
235373045c | ||
|
|
5841508b26 | ||
|
|
b941902088 | ||
|
|
485db3066f | ||
|
|
af43ebbf6f | ||
|
|
74300f24b3 | ||
|
|
2993ddb82c | ||
|
|
a5d4604dce | ||
|
|
daa6d6942b | ||
|
|
eb92642b3b | ||
|
|
78234b9e1e | ||
|
|
3dc6dda1ca | ||
|
|
fca05d8489 | ||
|
|
238c660796 | ||
|
|
19bea97106 | ||
|
|
de4edbbf3b | ||
|
|
9256391627 | ||
|
|
25439aee65 | ||
|
|
f2fe1f5bd6 | ||
|
|
f8d5aa1a21 | ||
|
|
38cf667830 | ||
|
|
18667836c2 | ||
|
|
2940f25fe0 | ||
|
|
a24d01717a | ||
|
|
22a032e56c | ||
|
|
9343813ace | ||
|
|
2dff7b0a93 | ||
|
|
54c5d77198 | ||
|
|
1880043a80 | ||
|
|
d8bee7f4f5 | ||
|
|
90c923b480 | ||
|
|
374b904191 | ||
|
|
f2725e2773 | ||
|
|
487f9abc10 | ||
|
|
1862cfcc08 | ||
|
|
480f95a5cc | ||
|
|
b939df9b7d | ||
|
|
4e831cbc5f | ||
|
|
0283cfb2bb | ||
|
|
4e2562190c | ||
|
|
64b59a0c27 | ||
|
|
3e7b2338ff | ||
|
|
104e98e390 | ||
|
|
72415622d9 | ||
|
|
a861450874 | ||
|
|
cbd5e45200 | ||
|
|
8ad7a57ab4 | ||
|
|
966d4d6709 | ||
|
|
208f1f29f5 | ||
|
|
d923239b9d | ||
|
|
54f85963ea | ||
|
|
36257299e1 | ||
|
|
91d078068f | ||
|
|
8fbbfc90f1 | ||
|
|
efdfdbf0b8 | ||
|
|
8db4de1c4a | ||
|
|
05ff8e4f19 | ||
|
|
8f76d5ad30 | ||
|
|
ff4a865c55 | ||
|
|
30dc56cad5 | ||
|
|
c10573d24a | ||
|
|
ca7eccd688 | ||
|
|
fc606dc4d1 | ||
|
|
046947de32 | ||
|
|
6e5532e497 | ||
|
|
7e00a08e35 | ||
|
|
48ec40fe3d | ||
|
|
50901a2eaf | ||
|
|
ff439e66a9 | ||
|
|
b0b5e5f749 | ||
|
|
be2db9d37c | ||
|
|
9cd3cd6492 | ||
|
|
e07042b9c9 | ||
|
|
799252a789 | ||
|
|
36c68eefb1 | ||
|
|
dea3c098df | ||
|
|
0a782fa4d5 | ||
|
|
5905448b90 | ||
|
|
4553bdc666 | ||
|
|
7f98dfe9c8 | ||
|
|
6a3153a7ec | ||
|
|
188f058df8 | ||
|
|
629cb0c601 | ||
|
|
0c3fe22df7 | ||
|
|
f4edfb8f43 | ||
|
|
66f53c8a00 | ||
|
|
f855a0add5 | ||
|
|
7c2317fae9 | ||
|
|
50fcec0c11 | ||
|
|
4a303e514c | ||
|
|
b828c2f66d | ||
|
|
40794d89a0 | ||
|
|
a6a99520a6 | ||
|
|
e2daa1ea0c | ||
|
|
923f36a5c3 | ||
|
|
b499f62c56 | ||
|
|
9d02623b3a | ||
|
|
762ab100b9 | ||
|
|
cbe66f4660 | ||
|
|
5b39432481 | ||
|
|
9e32052329 | ||
|
|
a55353fd1b | ||
|
|
548bdd2835 | ||
|
|
0153f9d985 | ||
|
|
70a56c75b0 | ||
|
|
31a9abd5c2 | ||
|
|
6d5139a994 | ||
|
|
b92f22fa75 | ||
|
|
f9a277d893 | ||
|
|
3b054b35c6 | ||
|
|
2493287334 | ||
|
|
9af8526ef9 | ||
|
|
07e0dc0d5e | ||
|
|
fd3f5ec1e6 | ||
|
|
72c6741af1 | ||
|
|
375127a78a | ||
|
|
fc09e030a0 | ||
|
|
5f76da4629 | ||
|
|
3a3122f3a1 | ||
|
|
229cbf1f69 | ||
|
|
176946e5e9 | ||
|
|
f3d2d2f6a8 | ||
|
|
a175d1e142 | ||
|
|
26afffc2f1 | ||
|
|
5eb19106ea | ||
|
|
7b3857cb38 | ||
|
|
2232e56ba1 | ||
|
|
d023fa1de4 | ||
|
|
527d31e4d0 | ||
|
|
1651e201e8 | ||
|
|
43b30ec3c3 | ||
|
|
68ba934b5a | ||
|
|
67754427c7 | ||
|
|
cbb66175f6 | ||
|
|
84bacfaff5 | ||
|
|
e8017e14c8 | ||
|
|
d11d9d4e37 | ||
|
|
50bdd53186 | ||
|
|
5c96e90d58 | ||
|
|
5c06287648 | ||
|
|
c75755a527 | ||
|
|
e8f282395e | ||
|
|
2bb65801d9 | ||
|
|
ccd4ff2d7a | ||
|
|
ac6958fe40 | ||
|
|
426e15084e | ||
|
|
8477f0754d | ||
|
|
12cadbf667 | ||
|
|
b9fab8bb57 | ||
|
|
69e639ff96 | ||
|
|
cb4cdcb0c7 | ||
|
|
c4e9a89ba5 | ||
|
|
f2d5172224 | ||
|
|
f0a66774b8 | ||
|
|
e95955fad6 | ||
|
|
fc2be69469 | ||
|
|
fdc635955b | ||
|
|
f56f46a543 | ||
|
|
f5961f3a59 | ||
|
|
8f3a2b202d | ||
|
|
0ea3f56f2c | ||
|
|
6e9350e63d | ||
|
|
1c15ed8003 | ||
|
|
9f3e689727 | ||
|
|
526d1bb875 | ||
|
|
b0c7b1fefe | ||
|
|
6644651fe5 | ||
|
|
2d6bc65b26 | ||
|
|
3565b44e3a | ||
|
|
43b10b250e | ||
|
|
7b6efa624f | ||
|
|
d9db1263d1 | ||
|
|
10a8f18784 | ||
|
|
993706fead | ||
|
|
1098ef8f6a | ||
|
|
4480dbdeed | ||
|
|
a7b4366d7c | ||
|
|
d72857f374 | ||
|
|
57ab9e2830 | ||
|
|
8116358011 | ||
|
|
c791399a61 | ||
|
|
5bd8ad2a93 | ||
|
|
345fe636ec | ||
|
|
7e32ea453b | ||
|
|
8f3b5265de | ||
|
|
4ca6caefb9 | ||
|
|
fb7d604ed6 | ||
|
|
9a60a621de | ||
|
|
7645d6161f | ||
|
|
c3f2665912 | ||
|
|
2a58745550 | ||
|
|
69d7046bc7 | ||
|
|
692c775b51 | ||
|
|
8b43c200b2 | ||
|
|
ab71cb377c | ||
|
|
814d296d75 | ||
|
|
eb667fd56b | ||
|
|
e4b3f84905 | ||
|
|
ea68b95c26 | ||
|
|
da77762b7f |
@@ -14,3 +14,4 @@ dist-pkg
|
|||||||
pkg/harvester/index.ts
|
pkg/harvester/index.ts
|
||||||
pkg/harvester/store/*
|
pkg/harvester/store/*
|
||||||
scripts/pkgfile.js
|
scripts/pkgfile.js
|
||||||
|
tmp
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ description: Setup node and install dependencies
|
|||||||
runs:
|
runs:
|
||||||
using: 'composite'
|
using: 'composite'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/setup-node@v4
|
- name: Setup Nodejs with yarn caching
|
||||||
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
|
|||||||
+12
-17
@@ -1,19 +1,14 @@
|
|||||||
pull_request_rules:
|
pull_request_rules:
|
||||||
- name: Automatically open backport PR to release-harvester-v1.5
|
- name: Ask to resolve conflict
|
||||||
conditions:
|
conditions:
|
||||||
- base=main
|
- conflict
|
||||||
actions:
|
actions:
|
||||||
backport:
|
comment:
|
||||||
branches:
|
message: This pull request is now in conflict. Could you fix it @{{author}}? 🙏
|
||||||
- "release-harvester-v1.5"
|
|
||||||
assignees:
|
|
||||||
- "{{ author }}"
|
|
||||||
labels:
|
|
||||||
- "v1.5 backport PR"
|
|
||||||
|
|
||||||
- name: Ask to resolve conflict
|
commands_restrictions:
|
||||||
conditions:
|
backport:
|
||||||
- conflict
|
conditions:
|
||||||
actions:
|
- or:
|
||||||
comment:
|
- sender-permission>=write
|
||||||
message: This pull request is now in conflict. Could you fix it @{{author}}? 🙏
|
- sender=github-actions[bot]
|
||||||
@@ -1,19 +1,23 @@
|
|||||||
<!-- This template is for Devs to give QA details before moving the issue To-Test -->
|
|
||||||
### Summary
|
### Summary
|
||||||
|
<!-- Give the fix root cause or feature requirement, at least list what this PR changes -->
|
||||||
|
|
||||||
### PR Checklists
|
|
||||||
- Do we need to backport this PR change to the [Harvester Dashboard](https://github.com/harvester/dashboard)?
|
|
||||||
- [ ] Yes, the relevant PR is at:
|
|
||||||
- Are backend engineers aware of UI changes?
|
|
||||||
- [ ] Yes, the backend owner is:
|
|
||||||
|
|
||||||
### Related Issue #
|
### PR Checklist
|
||||||
<!-- Define findings related to the feature or bug issue. -->
|
<!-- Check Yes if there is backend PR related to this UI PR, tag the backend owner's Github account -->
|
||||||
|
- Are backend engineers aware of UI changes ?
|
||||||
|
- [ ] Yes, the backend owner is @
|
||||||
|
- [ ] No, frontend-only.
|
||||||
|
|
||||||
### Test screenshot/video
|
### Related Issue
|
||||||
<!-- Attach screenshot or video of the changes and eventual comparison if you find it necessary -->
|
<!-- Link the issue as harvester/harvester#<issue_number> -->
|
||||||
|
harvester/harvester#<issue_number>
|
||||||
|
|
||||||
### Extra technical notes summary
|
|
||||||
<!-- Outline technical changes which may pass unobserved or may help to understand the process of solving the issue -->
|
### Test Steps
|
||||||
|
<!-- Provide the test steps for reviewer to verify the issue -->
|
||||||
|
1. Go to ... page
|
||||||
|
|
||||||
|
### Test screenshot or video
|
||||||
|
<!-- Attach screenshot or video of the changes and eventual comparison -->
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# .github/release.yml
|
||||||
|
changelog:
|
||||||
|
categories:
|
||||||
|
- title: Features
|
||||||
|
labels:
|
||||||
|
- feat
|
||||||
|
- title: Bug Fixes
|
||||||
|
labels:
|
||||||
|
- fix
|
||||||
|
- title: Dependencies
|
||||||
|
labels:
|
||||||
|
- deps
|
||||||
|
- title: Performance
|
||||||
|
labels:
|
||||||
|
- perf
|
||||||
|
- title: Documentation
|
||||||
|
labels:
|
||||||
|
- docs
|
||||||
|
- title: Style
|
||||||
|
labels:
|
||||||
|
- style
|
||||||
|
- title: Build & Continuous Integration
|
||||||
|
labels:
|
||||||
|
- build
|
||||||
|
- ci
|
||||||
|
- title: Security
|
||||||
|
labels:
|
||||||
|
- security
|
||||||
|
- title: Tests
|
||||||
|
labels:
|
||||||
|
- test
|
||||||
|
- title: Refactoring
|
||||||
|
labels:
|
||||||
|
- refactor
|
||||||
|
- title: Chores
|
||||||
|
labels:
|
||||||
|
- chore
|
||||||
|
- title: Others
|
||||||
|
labels:
|
||||||
|
- other
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": [
|
||||||
|
"config:recommended",
|
||||||
|
":semanticCommitTypeAll(deps)",
|
||||||
|
":semanticCommitScopeDisabled"
|
||||||
|
],
|
||||||
|
"baseBranches": [
|
||||||
|
"main",
|
||||||
|
"release-harvester-v1.7",
|
||||||
|
"release-harvester-v1.8"
|
||||||
|
],
|
||||||
|
"automergeMajor": false,
|
||||||
|
"semanticCommits": "enabled",
|
||||||
|
"semanticCommitType": "deps",
|
||||||
|
"prHourlyLimit": 12,
|
||||||
|
"digest": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
"timezone": "Asia/Taipei",
|
||||||
|
"schedule": ["after 10am on sunday"],
|
||||||
|
"postUpdateOptions": ["yarnDedupeFewer"],
|
||||||
|
"packageRules": [
|
||||||
|
{
|
||||||
|
"matchUpdateTypes": ["digest"],
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchUpdateTypes": ["major"],
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchPackagePatterns": ["vue", "vue-router", "vuex"],
|
||||||
|
"matchUpdateTypes": ["major"],
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchPackageNames": ["@rancher/shell"],
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchUpdateTypes": ["minor"],
|
||||||
|
"groupName": "minor dependencies",
|
||||||
|
"minimumReleaseAge": "7 days",
|
||||||
|
"labels": ["minor-update"],
|
||||||
|
"reviewers": ["a110605", "houhoucoop"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchUpdateTypes": ["patch"],
|
||||||
|
"automerge": false,
|
||||||
|
"minimumReleaseAge": "7 days",
|
||||||
|
"groupName": "patch dependencies",
|
||||||
|
"labels": ["patch-update"],
|
||||||
|
"reviewers": ["a110605", "houhoucoop"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
name: "PR Management Add PR Label Collect Data"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, reopened, edited]
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- 'release-harvester-v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
collect:
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.base_ref }}
|
||||||
|
|
||||||
|
- name: Setup Nodejs and yarn install
|
||||||
|
uses: ./.github/actions/setup
|
||||||
|
|
||||||
|
- name: Extract PR label
|
||||||
|
run: |
|
||||||
|
PR_LABEL=$(node ./scripts/extract-release-label.mjs "${{ github.event.pull_request.title }}")
|
||||||
|
echo "PR_LABEL=$PR_LABEL"
|
||||||
|
{
|
||||||
|
echo "PR_NUMBER=${{ github.event.pull_request.number }}"
|
||||||
|
echo "PR_LABEL=$PR_LABEL"
|
||||||
|
} > pr-add-label-data.env
|
||||||
|
|
||||||
|
- name: Upload PR data artifact
|
||||||
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||||
|
with:
|
||||||
|
name: pr-add-label-data
|
||||||
|
path: pr-add-label-data.env
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
name: "PR Management Add PR Label"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows:
|
||||||
|
- "PR Management Add PR Label Collect Data"
|
||||||
|
types: [completed]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
auto-assign-pr-label:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- name: Download PR data artifact
|
||||||
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||||
|
with:
|
||||||
|
name: pr-add-label-data
|
||||||
|
run-id: ${{ github.event.workflow_run.id }}
|
||||||
|
github-token: ${{ github.token }}
|
||||||
|
|
||||||
|
- name: Load PR data
|
||||||
|
run: |
|
||||||
|
cat pr-add-label-data.env >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Set PR label
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
echo "PR_LABEL = $PR_LABEL"
|
||||||
|
gh pr edit "$PR_NUMBER" --repo "${{ github.repository }}" --add-label "$PR_LABEL"
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
name: "PR Management Auto Assign Collect Data"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, ready_for_review]
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- 'release-harvester-v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
collect:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: write
|
||||||
|
steps:
|
||||||
|
- name: Save PR data to artifact
|
||||||
|
run: |
|
||||||
|
{
|
||||||
|
echo "PR_NUMBER=${{ github.event.pull_request.number }}"
|
||||||
|
echo "PR_AUTHOR=${{ github.event.pull_request.user.login }}"
|
||||||
|
} > pr-auto-assign-data.env
|
||||||
|
|
||||||
|
- name: Upload PR data artifact
|
||||||
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||||
|
with:
|
||||||
|
name: pr-auto-assign-data
|
||||||
|
path: pr-auto-assign-data.env
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
name: "PR Management Auto Assign"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows:
|
||||||
|
- "PR Management Auto Assign Collect Data"
|
||||||
|
types: [completed]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
auto-assign:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||||
|
env:
|
||||||
|
REVIEWERS: "a110605,houhoucoop"
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- name: Download PR data artifact
|
||||||
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||||
|
with:
|
||||||
|
name: pr-auto-assign-data
|
||||||
|
run-id: ${{ github.event.workflow_run.id }}
|
||||||
|
github-token: ${{ github.token }}
|
||||||
|
|
||||||
|
- name: Load PR data
|
||||||
|
run: |
|
||||||
|
cat pr-auto-assign-data.env >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Auto assign PR author
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
echo "Assigning PR author: $PR_AUTHOR"
|
||||||
|
gh pr edit "$PR_NUMBER" --repo "${{ github.repository }}" --add-assignee "$PR_AUTHOR" --add-reviewer "$REVIEWERS"
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
name: "PR Management Add Labels Collect Data"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, reopened]
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- 'release-harvester-v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
collect:
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: write
|
||||||
|
steps:
|
||||||
|
- name: Save PR data to artifact
|
||||||
|
run: |
|
||||||
|
{
|
||||||
|
echo "PR_NUMBER=${{ github.event.pull_request.number }}"
|
||||||
|
echo "PR_BASE_REF=${{ github.event.pull_request.base.ref }}"
|
||||||
|
echo "PR_USER_LOGIN=${{ github.event.pull_request.user.login }}"
|
||||||
|
} > pr-backport-label-data.env
|
||||||
|
|
||||||
|
- name: Upload PR data artifact
|
||||||
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||||
|
with:
|
||||||
|
name: pr-backport-label-data
|
||||||
|
path: pr-backport-label-data.env
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
name: "PR Management Add Backport Labels"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows:
|
||||||
|
- "PR Management Add Labels Collect Data"
|
||||||
|
types: [completed]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
add-require-backport-label:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- name: Download PR data artifact
|
||||||
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||||
|
with:
|
||||||
|
name: pr-backport-label-data
|
||||||
|
run-id: ${{ github.event.workflow_run.id }}
|
||||||
|
github-token: ${{ github.token }}
|
||||||
|
|
||||||
|
- name: Load PR data
|
||||||
|
run: |
|
||||||
|
cat pr-backport-label-data.env >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Add require-backport label (main branch PRs)
|
||||||
|
if: env.PR_BASE_REF == 'main'
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo="${{ github.repository }}"
|
||||||
|
pr_number="$PR_NUMBER"
|
||||||
|
|
||||||
|
release_branches=$(gh api "repos/${repo}/branches" --paginate --jq '.[].name' | grep -E '^release-harvester-v[0-9]+\.[0-9]+$' || true)
|
||||||
|
|
||||||
|
if [[ -z "$release_branches" ]]; then
|
||||||
|
echo "No release branches found, skipping."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
latest_branch=$(echo "$release_branches" | sort -Vr | head -n1)
|
||||||
|
version="${latest_branch#release-harvester-v}"
|
||||||
|
release_tag="v${version}.0"
|
||||||
|
|
||||||
|
tags=$(gh api "repos/${repo}/releases" --paginate --jq '.[].tag_name')
|
||||||
|
if echo "$tags" | grep -Fxq "$release_tag"; then
|
||||||
|
echo "Release $release_tag already published, skipping."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
label="require backport/v${version}"
|
||||||
|
pr_labels=$(gh pr view "$pr_number" --repo "$repo" --json labels --jq '.labels[].name' || echo "")
|
||||||
|
|
||||||
|
if echo "$pr_labels" | grep -Fxq "$label"; then
|
||||||
|
echo "Label '$label' already present, skipping."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Adding label: $label"
|
||||||
|
gh pr edit "$pr_number" --repo "$repo" --add-label "$label"
|
||||||
|
|
||||||
|
- name: Add backport label (release branch PRs opened by Mergify)
|
||||||
|
if: startsWith(env.PR_BASE_REF, 'release-harvester-v')
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
IS_MERGIFY=$(echo "$PR_USER_LOGIN" | grep -iq 'mergify' && echo true || echo false)
|
||||||
|
|
||||||
|
if [[ "$IS_MERGIFY" != "true" ]]; then
|
||||||
|
echo "PR author is not Mergify, skipping."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
version="${PR_BASE_REF#release-harvester-v}"
|
||||||
|
label="backport/v${version}"
|
||||||
|
echo "Adding label: $label"
|
||||||
|
gh pr edit "$PR_NUMBER" --repo "${{ github.repository }}" --add-label "$label"
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
name: "PR Management Request Backport via Mergify Collect Data"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [closed]
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
collect:
|
||||||
|
if: github.event.pull_request.merged == true
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: write
|
||||||
|
steps:
|
||||||
|
- name: Save PR data to artifact
|
||||||
|
run: |
|
||||||
|
labels_json='${{ toJson(github.event.pull_request.labels.*.name) }}'
|
||||||
|
{
|
||||||
|
echo "PR_NUMBER=${{ github.event.pull_request.number }}"
|
||||||
|
echo "PR_LABELS=$(echo "$labels_json" | jq -r '[.[]] | join(",")')"
|
||||||
|
} > pr-backport-request-data.env
|
||||||
|
|
||||||
|
- name: Upload PR data artifact
|
||||||
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||||
|
with:
|
||||||
|
name: pr-backport-request-data
|
||||||
|
path: pr-backport-request-data.env
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
name: "PR Management Request Backport via Mergify"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows:
|
||||||
|
- "PR Management Request Backport via Mergify Collect Data"
|
||||||
|
types: [completed]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
comment-backport:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- name: Download PR data artifact
|
||||||
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||||
|
with:
|
||||||
|
name: pr-backport-request-data
|
||||||
|
run-id: ${{ github.event.workflow_run.id }}
|
||||||
|
github-token: ${{ github.token }}
|
||||||
|
|
||||||
|
- name: Load PR data
|
||||||
|
run: |
|
||||||
|
cat pr-backport-request-data.env >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Post Mergify backport command
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
pr_number="$PR_NUMBER"
|
||||||
|
repo="${{ github.repository }}"
|
||||||
|
|
||||||
|
labels=$(echo "$PR_LABELS" | tr ',' '\n')
|
||||||
|
|
||||||
|
echo "Labels on PR: $labels"
|
||||||
|
|
||||||
|
matches=$(echo "$labels" | grep -oE '^require backport/v[0-9]+\.[0-9]+$' || true)
|
||||||
|
|
||||||
|
if [[ -z "$matches" ]]; then
|
||||||
|
echo "No back-port labels found — skipping."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
branches=$(echo "$matches" \
|
||||||
|
| sed -E 's/^require backport\/v/release-harvester-v/' \
|
||||||
|
| sort -u | tr '\n' ' ')
|
||||||
|
branches=${branches%% }
|
||||||
|
|
||||||
|
cmd="@Mergifyio backport $branches"
|
||||||
|
echo "Posting Mergify command: $cmd"
|
||||||
|
gh pr comment "$pr_number" --repo "$repo" --body "$cmd"
|
||||||
@@ -11,18 +11,19 @@ defaults:
|
|||||||
working-directory: ./
|
working-directory: ./
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
check-version:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Checkout repository
|
||||||
with:
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
fetch-depth: 1
|
|
||||||
|
|
||||||
- name: Run tests
|
- name: Check package version
|
||||||
uses: ./.github/actions/lint
|
env:
|
||||||
|
TAG_VERSION: ${{ github.event.release.tag_name }}
|
||||||
|
run: ./scripts/check-version.sh $TAG_VERSION
|
||||||
|
|
||||||
build-and-push-extension-catalog:
|
build-and-push-extension-catalog:
|
||||||
needs: lint
|
needs: check-version
|
||||||
uses: ./.github/workflows/build-extension-catalog.yml
|
uses: ./.github/workflows/build-extension-catalog.yml
|
||||||
permissions:
|
permissions:
|
||||||
actions: write
|
actions: write
|
||||||
@@ -32,5 +33,4 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
registry_target: docker.io
|
registry_target: docker.io
|
||||||
registry_user: rancher
|
registry_user: rancher
|
||||||
repository_name: harvester-ui-catalog
|
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|||||||
@@ -25,12 +25,12 @@ jobs:
|
|||||||
name: Build & Upload Hosted
|
name: Build & Upload Hosted
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
# Note - Cannot use the setup action here as it uses a different yarn install arg
|
# Note - Cannot use the setup action here as it uses a different yarn install arg
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
@@ -45,19 +45,20 @@ jobs:
|
|||||||
run: ./scripts/build-upload-gate
|
run: ./scripts/build-upload-gate
|
||||||
|
|
||||||
- name: Get gcs auth
|
- name: Get gcs auth
|
||||||
uses: rancher-eio/read-vault-secrets@main
|
uses: rancher-eio/read-vault-secrets@d266f55186f80a893839f6e15662e67388e443e6 # v3
|
||||||
with:
|
with:
|
||||||
secrets: |
|
secrets: |
|
||||||
secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ;
|
secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ;
|
||||||
|
|
||||||
- name: Apply gcs auth
|
- name: Apply gcs auth
|
||||||
# https://github.com/google-github-actions/auth
|
# https://github.com/google-github-actions/auth
|
||||||
uses: 'google-github-actions/auth@v2'
|
uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2
|
||||||
|
|
||||||
with:
|
with:
|
||||||
credentials_json: "${{ env.GOOGLE_AUTH }}"
|
credentials_json: "${{ env.GOOGLE_AUTH }}"
|
||||||
|
|
||||||
- name: Upload build
|
- name: Upload build
|
||||||
uses: 'google-github-actions/upload-cloud-storage@v2'
|
uses: google-github-actions/upload-cloud-storage@c0f6160ff80057923ff50e5e567695cea181ec23 # v2
|
||||||
# https://github.com/google-github-actions/upload-cloud-storage
|
# https://github.com/google-github-actions/upload-cloud-storage
|
||||||
with:
|
with:
|
||||||
path: ${{steps.build-hosted.outputs.BUILD_HOSTED_DIR}}
|
path: ${{steps.build-hosted.outputs.BUILD_HOSTED_DIR}}
|
||||||
@@ -71,12 +72,12 @@ jobs:
|
|||||||
name: Build & Upload Embedded
|
name: Build & Upload Embedded
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
# Note - Cannot use the setup action here as it uses a different yarn install arg
|
# Note - Cannot use the setup action here as it uses a different yarn install arg
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
@@ -89,19 +90,19 @@ jobs:
|
|||||||
DISABLED_EMBED_PKG: https://releases.rancher.com/harvester-ui/plugin/harvester-1.0.3.tar.gz
|
DISABLED_EMBED_PKG: https://releases.rancher.com/harvester-ui/plugin/harvester-1.0.3.tar.gz
|
||||||
|
|
||||||
- name: Get gcs auth
|
- name: Get gcs auth
|
||||||
uses: rancher-eio/read-vault-secrets@main
|
uses: rancher-eio/read-vault-secrets@d266f55186f80a893839f6e15662e67388e443e6 # v3
|
||||||
with:
|
with:
|
||||||
secrets: |
|
secrets: |
|
||||||
secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ;
|
secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ;
|
||||||
|
|
||||||
- name: Apply gcs auth
|
- name: Apply gcs auth
|
||||||
# https://github.com/google-github-actions/auth
|
# https://github.com/google-github-actions/auth
|
||||||
uses: 'google-github-actions/auth@v2'
|
uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2
|
||||||
with:
|
with:
|
||||||
credentials_json: "${{ env.GOOGLE_AUTH }}"
|
credentials_json: "${{ env.GOOGLE_AUTH }}"
|
||||||
|
|
||||||
- name: Upload tar
|
- name: Upload tar
|
||||||
uses: 'google-github-actions/upload-cloud-storage@v2'
|
uses: google-github-actions/upload-cloud-storage@c0f6160ff80057923ff50e5e567695cea181ec23 # v2
|
||||||
with:
|
with:
|
||||||
path: ${{steps.build-embedded.outputs.BUILD_EMBEDED_TGZ}}
|
path: ${{steps.build-embedded.outputs.BUILD_EMBEDED_TGZ}}
|
||||||
destination: releases.rancher.com/harvester-ui/dashboard
|
destination: releases.rancher.com/harvester-ui/dashboard
|
||||||
@@ -114,12 +115,12 @@ jobs:
|
|||||||
name: Build & Upload Plugin
|
name: Build & Upload Plugin
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
# Note - Cannot use the setup action here as it uses a different yarn install arg
|
# Note - Cannot use the setup action here as it uses a different yarn install arg
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
@@ -133,19 +134,19 @@ jobs:
|
|||||||
run: ./scripts/build-upload-gate
|
run: ./scripts/build-upload-gate
|
||||||
|
|
||||||
- name: Get gcs auth
|
- name: Get gcs auth
|
||||||
uses: rancher-eio/read-vault-secrets@main
|
uses: rancher-eio/read-vault-secrets@d266f55186f80a893839f6e15662e67388e443e6 # v3
|
||||||
with:
|
with:
|
||||||
secrets: |
|
secrets: |
|
||||||
secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ;
|
secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ;
|
||||||
|
|
||||||
- name: Apply gcs auth
|
- name: Apply gcs auth
|
||||||
# https://github.com/google-github-actions/auth
|
# https://github.com/google-github-actions/auth
|
||||||
uses: 'google-github-actions/auth@v2'
|
uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2
|
||||||
with:
|
with:
|
||||||
credentials_json: "${{ env.GOOGLE_AUTH }}"
|
credentials_json: "${{ env.GOOGLE_AUTH }}"
|
||||||
|
|
||||||
- name: Upload plugin tar
|
- name: Upload plugin tar
|
||||||
uses: 'google-github-actions/upload-cloud-storage@v2'
|
uses: google-github-actions/upload-cloud-storage@c0f6160ff80057923ff50e5e567695cea181ec23 # v2
|
||||||
with:
|
with:
|
||||||
path: dist-pkg/${{steps.ci-build-pkg.outputs.PKG_TARBALL}}
|
path: dist-pkg/${{steps.ci-build-pkg.outputs.PKG_TARBALL}}
|
||||||
destination: releases.rancher.com/harvester-ui/plugin
|
destination: releases.rancher.com/harvester-ui/plugin
|
||||||
@@ -155,7 +156,7 @@ jobs:
|
|||||||
process_gcloudignore: false
|
process_gcloudignore: false
|
||||||
|
|
||||||
- name: Upload plugin directory
|
- name: Upload plugin directory
|
||||||
uses: 'google-github-actions/upload-cloud-storage@v2'
|
uses: google-github-actions/upload-cloud-storage@c0f6160ff80057923ff50e5e567695cea181ec23 # v2
|
||||||
with:
|
with:
|
||||||
path: dist-pkg/${{steps.ci-build-pkg.outputs.PKG_NAME}}
|
path: dist-pkg/${{steps.ci-build-pkg.outputs.PKG_NAME}}
|
||||||
destination: releases.rancher.com/harvester-ui/plugin/${{steps.ci-build-pkg.outputs.PKG_NAME}}
|
destination: releases.rancher.com/harvester-ui/plugin/${{steps.ci-build-pkg.outputs.PKG_NAME}}
|
||||||
|
|||||||
@@ -6,9 +6,6 @@ on:
|
|||||||
registry_target:
|
registry_target:
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
repository_name:
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
registry_user:
|
registry_user:
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
@@ -30,14 +27,14 @@ jobs:
|
|||||||
build-status: ${{ job.status }}
|
build-status: ${{ job.status }}
|
||||||
steps:
|
steps:
|
||||||
- name: Read Secrets
|
- name: Read Secrets
|
||||||
uses: rancher-eio/read-vault-secrets@main
|
uses: rancher-eio/read-vault-secrets@d266f55186f80a893839f6e15662e67388e443e6 # v3
|
||||||
with:
|
with:
|
||||||
secrets: |
|
secrets: |
|
||||||
secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials username | DOCKER_USERNAME ;
|
secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials username | DOCKER_USERNAME ;
|
||||||
secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials password | DOCKER_PASSWORD ;
|
secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials password | DOCKER_PASSWORD ;
|
||||||
|
|
||||||
- name: Checkout repository (normal flow)
|
- name: Checkout repository (normal flow)
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
|
|
||||||
- name: Enable Corepack
|
- name: Enable Corepack
|
||||||
run: corepack enable
|
run: corepack enable
|
||||||
@@ -48,34 +45,20 @@ jobs:
|
|||||||
git config user.email 'github-actions[bot]@users.noreply.github.com'
|
git config user.email 'github-actions[bot]@users.noreply.github.com'
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
||||||
with:
|
with:
|
||||||
username: ${{ env.DOCKER_USERNAME }}
|
username: ${{ env.DOCKER_USERNAME }}
|
||||||
password: ${{ env.DOCKER_PASSWORD }}
|
password: ${{ env.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
- name: Setup Helm
|
- name: Setup Helm
|
||||||
uses: azure/setup-helm@v3
|
uses: azure/setup-helm@5119fcb9089d432beecbf79bb2c7915207344b78 # v3
|
||||||
with:
|
with:
|
||||||
version: v3.8.0
|
version: v3.8.0
|
||||||
|
|
||||||
- name: Setup Nodejs with yarn caching
|
- name: Setup Nodejs with yarn install
|
||||||
uses: actions/setup-node@v4
|
uses: ./.github/actions/setup
|
||||||
with:
|
|
||||||
node-version: '20'
|
|
||||||
cache: yarn
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: yarn
|
|
||||||
|
|
||||||
- name: Build and push UI image
|
- name: Build and push UI image
|
||||||
run: |
|
run: |
|
||||||
publish="yarn publish-pkgs -c -r ${{ inputs.registry_target }} -o ${{ inputs.registry_user }}"
|
publish="yarn publish-pkgs -cp -r ${{ inputs.registry_target }} -o ${{ inputs.registry_user }}"
|
||||||
$publish
|
$publish
|
||||||
|
|
||||||
REPO_NAME=$(echo "${{ github.repository }}" | cut -d'/' -f2)
|
|
||||||
IMAGE_VERSION=$(cat package.json | jq .version | sed 's/"//g')
|
|
||||||
FULL_IMAGE_NAME=$(echo ${{ inputs.registry_target }}/${{ inputs.registry_user }}/ui-extension-$REPO_NAME:$IMAGE_VERSION)
|
|
||||||
IMAGE_ID=$(docker inspect --format='{{.Id}}' $FULL_IMAGE_NAME)
|
|
||||||
|
|
||||||
docker tag $IMAGE_ID ${{ inputs.registry_target }}/${{ inputs.registry_user }}/${{ inputs.repository_name }}:$IMAGE_VERSION
|
|
||||||
docker push ${{ inputs.registry_target }}/${{ inputs.registry_user }}/${{ inputs.repository_name }}:$IMAGE_VERSION
|
|
||||||
@@ -4,6 +4,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- 'release-harvester-v*'
|
- 'release-harvester-v*'
|
||||||
|
- 'main'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
setup-target-branch:
|
setup-target-branch:
|
||||||
@@ -12,19 +13,22 @@ jobs:
|
|||||||
target_branch: ${{ steps.get-version.outputs.target_branch }}
|
target_branch: ${{ steps.get-version.outputs.target_branch }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
|
|
||||||
- name: Determine target branch
|
- name: Determine target branch
|
||||||
id: get-version
|
id: get-version
|
||||||
run: |
|
run: |
|
||||||
if [[ "${{ github.ref_name }}" =~ ^release-harvester-v([0-9]+\.[0-9]+)$ ]]; then
|
if [[ "${{ github.ref_name }}" == "main" ]]; then
|
||||||
|
TARGET_BRANCH="main-head"
|
||||||
|
elif [[ "${{ github.ref_name }}" =~ ^release-harvester-v([0-9]+\.[0-9]+)$ ]]; then
|
||||||
TARGET_BRANCH="v${BASH_REMATCH[1]}-head"
|
TARGET_BRANCH="v${BASH_REMATCH[1]}-head"
|
||||||
echo "target_branch=${TARGET_BRANCH}" >> $GITHUB_ENV
|
|
||||||
echo "target_branch=${TARGET_BRANCH}" >> $GITHUB_OUTPUT
|
|
||||||
else
|
else
|
||||||
echo "Error: invalid branch format." && exit 1
|
echo "Error: invalid branch format." && exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "target_branch=${TARGET_BRANCH}" >> $GITHUB_ENV
|
||||||
|
echo "target_branch=${TARGET_BRANCH}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Ensure target branch exists
|
- name: Ensure target branch exists
|
||||||
run: |
|
run: |
|
||||||
git fetch --all
|
git fetch --all
|
||||||
@@ -40,7 +44,7 @@ jobs:
|
|||||||
version: ${{ steps.get_version.outputs.version }}
|
version: ${{ steps.get_version.outputs.version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
|
|
||||||
- name: Extract version from package.json
|
- name: Extract version from package.json
|
||||||
id: get_version
|
id: get_version
|
||||||
@@ -58,7 +62,13 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
|
|
||||||
|
- name: Setup Node from .nvmrc
|
||||||
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
|
with:
|
||||||
|
node-version-file: '.nvmrc'
|
||||||
|
cache: 'yarn'
|
||||||
|
|
||||||
- name: Setup environment
|
- name: Setup environment
|
||||||
run: |
|
run: |
|
||||||
@@ -66,7 +76,7 @@ jobs:
|
|||||||
yarn install --frozen-lockfile
|
yarn install --frozen-lockfile
|
||||||
|
|
||||||
- name: Setup Helm
|
- name: Setup Helm
|
||||||
uses: azure/setup-helm@v3
|
uses: azure/setup-helm@5119fcb9089d432beecbf79bb2c7915207344b78 # v3
|
||||||
with:
|
with:
|
||||||
version: v3.8.0
|
version: v3.8.0
|
||||||
|
|
||||||
@@ -75,7 +85,7 @@ jobs:
|
|||||||
yarn publish-pkgs -s ${{ github.repository }} -b ${{ needs.setup-target-branch.outputs.target_branch }} -t harvester-${{ needs.extract-version.outputs.version }}
|
yarn publish-pkgs -s ${{ github.repository }} -b ${{ needs.setup-target-branch.outputs.target_branch }} -t harvester-${{ needs.extract-version.outputs.version }}
|
||||||
|
|
||||||
- name: Upload charts artifact
|
- name: Upload charts artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||||
with:
|
with:
|
||||||
name: charts
|
name: charts
|
||||||
path: tmp
|
path: tmp
|
||||||
@@ -90,7 +100,7 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout release branch
|
- name: Checkout release branch
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
with:
|
with:
|
||||||
ref: '${{ github.ref_name }}'
|
ref: '${{ github.ref_name }}'
|
||||||
|
|
||||||
@@ -101,7 +111,7 @@ jobs:
|
|||||||
echo "LAST_COMMIT=${LAST_COMMIT}" >> $GITHUB_ENV
|
echo "LAST_COMMIT=${LAST_COMMIT}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Checkout target branch
|
- name: Checkout target branch
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
with:
|
with:
|
||||||
ref: '${{ needs.setup-target-branch.outputs.target_branch }}'
|
ref: '${{ needs.setup-target-branch.outputs.target_branch }}'
|
||||||
|
|
||||||
@@ -117,7 +127,7 @@ jobs:
|
|||||||
git config user.email 'github-actions[bot]@users.noreply.github.com'
|
git config user.email 'github-actions[bot]@users.noreply.github.com'
|
||||||
|
|
||||||
- name: Download build artifacts
|
- name: Download build artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||||
with:
|
with:
|
||||||
name: charts
|
name: charts
|
||||||
|
|
||||||
@@ -128,7 +138,7 @@ jobs:
|
|||||||
git push origin ${{ needs.setup-target-branch.outputs.target_branch }}
|
git push origin ${{ needs.setup-target-branch.outputs.target_branch }}
|
||||||
|
|
||||||
- name: Run Helm chart releaser
|
- name: Run Helm chart releaser
|
||||||
uses: helm/chart-releaser-action@v1.4.1
|
uses: helm/chart-releaser-action@cae68fefc6b5f367a0275617c9f83181ba54714f # v1.7.0
|
||||||
with:
|
with:
|
||||||
charts_dir: ./charts
|
charts_dir: ./charts
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -17,37 +17,36 @@ jobs:
|
|||||||
release_tag: ${{ steps.determine_tag.outputs.release_tag }}
|
release_tag: ${{ steps.determine_tag.outputs.release_tag }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
|
|
||||||
- name: Determine release tag
|
- name: Determine release tag
|
||||||
id: determine_tag
|
id: determine_tag
|
||||||
run: |
|
run: |
|
||||||
if [[ "${{ github.ref_name }}" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
|
if [[ "${{ github.event.release.tag_name }}" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
|
||||||
RELEASE_TAG="harvester-${BASH_REMATCH[1]}"
|
RELEASE_TAG="harvester-${BASH_REMATCH[1]}"
|
||||||
echo "${RELEASE_TAG}"
|
echo "${RELEASE_TAG}"
|
||||||
echo "release_tag=${RELEASE_TAG}" >> $GITHUB_OUTPUT
|
echo "release_tag=${RELEASE_TAG}" >> $GITHUB_OUTPUT
|
||||||
else
|
else
|
||||||
echo "Error: invalid tag format." && exit 1
|
echo "Error: invalid tag format." && exit 1
|
||||||
fi
|
fi
|
||||||
lint:
|
check-version:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Checkout repository
|
||||||
with:
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
fetch-depth: 1
|
- name: Check package version
|
||||||
|
env:
|
||||||
|
TAG_VERSION: ${{ github.event.release.tag_name }}
|
||||||
|
run: ./scripts/check-version.sh $TAG_VERSION
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
uses: ./.github/actions/lint
|
|
||||||
build-extension-charts:
|
build-extension-charts:
|
||||||
needs:
|
needs:
|
||||||
- setup-release-tag
|
- setup-release-tag
|
||||||
- lint
|
- check-version
|
||||||
uses: rancher/dashboard/.github/workflows/build-extension-charts.yml@master
|
uses: rancher/dashboard/.github/workflows/build-extension-charts.yml@9bf23dff6650116673346447bcf0b088e0edb27f # creators-pkg-v3.0.12
|
||||||
permissions:
|
permissions:
|
||||||
actions: write
|
|
||||||
contents: write
|
contents: write
|
||||||
deployments: write
|
pull-requests: write
|
||||||
pages: write
|
|
||||||
with:
|
with:
|
||||||
target_branch: gh-pages
|
target_branch: gh-pages
|
||||||
tagged_release: '${{ needs.setup-release-tag.outputs.release_tag }}'
|
tagged_release: '${{ needs.setup-release-tag.outputs.release_tag }}'
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
name: Build Standalone on PR Merge
|
name: Build Standalone on PR Merge
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch: # manually trigger on Github Actions only support PR branch pushed to harvester/harvester-ui-extension
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
@@ -29,3 +30,35 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
with:
|
with:
|
||||||
CI_BRANCH: ${{github.ref_name}}
|
CI_BRANCH: ${{github.ref_name}}
|
||||||
|
comment-on-pr:
|
||||||
|
name: Comment build result on PR
|
||||||
|
# Only for manual runs so the existing push / pull_request flows are untouched.
|
||||||
|
if: github.event_name == 'workflow_dispatch'
|
||||||
|
needs:
|
||||||
|
- build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- name: Add build artifact comment to the PR opened from this branch
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
BRANCH: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
# Match scripts/version: sanitize the branch and map main -> latest.
|
||||||
|
if [ "$BRANCH" = "main" ]; then
|
||||||
|
DIR="latest"
|
||||||
|
else
|
||||||
|
DIR=$(printf '%s' "$BRANCH" | sed -E 's/[^a-zA-Z0-9.-]+/-/g')
|
||||||
|
fi
|
||||||
|
URL="https://releases.rancher.com/harvester-ui/dashboard/${DIR}/index.html"
|
||||||
|
|
||||||
|
PR=$(gh pr list --repo "$GITHUB_REPOSITORY" --state open --base main --head "$BRANCH" --json number --jq '.[0].number // empty')
|
||||||
|
if [ -z "$PR" ]; then
|
||||||
|
echo "No open PR found for branch $BRANCH, skipping comment."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
BODY=$(printf '🚀 **UI Artifact Build Status:** `SUCCESS` ✅\n> ui-index: %s\n> ui-source: external' "$URL")
|
||||||
|
gh pr comment "$PR" --repo "$GITHUB_REPOSITORY" --body "$BODY"
|
||||||
|
|||||||
@@ -5,14 +5,20 @@ on:
|
|||||||
- v[1-9].*
|
- v[1-9].*
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-validation:
|
check-version:
|
||||||
name: Build Test
|
runs-on: ubuntu-latest
|
||||||
uses: ./.github/workflows/run-lint.yaml
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
|
- name: Check package version
|
||||||
|
env:
|
||||||
|
TAG_VERSION: ${{github.ref_name}}
|
||||||
|
run: ./scripts/check-version.sh $TAG_VERSION
|
||||||
|
|
||||||
build:
|
build:
|
||||||
name: Build and Upload Package
|
name: Build and Upload Package
|
||||||
uses: ./.github/workflows/build-and-publish-standalone.yaml
|
uses: ./.github/workflows/build-and-publish-standalone.yaml
|
||||||
needs:
|
needs: check-version
|
||||||
- build-validation
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
name: FOSSA Scanning
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["main", "release-harvester-v*"]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
fossa-scanning:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
|
# The FOSSA token is shared between all repos in Harvester's GH org. It can
|
||||||
|
# be used directly and there is no need to request specific access to EIO.
|
||||||
|
- name: Read FOSSA token
|
||||||
|
uses: rancher-eio/read-vault-secrets@d266f55186f80a893839f6e15662e67388e443e6 # v3
|
||||||
|
with:
|
||||||
|
secrets: |
|
||||||
|
secret/data/github/org/harvester/fossa/credentials token | FOSSA_API_KEY_PUSH_ONLY
|
||||||
|
|
||||||
|
- name: FOSSA scan
|
||||||
|
uses: fossas/fossa-action@ff70fe9fe17cbd2040648f1c45e8ec4e4884dcf3 # v1.9.0
|
||||||
|
with:
|
||||||
|
api-key: ${{ env.FOSSA_API_KEY_PUSH_ONLY }}
|
||||||
|
# Only runs the scan and do not provide/returns any results back to the
|
||||||
|
# pipeline.
|
||||||
|
run-tests: false
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
name: Tests
|
name: Tests
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_call: # This tells GH that the workflow is reusable
|
workflow_call: # This tells GH that the workflow is reusable
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- 'release-*'
|
- 'release-*'
|
||||||
pull_request:
|
pull_request:
|
||||||
|
types: [opened, reopened, edited, synchronize]
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- 'release-*'
|
- 'release-*'
|
||||||
@@ -15,9 +16,58 @@ jobs:
|
|||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 0 # Need full history for commit-lint
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
uses: ./.github/actions/lint
|
uses: ./.github/actions/lint
|
||||||
|
|
||||||
|
- name: Validate PR title lint
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
shell: bash
|
||||||
|
run: echo "${{ github.event.pull_request.title }}" | npx commitlint
|
||||||
|
|
||||||
|
- name: Validate commit messages
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "GITHUB_EVENT_NAME=$GITHUB_EVENT_NAME"
|
||||||
|
echo "GITHUB_BASE_SHA=$GITHUB_BASE_SHA"
|
||||||
|
echo "GITHUB_HEAD_SHA=$GITHUB_HEAD_SHA"
|
||||||
|
echo "GITHUB_BEFORE=$GITHUB_BEFORE"
|
||||||
|
echo "GITHUB_AFTER=$GITHUB_AFTER"
|
||||||
|
|
||||||
|
if [ "$GITHUB_EVENT_NAME" = "pull_request" ] && [ -n "$GITHUB_BASE_SHA" ] && [ -n "$GITHUB_HEAD_SHA" ]; then
|
||||||
|
FROM="$GITHUB_BASE_SHA"
|
||||||
|
TO="$GITHUB_HEAD_SHA"
|
||||||
|
elif [ -n "$GITHUB_BEFORE" ] && [ -n "$GITHUB_AFTER" ]; then
|
||||||
|
if [ "$GITHUB_BEFORE" = "0000000000000000000000000000000000000000" ]; then
|
||||||
|
# first push to HEAD
|
||||||
|
FROM=""
|
||||||
|
TO="$GITHUB_AFTER"
|
||||||
|
else
|
||||||
|
FROM="$GITHUB_BEFORE"
|
||||||
|
TO="$GITHUB_AFTER"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "No valid commit range found, skipping commitlint."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "FROM=$FROM"
|
||||||
|
echo "TO=$TO"
|
||||||
|
|
||||||
|
if [ -z "$FROM" ]; then
|
||||||
|
echo "Linting last commit $TO"
|
||||||
|
npx commitlint --last --verbose
|
||||||
|
|
||||||
|
else
|
||||||
|
echo "Linting commits from $FROM to $TO"
|
||||||
|
npx commitlint --from "$FROM" --to "$TO" --verbose
|
||||||
|
fi
|
||||||
|
env:
|
||||||
|
GITHUB_EVENT_NAME: ${{ github.event_name }}
|
||||||
|
GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||||
|
GITHUB_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||||
|
GITHUB_BEFORE: ${{ github.event.before }}
|
||||||
|
GITHUB_AFTER: ${{ github.event.after }}
|
||||||
|
|||||||
Executable
+1
@@ -0,0 +1 @@
|
|||||||
|
npx --no -- commitlint --edit $1
|
||||||
Executable
+1
@@ -0,0 +1 @@
|
|||||||
|
yarn lint
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
> This `./AGENTS.md` file is generated by running `yarn agents:generate`. `CLAUDE.md` and `GEMINI.md` are symlinks to this file so Claude Code, GitHub Copilot and Gemini all read the same content.
|
||||||
|
|
||||||
|
|
||||||
|
# Project Overview
|
||||||
|
|
||||||
|
The Harvester UI Extension is a Rancher extension that provides the user interface for Harvester within the Rancher Dashboard.
|
||||||
|
|
||||||
|
```
|
||||||
|
Note: This extension is available starting from Rancher 2.10.0. Ensure your Rancher version is 2.10.0 or later to access Harvester integration.
|
||||||
|
```
|
||||||
|
|
||||||
|
# Personas
|
||||||
|
|
||||||
|
## Software Developer
|
||||||
|
|
||||||
|
You are an expert Senior Software Engineer specializing in Vue.js and TypeScript. You have deep knowledge of Kubernetes and the Rancher ecosystem.
|
||||||
|
- **Focus**: Writing clean, maintainable, and performant code.
|
||||||
|
- **Priorities**: Adhering to the project's code style, ensuring type safety, and following best practices for component design.
|
||||||
|
|
||||||
|
|
||||||
|
# Agents
|
||||||
|
|
||||||
|
## Boundaries
|
||||||
|
|
||||||
|
- ✅ **Always:**
|
||||||
|
- Write to `.github`, `docs/`, `pkg/harvester`, `scripts/`.
|
||||||
|
- Make commits in a new branch (for a PR).
|
||||||
|
- Run `yarn lint:fix` before commits.
|
||||||
|
- Use conventional commit format:
|
||||||
|
```
|
||||||
|
<type>: <description>
|
||||||
|
```
|
||||||
|
- Follow existing naming conventions (PascalCase for components, camelCase for functions).
|
||||||
|
- After changing a Vue, JS, or TS file, make sure it's automatically formatted with ESLint.
|
||||||
|
- After updating files in `docs/agents.md/`, run `yarn agents:generate` to update root `AGENTS.md`.
|
||||||
|
- ⚠️ **Ask first:**
|
||||||
|
- Adding dependencies
|
||||||
|
- Upgrading dependencies
|
||||||
|
- 🚫 **Never:**
|
||||||
|
- Commit or log secrets, `.env`, `kubeconfig` or any API keys.
|
||||||
|
- Edit `node_modules/`.
|
||||||
|
- Commit directly to `main` (use PRs).
|
||||||
|
- Skip git hooks with `--no-verify` flag.
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- **Node.js**: `>= 24.0.0` (see `.nvmrc` for the pinned version: `24`)
|
||||||
|
- **Package manager**: `yarn` (v1 classic)
|
||||||
|
|
||||||
|
### Common Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `yarn install --frozen-lockfile` | Install dependencies (CI-safe, no lockfile changes) |
|
||||||
|
| `RANCHER_ENV=harvester API=https://<harvester-ip> yarn dev` | Start development server at `https://127.0.0.1:8005` |
|
||||||
|
| `yarn build-pkg harvester` | Build the Harvester extension package |
|
||||||
|
| `yarn serve-pkgs` | Serve the locally built extension for testing |
|
||||||
|
| `yarn lint` | Run ESLint (zero warnings allowed) |
|
||||||
|
| `yarn lint:fix` | Run ESLint with auto-fix |
|
||||||
|
| `yarn clean` | Clean build artifacts |
|
||||||
|
| `yarn agents:generate` | Regenerate `AGENTS.md` from `docs/agents.md/` sources |
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
- **Start dev server**:
|
||||||
|
```bash
|
||||||
|
RANCHER_ENV=harvester API=https://your-harvester-ip yarn dev
|
||||||
|
```
|
||||||
|
- `API` should point to a running Harvester cluster (e.g., `https://x.x.x.x`).
|
||||||
|
- The dashboard will be available at `https://127.0.0.1:8005`.
|
||||||
|
|
||||||
|
### Building
|
||||||
|
|
||||||
|
- **Build extension package**:
|
||||||
|
```bash
|
||||||
|
yarn build-pkg harvester
|
||||||
|
```
|
||||||
|
- **Serve locally built extension** (for integration testing with a Rancher instance):
|
||||||
|
```bash
|
||||||
|
yarn serve-pkgs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Linting
|
||||||
|
|
||||||
|
- **Check** (must pass with zero warnings):
|
||||||
|
```bash
|
||||||
|
yarn lint
|
||||||
|
```
|
||||||
|
- **Auto-fix**:
|
||||||
|
```bash
|
||||||
|
yarn lint:fix
|
||||||
|
```
|
||||||
|
- ESLint covers `.js`, `.ts`, and `.vue` files.
|
||||||
|
- Always run `yarn lint:fix` before committing.
|
||||||
|
|
||||||
|
### Commit Conventions
|
||||||
|
|
||||||
|
- Commits are validated by [commitlint](https://commitlint.js.org/) via a Husky `commit-msg` hook.
|
||||||
|
- Follow [Conventional Commits](https://www.conventionalcommits.org/) format (configured in `commitlint.config.js`).
|
||||||
|
|
||||||
|
### Agent Documentation
|
||||||
|
|
||||||
|
- Source files live in `docs/agents.md/` (agents, contributors, personas subdirectories).
|
||||||
|
- After editing any source file, regenerate the root `AGENTS.md`:
|
||||||
|
```bash
|
||||||
|
yarn agents:generate
|
||||||
|
```
|
||||||
|
|
||||||
|
# Contributors Guide
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
Please see the [Harvester UI Extension README](https://github.com/harvester/harvester-ui-extension).
|
||||||
|
|
||||||
|
To get started, follow the `Development Setup` section.
|
||||||
|
|
||||||
|
## Project Information
|
||||||
|
|
||||||
|
- **Tech Stack:**
|
||||||
|
- `Vue.js`: Framework
|
||||||
|
- `Linting`: ESLint
|
||||||
|
- `CSS`: SCSS should be used
|
||||||
|
- `TypeScript`: Primary language for logic.
|
||||||
|
- **Code Style and Standards:**
|
||||||
|
- `Language`: TypeScript is preferred for new code.
|
||||||
|
- `Vue.js`:
|
||||||
|
- Composition API components are preferred over Options API.
|
||||||
|
- Large pages with lots of code and styles should be avoided by breaking the page up into smaller Vue components.
|
||||||
|
- Order the Single File Component blocks as `<script>`, then `<template>`, then `<style>`.
|
||||||
|
- The `<style>` tag should include `lang="scss" scoped`.
|
||||||
|
- `Linting`: Follow the ESLint configuration in the root.
|
||||||
|
|
||||||
|
- **File Structure:**
|
||||||
|
- `.github/`: CI/CD workflows and Renovate config.
|
||||||
|
- `docs/`: Documentation source for `AGENTS.md` generation.
|
||||||
|
- `scripts/`: Bash scripts for build, CI and doc generation.
|
||||||
|
- `pkg/harvester/`: Main extension source. Files are named after K8s resource types (e.g., `kubevirt.io.virtualmachine`, `harvesterhci.io.volume`).
|
||||||
|
- `index.ts`: Plugin entry point — registers the product and auto-imports models/detail/edit views.
|
||||||
|
- `types.ts`: `HCI` constant mapping 60+ K8s resource types to string identifiers.
|
||||||
|
- `components/`: Reusable Vue components (VNC/serial console, settings panels, upgrade banners, filters).
|
||||||
|
- `config/`: Constants — settings keys, table column definitions, feature flags, type mappings, doc links.
|
||||||
|
- `detail/`: Read-only detail views per resource type. Complex resources use subdirectories with tabs.
|
||||||
|
- `dialog/`: Modal dialogs for operations (VM clone/migrate/restart, backup/restore, device passthrough, etc.).
|
||||||
|
- `edit/`: Create/edit forms. Complex resources (e.g., VM) split into subcomponents (CpuMemory, Network, Volume, CloudConfig, etc.).
|
||||||
|
- `formatters/`: Table cell formatters — state badges, usage bars, resource references.
|
||||||
|
- `l10n/`: Localization (`en-us.yaml`).
|
||||||
|
- `list/`: List (table) views per resource type, mirroring `detail/` and `edit/` naming.
|
||||||
|
- `mixins/`: Shared Vue mixins — VM helpers (`harvester-vm/`) and disk helpers (`harvester-disk.js`).
|
||||||
|
- `models/`: Model classes extending `SteveModel` with computed properties and actions. Base class: `harvester.js`.
|
||||||
|
- `pages/`: Route-level pages — dashboard, support, console, members, brand, alertmanager.
|
||||||
|
- `promptRemove/`: Custom delete-confirmation dialogs (VM, backup).
|
||||||
|
- `routing/`: Vue Router config — all product routes (CRUD, console, support, upgrade, etc.).
|
||||||
|
- `store/`: Vuex modules — `harvester-common.js` for shared state; `harvester-store/` for VM/resource creation actions with Steve integration.
|
||||||
|
- `styles/`: Global SCSS files.
|
||||||
|
- `utils/`: Helpers — VM volume templates, CPU/memory calc, cron parsing, regex validators, feature flags.
|
||||||
|
- `validators/`: Form validation functions per resource type, pushing i18n error messages.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Harvester UI Extension Development Guide
|
||||||
|
|
||||||
|
### Backward Compatibility
|
||||||
|
|
||||||
|
The Harvester UI Extension supports earlier cluster versions (e.g., UI Ext v1.8.0 works with clusters v1.7.0 and v1.6.0). It uses Feature Flags defined in `pkg/harvester/config/` to ensure the UI matches the cluster's specific version.
|
||||||
|
|
||||||
|
### Implementation Steps for New Features
|
||||||
|
|
||||||
|
To add a feature in a new release, follow these steps:
|
||||||
|
|
||||||
|
1. **Register**: Add a unique `[Feature Name]` to the corresponding release array in the configuration.
|
||||||
|
2. **Check**: Use the following getter to verify if the feature is enabled for the current version:
|
||||||
|
|
||||||
|
```js
|
||||||
|
computed: {
|
||||||
|
newFeatureEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('[Feature Name]');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Render**: Use the result of the check to conditionally render the UI components.
|
||||||
|
|
||||||
|
|
||||||
|
## E2E Tests (Cypress)
|
||||||
|
|
||||||
|
See https://github.com/harvester/harvester-ui-tests
|
||||||
|
|
||||||
|
## Node Dependencies
|
||||||
|
|
||||||
|
Dependencies are managed via `package.json` and `yarn`
|
||||||
|
|
||||||
|
- To install dependencies use `yarn install`. This will fail if dependencies and versions listed in `package.json` are out of step with the `yarn.lock` file
|
||||||
|
- To add a dependency use `yarn run add:no-lock ...` instead of `yarn add`
|
||||||
|
- To upgrade a dependency use `yarn run upgrade:no-lock ...` instead of `yarn upgrade`
|
||||||
|
|
||||||
|
|
||||||
|
Renovate automatically updates dependencies and opens upgrade PRs after 10:00 AM on Sundays (`Asia/Taipei`).
|
||||||
|
For the Renovate config, see `.github/renovate.json`.
|
||||||
|
|
||||||
|
|
||||||
|
## Milestone guidance
|
||||||
|
|
||||||
|
- All issues must first be resolved in the `main` branch
|
||||||
|
- If backports are needed they can be made via the backport bot
|
||||||
|
- pull requests
|
||||||
|
- comment `@Mergifyio backport <target branch>` e.g. `@Mergifyio backport release-harvester-v1.8`
|
||||||
|
- All backported pull requests must link to a backported issue
|
||||||
|
|
||||||
|
|
||||||
|
## Creating a branch
|
||||||
|
|
||||||
|
### To resolve an issue
|
||||||
|
- Checkout the branch matching the milestone of the issue `git checkout ${targetMilestoneBranch}`. Replace `${targetMilestoneBranch}` with the target milestone of the issue. For example
|
||||||
|
- `main` for the latest unreleased minor version
|
||||||
|
- `release-harvester-v.X` for release minor versions
|
||||||
|
- `release-harvester-v1.6`
|
||||||
|
- `release-harvester-v1.7`
|
||||||
|
- `release-harvester-v1.8`
|
||||||
|
- Ensure you have the latest of that branch `git pull --rebase`
|
||||||
|
- Checkout the branch to commit the changes to `git checkout issue-${issueNumber}`. Replace `${issueNumber}` with the issue number.
|
||||||
|
|
||||||
|
## Creating a commit
|
||||||
|
|
||||||
|
- This project uses commit-lint with [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) to ensure consistent and meaningful commit messages.
|
||||||
|
|
||||||
|
### Commit Message Format
|
||||||
|
All commit messages must follow the conventional commit format:
|
||||||
|
```
|
||||||
|
<type>: <description>
|
||||||
|
|
||||||
|
[optional body]
|
||||||
|
|
||||||
|
[optional footer(s)]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Supported Types
|
||||||
|
- feat: New features
|
||||||
|
- fix: Bug fixes
|
||||||
|
- docs: Documentation changes
|
||||||
|
- style: Code style changes (formatting, missing semicolons, etc.)
|
||||||
|
- refactor: Code refactoring
|
||||||
|
- perf: Performance improvements
|
||||||
|
- test: Adding or updating tests
|
||||||
|
- build: Build system or external dependencies
|
||||||
|
- ci: CI/CD changes
|
||||||
|
- chore: Other changes that don't modify src or test files
|
||||||
|
- revert: Reverts a previous commit
|
||||||
|
- wip: Work in progress
|
||||||
|
- deps: Dependency updates
|
||||||
|
- security: Security fixes
|
||||||
|
|
||||||
|
## Creating a Pull Request
|
||||||
|
|
||||||
|
- Pull requests must come from forks
|
||||||
|
- Description should always have commit supported type prefix. E.g `fix: XXX`, `feat: OOO`
|
||||||
|
- A Pull Request will only be merged once
|
||||||
|
- ALL CI gates have passed
|
||||||
|
- At least one harvester/harvester-ui-extension code owners reviews and approves the PR
|
||||||
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
@a110605
|
||||||
|
@houhoucoop
|
||||||
@@ -1,63 +1,189 @@
|
|||||||
# harvester-ui-extension
|
# harvester-ui-extension
|
||||||
|
|
||||||
Rancher Extension used in [rancher/dashboard](https://github.com/rancher/dashboard) for [Harvester](https://harvesterhci.io) Dashboard UI.
|
The Harvester UI Extension is a Rancher extension that provides the user interface for [Harvester](https://harvesterhci.io) within the [Rancher Dashboard](https://github.com/rancher/dashboard).
|
||||||
|
|
||||||
|
> **Note:**
|
||||||
|
> This extension is available starting from **Rancher 2.10.0**. Ensure your Rancher version is **2.10.0 or later** to access Harvester integration.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Installation](#installation)
|
||||||
|
- [Development Setup](#development-setup)
|
||||||
|
- [Commit Message Guidelines](#commit-message-guidelines)
|
||||||
|
- [Branch Structure](#branch-structure)
|
||||||
|
- [Testing Guidelines](#testing-guidelines)
|
||||||
|
- [Release](#release)
|
||||||
|
- [Contributing](#contributing)
|
||||||
|
- [License](#license)
|
||||||
|
|
||||||
|
|
||||||
`
|
|
||||||
From Rancher 2.10.0, Harvester integration is provided by this new Harvester UI Extension.
|
|
||||||
`
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
To install the Harvester UI Extension in Rancher 2.10.0 and newer, please refer to [installation](https://docs.harvesterhci.io/v1.5/rancher/harvester-ui-extension#installation-on-rancher-210) on harvester doc.
|
For Harvester UI extension installation instructions, please refer to the page **Rancher Integration** -> **Harvester UI Extension** in [official Harvester documentation](https://docs.harvesterhci.io).
|
||||||
|
|
||||||
## Running for Development
|
|
||||||
|
|
||||||
This is what you probably want to get started.
|
## Development Setup
|
||||||
|
|
||||||
### Prerequisite
|
|
||||||
|
|
||||||
Nodejs version >= 20
|
|
||||||
|
|
||||||
|
Ensure **Node.js v24 or later** is installed for development and debugging.
|
||||||
|
|
||||||
### Standalone Mode
|
### Standalone Mode
|
||||||
|
|
||||||
For development, serve with hot reload at https://localhost:8005
|
Run the extension standalone with hot reload at `https://localhost:8005`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
yarn install
|
yarn install
|
||||||
|
|
||||||
# Use RANCHER_ENV to specify the endpoint for your Harvester API
|
# Start the development server
|
||||||
RANCHER_ENV=harvester API=https://your-harvester-ip yarn dev
|
RANCHER_ENV=harvester API=https://your-harvester-ip yarn dev
|
||||||
|
|
||||||
# Use VUE_APP_SERVER_VERSION to specify the Harvester server version
|
# Example with specific server version
|
||||||
# in case your are running a dev build of Harvester
|
RANCHER_ENV=harvester VUE_APP_SERVER_VERSION=v1.5.0 API=https://192.168.1.123 yarn dev
|
||||||
RANCHER_ENV=harvester VUE_APP_SERVER_VERSION=your-harvester-server-version API=https://your-harvester-ip yarn dev
|
```
|
||||||
|
|
||||||
# or put your env variables into a .env file
|
You may also define environment variables in a `.env` file:
|
||||||
|
|
||||||
# Example
|
```env
|
||||||
RANCHER_ENV=harvester VUE_APP_SERVER_VERSION=v1.4.0 API=https://192.168.1.123 yarn dev
|
RANCHER_ENV=harvester
|
||||||
|
VUE_APP_SERVER_VERSION=v1.5.0
|
||||||
|
API=https://192.168.1.123
|
||||||
```
|
```
|
||||||
|
|
||||||
### Rancher Integration Mode
|
### Rancher Integration Mode
|
||||||
|
|
||||||
In this mode, you need to have Rancher and run Harvester UI Extension as Rancher extension, see more detail in [Rancher UI extension](https://extensions.rancher.io/extensions/next/extensions-getting-started#running-the-app).
|
To run as a Rancher extension, follow the [Rancher UI Extension Guide](https://extensions.rancher.io/extensions/next/extensions-getting-started#running-the-app).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
API=https://your-rancher-ip yarn dev
|
API=https://your-rancher-ip yarn dev
|
||||||
|
|
||||||
# Example
|
|
||||||
API=https://192.168.1.123 yarn dev
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Commit Message Guidelines
|
||||||
|
|
||||||
|
This project uses [commit-lint](https://commitlint.js.org/) with [Conventional Commits](https://www.conventionalcommits.org/) to ensure consistent and meaningful commit messages.
|
||||||
|
|
||||||
|
### Commit Message Format
|
||||||
|
|
||||||
|
All commit messages must follow the conventional commit format:
|
||||||
|
|
||||||
|
```
|
||||||
|
<type>[optional scope]: <description>
|
||||||
|
|
||||||
|
[optional body]
|
||||||
|
|
||||||
|
[optional footer(s)]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Supported Types
|
||||||
|
|
||||||
|
- **feat**: New features
|
||||||
|
- **fix**: Bug fixes
|
||||||
|
- **docs**: Documentation changes
|
||||||
|
- **style**: Code style changes (formatting, missing semicolons, etc.)
|
||||||
|
- **refactor**: Code refactoring
|
||||||
|
- **perf**: Performance improvements
|
||||||
|
- **test**: Adding or updating tests
|
||||||
|
- **build**: Build system or external dependencies
|
||||||
|
- **ci**: CI/CD changes
|
||||||
|
- **chore**: Other changes that don't modify src or test files
|
||||||
|
- **revert**: Reverts a previous commit
|
||||||
|
- **wip**: Work in progress
|
||||||
|
- **deps**: Dependency updates
|
||||||
|
- **security**: Security fixes
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Feature
|
||||||
|
git commit -m "feat: add new virtual machine creation wizard"
|
||||||
|
|
||||||
|
# Bug fix
|
||||||
|
git commit -m "fix: resolve memory leak in VM console"
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
git commit -m "docs: update installation instructions"
|
||||||
|
|
||||||
|
# Breaking change
|
||||||
|
git commit -m "feat!: change API endpoint structure
|
||||||
|
|
||||||
|
BREAKING CHANGE: The /api/v1/vms endpoint has been replaced with /api/v2/vms"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Git Hooks
|
||||||
|
|
||||||
|
The project uses [Husky](https://typicode.github.io/husky/) to automatically validate commit messages and run linting before commits:
|
||||||
|
|
||||||
|
- **pre-commit**: Runs ESLint to ensure code quality
|
||||||
|
- **commit-msg**: Validates commit message format using commit-lint
|
||||||
|
|
||||||
|
These hooks are automatically installed when you run `yarn install`.
|
||||||
|
|
||||||
|
### Manual Validation
|
||||||
|
|
||||||
|
You can manually validate commit messages:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Validate the last commit
|
||||||
|
yarn commitlint
|
||||||
|
|
||||||
|
# Validate a specific commit
|
||||||
|
npx commitlint --from <commit-hash>
|
||||||
|
|
||||||
|
# Validate a range of commits
|
||||||
|
npx commitlint --from <start-hash> --to <end-hash>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Branch Structure
|
||||||
|
|
||||||
|
- **`main`** – Main development branch
|
||||||
|
- **`release-harvester-vX.Y`** – Stable release branches per version series
|
||||||
|
- **`vX.Y-head`** – Testing branches for ongoing changes to extension builds in each release series
|
||||||
|
|
||||||
|
> **Note:**
|
||||||
|
> The `vX.Y-head` branches are auto-generated and kept in sync with release branches. Use these for testing the latest changes in each version series.
|
||||||
|
|
||||||
|
## Testing Guidelines
|
||||||
|
|
||||||
|
### UI Extension Testing
|
||||||
|
|
||||||
|
To validate changes in a release series, switch to the appropriate `vX.Y-head` branch. For main branch testing, use `main-head`.
|
||||||
|
|
||||||
|
- Examples:
|
||||||
|
- Test `1.0.x` series → `v1.0-head`
|
||||||
|
- Test `1.5.x` series → `v1.5-head`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. Navigate to **Rancher UI** → **Local** → **App** → **Repositories**
|
||||||
|
2. Refresh the Harvester repository using the target `vX.Y-head` branch
|
||||||
|
3. Go to the **Extensions** page and install the desired version
|
||||||
|
|
||||||
|
### Standalone Mode Testing
|
||||||
|
|
||||||
|
To test the standalone UI, configure Harvester to load the UI from an external source.
|
||||||
|
|
||||||
|
- Examples of `ui-index`:
|
||||||
|
- Main branch → `https://releases.rancher.com/harvester-ui/dashboard/latest/index.html`
|
||||||
|
- Release series `1.5.x` → `https://releases.rancher.com/harvester-ui/dashboard/release-harvester-v1.5/index.html`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. Go to **Harvester UI** → **Advanced** → **Settings** → **UI**
|
||||||
|
2. Set **ui-source** to `External`
|
||||||
|
3. Set **ui-index** to the desired URL
|
||||||
|
|
||||||
|
|
||||||
|
## Release
|
||||||
|
|
||||||
|
The Harvester UI Extension follows the [Harvester](https://github.com/harvester/harvester) release cycle. After RC1 is cut for a new Harvester version, we usually create and work from the corresponding release branch (for example, `release-harvester-v1.8`). The remaining RC builds and the final official release are published from that branch.
|
||||||
|
|
||||||
|
After Harvester releases a new version, update the Harvester entry in rancher/ui-plugin-charts [manifest.json](https://github.com/rancher/ui-plugin-charts/blob/aafd215debbc6cb3100e7ba4b0a542c932397acd/manifest.json#L133-L151). This ensures air-gapped users can pull the new Harvester UI Extension image.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
For developers, after reading through the introduction on this page, head over to our [Getting Started](https://extensions.rancher.io/extensions/next/extensions-getting-started) guide to learn more.
|
If you want to contribute, start by reading this document, then visit our [Getting Started guide](https://extensions.rancher.io/extensions/next/extensions-getting-started) to learn how to develop and submit changes.
|
||||||
|
|
||||||
License
|
## License
|
||||||
=======
|
|
||||||
Copyright (c) 2014-2025 [SUSE, LLC.](https://www.suse.com/)
|
Copyright (c) 2014-2026 [SUSE, LLC.](https://www.suse.com/)
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: ['@commitlint/config-conventional'],
|
||||||
|
rules: {
|
||||||
|
// Enforce conventional commit format
|
||||||
|
'type-enum': [
|
||||||
|
2,
|
||||||
|
'always',
|
||||||
|
[
|
||||||
|
'feat', // New features
|
||||||
|
'fix', // Bug fixes
|
||||||
|
'docs', // Documentation changes
|
||||||
|
'style', // Code style changes (formatting, missing semicolons, etc.)
|
||||||
|
'refactor', // Code refactoring
|
||||||
|
'perf', // Performance improvements
|
||||||
|
'test', // Adding or updating tests
|
||||||
|
'build', // Build system or external dependencies
|
||||||
|
'ci', // CI/CD changes
|
||||||
|
'chore', // Other changes that don't modify src or test files
|
||||||
|
'revert', // Reverts a previous commit
|
||||||
|
'wip', // Work in progress
|
||||||
|
'deps', // Dependency updates
|
||||||
|
'security', // Security fixes
|
||||||
|
]
|
||||||
|
],
|
||||||
|
// Allows the scope to be empty. [0] means the rule is disabled.
|
||||||
|
'scope-empty': [0, 'always'],
|
||||||
|
// Allows any string for the scope.
|
||||||
|
'scope-case': [2, 'always', 'kebab-case'],
|
||||||
|
'type-case': [2, 'always', 'lower-case'],
|
||||||
|
'type-empty': [2, 'never'],
|
||||||
|
'subject-case': [0, 'never'],
|
||||||
|
'subject-empty': [2, 'never'],
|
||||||
|
'subject-full-stop': [2, 'never', '.'],
|
||||||
|
'subject-max-length': [0, 'never'],
|
||||||
|
'header-max-length': [0, 'always', 100],
|
||||||
|
'body-leading-blank': [2, 'always'],
|
||||||
|
'body-max-line-length': [0, 'always', 100],
|
||||||
|
'footer-leading-blank': [2, 'always'],
|
||||||
|
'footer-max-line-length': [0, 'always', 100],
|
||||||
|
},
|
||||||
|
// Ignore merge commits and revert commits
|
||||||
|
ignores: [
|
||||||
|
(commit) => commit.includes('Merge'),
|
||||||
|
(commit) => commit.includes('Revert'),
|
||||||
|
(commit) => commit.includes('merge'),
|
||||||
|
(commit) => commit.includes('revert'),
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# Agents.md
|
||||||
|
|
||||||
|
This folder contains files used to construct a single root `AGENTS.md`
|
||||||
|
|
||||||
|
We do this because
|
||||||
|
- The single commonality between almost all AI tools is that they look for a root `AGENTS.md`
|
||||||
|
- Having the information in a single place for all AI tools is better than duplicating and maintaining the information in multiple files for each AI tool
|
||||||
|
|
||||||
|
## Process
|
||||||
|
1. Add files to or update files in `./docs/agents.md/agents`, `./docs/agents.md/personas`, `./docs/agents.md/contributors`.
|
||||||
|
2. Each file must
|
||||||
|
- Start markdown headers at the `##` level
|
||||||
|
- End in a new line
|
||||||
|
- Order contents in AGENTS.md by giving an optional number prefix
|
||||||
|
3. Run `yarn agents:generate`.
|
||||||
|
- Take the contents of `./docs/agents.md/template_agents.md`
|
||||||
|
- Insert the contents of the agents, personas and contributors files
|
||||||
|
- Output to `./AGENTS.md`
|
||||||
|
4. Commit all changes, including root `./AGENTS.md` and any files that link to it
|
||||||
|
|
||||||
|
## Guidance
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
When referencing code or configuration it's good to include an example
|
||||||
|
|
||||||
|
## Future Tasks
|
||||||
|
- Add real world examples when referencing code
|
||||||
|
- Create a GH workflow to monitor `agents.md` folder files in a PR, if changed run generate script and add new AGENTS.md to PR
|
||||||
|
- Discuss
|
||||||
|
- Three multi AI tool related questions
|
||||||
|
- Questions
|
||||||
|
- how AI specific patterns, like co-pilot instructions, can be expressed in a way to be adopted by or reduce duplication with the root agents.md
|
||||||
|
- how AI tool specific metadata code can be included
|
||||||
|
- multiple agents vs personas approach works
|
||||||
|
- Proposal
|
||||||
|
- Generic information applicable to all AI tools is added to the root `agents.md`
|
||||||
|
- Agent, instruction or other AI specific files or patterns can be used in files specific to that AI tool. They must not contain duplicate information to the root `agents.md`
|
||||||
|
- The root `agents.md` continues to be AI tool agnostic
|
||||||
|
- How can folder specific agents be used
|
||||||
|
- Proposal
|
||||||
|
- `agents.md` are added to folders as long as most LLMs can make use of them and they do not duplicate information in the root `agents.md`
|
||||||
|
|
||||||
|
## Test Prompts
|
||||||
|
|
||||||
|
To validate LLMs are picking up information consider running the following prompts.
|
||||||
|
|
||||||
|
### Information
|
||||||
|
|
||||||
|
Can they provide correct information?
|
||||||
|
|
||||||
|
- `what's this project about?`
|
||||||
|
- `can you tell me the process for fixing a github issue in this repo?`
|
||||||
|
- `what makes a good software developer?`
|
||||||
|
- `what should never be done in this project?`
|
||||||
|
- `what are the folders in this project for?`
|
||||||
|
- `what are the env vars i could use when running e2e tests?`
|
||||||
|
|
||||||
|
### Actions
|
||||||
|
|
||||||
|
Can they use the information in their actions?
|
||||||
|
|
||||||
|
Pending
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
## Boundaries
|
||||||
|
|
||||||
|
- ✅ **Always:**
|
||||||
|
- Write to `.github`, `docs/`, `pkg/harvester`, `scripts/`.
|
||||||
|
- Make commits in a new branch (for a PR).
|
||||||
|
- Run `yarn lint:fix` before commits.
|
||||||
|
- Use conventional commit format:
|
||||||
|
```
|
||||||
|
<type>: <description>
|
||||||
|
```
|
||||||
|
- Follow existing naming conventions (PascalCase for components, camelCase for functions).
|
||||||
|
- After changing a Vue, JS, or TS file, make sure it's automatically formatted with ESLint.
|
||||||
|
- After updating files in `docs/agents.md/`, run `yarn agents:generate` to update root `AGENTS.md`.
|
||||||
|
- ⚠️ **Ask first:**
|
||||||
|
- Adding dependencies
|
||||||
|
- Upgrading dependencies
|
||||||
|
- 🚫 **Never:**
|
||||||
|
- Commit or log secrets, `.env`, `kubeconfig` or any API keys.
|
||||||
|
- Edit `node_modules/`.
|
||||||
|
- Commit directly to `main` (use PRs).
|
||||||
|
- Skip git hooks with `--no-verify` flag.
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
## Tools
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- **Node.js**: `>= 24.0.0` (see `.nvmrc` for the pinned version: `24`)
|
||||||
|
- **Package manager**: `yarn` (v1 classic)
|
||||||
|
|
||||||
|
### Common Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `yarn install --frozen-lockfile` | Install dependencies (CI-safe, no lockfile changes) |
|
||||||
|
| `RANCHER_ENV=harvester API=https://<harvester-ip> yarn dev` | Start development server at `https://127.0.0.1:8005` |
|
||||||
|
| `yarn build-pkg harvester` | Build the Harvester extension package |
|
||||||
|
| `yarn serve-pkgs` | Serve the locally built extension for testing |
|
||||||
|
| `yarn lint` | Run ESLint (zero warnings allowed) |
|
||||||
|
| `yarn lint:fix` | Run ESLint with auto-fix |
|
||||||
|
| `yarn clean` | Clean build artifacts |
|
||||||
|
| `yarn agents:generate` | Regenerate `AGENTS.md` from `docs/agents.md/` sources |
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
- **Start dev server**:
|
||||||
|
```bash
|
||||||
|
RANCHER_ENV=harvester API=https://your-harvester-ip yarn dev
|
||||||
|
```
|
||||||
|
- `API` should point to a running Harvester cluster (e.g., `https://x.x.x.x`).
|
||||||
|
- The dashboard will be available at `https://127.0.0.1:8005`.
|
||||||
|
|
||||||
|
### Building
|
||||||
|
|
||||||
|
- **Build extension package**:
|
||||||
|
```bash
|
||||||
|
yarn build-pkg harvester
|
||||||
|
```
|
||||||
|
- **Serve locally built extension** (for integration testing with a Rancher instance):
|
||||||
|
```bash
|
||||||
|
yarn serve-pkgs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Linting
|
||||||
|
|
||||||
|
- **Check** (must pass with zero warnings):
|
||||||
|
```bash
|
||||||
|
yarn lint
|
||||||
|
```
|
||||||
|
- **Auto-fix**:
|
||||||
|
```bash
|
||||||
|
yarn lint:fix
|
||||||
|
```
|
||||||
|
- ESLint covers `.js`, `.ts`, and `.vue` files.
|
||||||
|
- Always run `yarn lint:fix` before committing.
|
||||||
|
|
||||||
|
### Commit Conventions
|
||||||
|
|
||||||
|
- Commits are validated by [commitlint](https://commitlint.js.org/) via a Husky `commit-msg` hook.
|
||||||
|
- Follow [Conventional Commits](https://www.conventionalcommits.org/) format (configured in `commitlint.config.js`).
|
||||||
|
|
||||||
|
### Agent Documentation
|
||||||
|
|
||||||
|
- Source files live in `docs/agents.md/` (agents, contributors, personas subdirectories).
|
||||||
|
- After editing any source file, regenerate the root `AGENTS.md`:
|
||||||
|
```bash
|
||||||
|
yarn agents:generate
|
||||||
|
```
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
## Getting Started
|
||||||
|
|
||||||
|
Please see the [Harvester UI Extension README](https://github.com/harvester/harvester-ui-extension).
|
||||||
|
|
||||||
|
To get started, follow the `Development Setup` section.
|
||||||
|
|
||||||
|
## Project Information
|
||||||
|
|
||||||
|
- **Tech Stack:**
|
||||||
|
- `Vue.js`: Framework
|
||||||
|
- `Linting`: ESLint
|
||||||
|
- `CSS`: SCSS should be used
|
||||||
|
- `TypeScript`: Primary language for logic.
|
||||||
|
- **Code Style and Standards:**
|
||||||
|
- `Language`: TypeScript is preferred for new code.
|
||||||
|
- `Vue.js`:
|
||||||
|
- Composition API components are preferred over Options API.
|
||||||
|
- Large pages with lots of code and styles should be avoided by breaking the page up into smaller Vue components.
|
||||||
|
- Order the Single File Component blocks as `<script>`, then `<template>`, then `<style>`.
|
||||||
|
- The `<style>` tag should include `lang="scss" scoped`.
|
||||||
|
- `Linting`: Follow the ESLint configuration in the root.
|
||||||
|
|
||||||
|
- **File Structure:**
|
||||||
|
- `.github/`: CI/CD workflows and Renovate config.
|
||||||
|
- `docs/`: Documentation source for `AGENTS.md` generation.
|
||||||
|
- `scripts/`: Bash scripts for build, CI and doc generation.
|
||||||
|
- `pkg/harvester/`: Main extension source. Files are named after K8s resource types (e.g., `kubevirt.io.virtualmachine`, `harvesterhci.io.volume`).
|
||||||
|
- `index.ts`: Plugin entry point — registers the product and auto-imports models/detail/edit views.
|
||||||
|
- `types.ts`: `HCI` constant mapping 60+ K8s resource types to string identifiers.
|
||||||
|
- `components/`: Reusable Vue components (VNC/serial console, settings panels, upgrade banners, filters).
|
||||||
|
- `config/`: Constants — settings keys, table column definitions, feature flags, type mappings, doc links.
|
||||||
|
- `detail/`: Read-only detail views per resource type. Complex resources use subdirectories with tabs.
|
||||||
|
- `dialog/`: Modal dialogs for operations (VM clone/migrate/restart, backup/restore, device passthrough, etc.).
|
||||||
|
- `edit/`: Create/edit forms. Complex resources (e.g., VM) split into subcomponents (CpuMemory, Network, Volume, CloudConfig, etc.).
|
||||||
|
- `formatters/`: Table cell formatters — state badges, usage bars, resource references.
|
||||||
|
- `l10n/`: Localization (`en-us.yaml`).
|
||||||
|
- `list/`: List (table) views per resource type, mirroring `detail/` and `edit/` naming.
|
||||||
|
- `mixins/`: Shared Vue mixins — VM helpers (`harvester-vm/`) and disk helpers (`harvester-disk.js`).
|
||||||
|
- `models/`: Model classes extending `SteveModel` with computed properties and actions. Base class: `harvester.js`.
|
||||||
|
- `pages/`: Route-level pages — dashboard, support, console, members, brand, alertmanager.
|
||||||
|
- `promptRemove/`: Custom delete-confirmation dialogs (VM, backup).
|
||||||
|
- `routing/`: Vue Router config — all product routes (CRUD, console, support, upgrade, etc.).
|
||||||
|
- `store/`: Vuex modules — `harvester-common.js` for shared state; `harvester-store/` for VM/resource creation actions with Steve integration.
|
||||||
|
- `styles/`: Global SCSS files.
|
||||||
|
- `utils/`: Helpers — VM volume templates, CPU/memory calc, cron parsing, regex validators, feature flags.
|
||||||
|
- `validators/`: Form validation functions per resource type, pushing i18n error messages.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Harvester UI Extension Development Guide
|
||||||
|
|
||||||
|
### Backward Compatibility
|
||||||
|
|
||||||
|
The Harvester UI Extension supports earlier cluster versions (e.g., UI Ext v1.8.0 works with clusters v1.7.0 and v1.6.0). It uses Feature Flags defined in `pkg/harvester/config/` to ensure the UI matches the cluster's specific version.
|
||||||
|
|
||||||
|
### Implementation Steps for New Features
|
||||||
|
|
||||||
|
To add a feature in a new release, follow these steps:
|
||||||
|
|
||||||
|
1. **Register**: Add a unique `[Feature Name]` to the corresponding release array in the configuration.
|
||||||
|
2. **Check**: Use the following getter to verify if the feature is enabled for the current version:
|
||||||
|
|
||||||
|
```js
|
||||||
|
computed: {
|
||||||
|
newFeatureEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('[Feature Name]');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Render**: Use the result of the check to conditionally render the UI components.
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
## E2E Tests (Cypress)
|
||||||
|
|
||||||
|
See https://github.com/harvester/harvester-ui-tests
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
## Node Dependencies
|
||||||
|
|
||||||
|
Dependencies are managed via `package.json` and `yarn`
|
||||||
|
|
||||||
|
- To install dependencies use `yarn install`. This will fail if dependencies and versions listed in `package.json` are out of step with the `yarn.lock` file
|
||||||
|
- To add a dependency use `yarn run add:no-lock ...` instead of `yarn add`
|
||||||
|
- To upgrade a dependency use `yarn run upgrade:no-lock ...` instead of `yarn upgrade`
|
||||||
|
|
||||||
|
|
||||||
|
Renovate automatically updates dependencies and opens upgrade PRs after 10:00 AM on Sundays (`Asia/Taipei`).
|
||||||
|
For the Renovate config, see `.github/renovate.json`.
|
||||||
|
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
## Milestone guidance
|
||||||
|
|
||||||
|
- All issues must first be resolved in the `main` branch
|
||||||
|
- If backports are needed they can be made via the backport bot
|
||||||
|
- pull requests
|
||||||
|
- comment `@Mergifyio backport <target branch>` e.g. `@Mergifyio backport release-harvester-v1.8`
|
||||||
|
- All backported pull requests must link to a backported issue
|
||||||
|
|
||||||
|
|
||||||
|
## Creating a branch
|
||||||
|
|
||||||
|
### To resolve an issue
|
||||||
|
- Checkout the branch matching the milestone of the issue `git checkout ${targetMilestoneBranch}`. Replace `${targetMilestoneBranch}` with the target milestone of the issue. For example
|
||||||
|
- `main` for the latest unreleased minor version
|
||||||
|
- `release-harvester-v.X` for release minor versions
|
||||||
|
- `release-harvester-v1.6`
|
||||||
|
- `release-harvester-v1.7`
|
||||||
|
- `release-harvester-v1.8`
|
||||||
|
- Ensure you have the latest of that branch `git pull --rebase`
|
||||||
|
- Checkout the branch to commit the changes to `git checkout issue-${issueNumber}`. Replace `${issueNumber}` with the issue number.
|
||||||
|
|
||||||
|
## Creating a commit
|
||||||
|
|
||||||
|
- This project uses commit-lint with [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) to ensure consistent and meaningful commit messages.
|
||||||
|
|
||||||
|
### Commit Message Format
|
||||||
|
All commit messages must follow the conventional commit format:
|
||||||
|
```
|
||||||
|
<type>: <description>
|
||||||
|
|
||||||
|
[optional body]
|
||||||
|
|
||||||
|
[optional footer(s)]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Supported Types
|
||||||
|
- feat: New features
|
||||||
|
- fix: Bug fixes
|
||||||
|
- docs: Documentation changes
|
||||||
|
- style: Code style changes (formatting, missing semicolons, etc.)
|
||||||
|
- refactor: Code refactoring
|
||||||
|
- perf: Performance improvements
|
||||||
|
- test: Adding or updating tests
|
||||||
|
- build: Build system or external dependencies
|
||||||
|
- ci: CI/CD changes
|
||||||
|
- chore: Other changes that don't modify src or test files
|
||||||
|
- revert: Reverts a previous commit
|
||||||
|
- wip: Work in progress
|
||||||
|
- deps: Dependency updates
|
||||||
|
- security: Security fixes
|
||||||
|
|
||||||
|
## Creating a Pull Request
|
||||||
|
|
||||||
|
- Pull requests must come from forks
|
||||||
|
- Description should always have commit supported type prefix. E.g `fix: XXX`, `feat: OOO`
|
||||||
|
- A Pull Request will only be merged once
|
||||||
|
- ALL CI gates have passed
|
||||||
|
- At least one harvester/harvester-ui-extension code owners reviews and approves the PR
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
## Software Developer
|
||||||
|
|
||||||
|
You are an expert Senior Software Engineer specializing in Vue.js and TypeScript. You have deep knowledge of Kubernetes and the Rancher ecosystem.
|
||||||
|
- **Focus**: Writing clean, maintainable, and performant code.
|
||||||
|
- **Priorities**: Adhering to the project's code style, ensuring type safety, and following best practices for component design.
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
> This `./AGENTS.md` file is generated by running `yarn agents:generate`. `CLAUDE.md` and `GEMINI.md` are symlinks to this file so Claude Code, GitHub Copilot and Gemini all read the same content.
|
||||||
|
|
||||||
|
|
||||||
|
# Project Overview
|
||||||
|
|
||||||
|
The Harvester UI Extension is a Rancher extension that provides the user interface for Harvester within the Rancher Dashboard.
|
||||||
|
|
||||||
|
```
|
||||||
|
Note: This extension is available starting from Rancher 2.10.0. Ensure your Rancher version is 2.10.0 or later to access Harvester integration.
|
||||||
|
```
|
||||||
|
|
||||||
|
# Personas
|
||||||
|
|
||||||
|
`<personas>`
|
||||||
|
|
||||||
|
# Agents
|
||||||
|
|
||||||
|
`<agents>`
|
||||||
|
|
||||||
|
# Contributors Guide
|
||||||
|
|
||||||
|
`<contributors>`
|
||||||
+36
-17
@@ -1,36 +1,47 @@
|
|||||||
{
|
{
|
||||||
"name": "harvester-ui-extension",
|
"name": "harvester-ui-extension",
|
||||||
"version": "1.5.0-rc2",
|
"version": "1.10.0-dev",
|
||||||
"private": false,
|
"private": false,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=24.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rancher/shell": "3.0.2-rc.6",
|
"@babel/plugin-transform-class-static-block": "7.29.7",
|
||||||
|
"@rancher/shell": "3.0.12-rc.7",
|
||||||
|
"@vue-flow/background": "^1.3.0",
|
||||||
|
"@vue-flow/controls": "^1.1.1",
|
||||||
|
"@vue-flow/core": "^1.33.5",
|
||||||
|
"@vue-flow/minimap": "^1.4.0",
|
||||||
"cache-loader": "^4.1.0",
|
"cache-loader": "^4.1.0",
|
||||||
"color": "4.2.3",
|
"color": "4.2.3",
|
||||||
"ip": "2.0.1",
|
"ip": "2.0.1",
|
||||||
|
"@xterm/xterm": "6.0.0",
|
||||||
|
"@xterm/addon-fit": "0.11.0",
|
||||||
|
"@xterm/addon-search": "0.16.0",
|
||||||
|
"@xterm/addon-web-links": "0.12.0",
|
||||||
|
"@xterm/addon-webgl": "0.19.0",
|
||||||
|
"@xterm/addon-canvas": "0.7.0",
|
||||||
"node-polyfill-webpack-plugin": "^3.0.0",
|
"node-polyfill-webpack-plugin": "^3.0.0",
|
||||||
|
"elkjs": "^0.11.0",
|
||||||
"vue-draggable-next": "^2.2.1",
|
"vue-draggable-next": "^2.2.1",
|
||||||
"yaml": "^2.5.1",
|
"yaml": "^2.5.1"
|
||||||
"@babel/plugin-transform-class-static-block": "7.26.0"
|
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@types/node": "~20.10.0",
|
"@types/node": "25.9.5",
|
||||||
"cronstrue": "2.50.0",
|
"cronstrue": "2.59.0",
|
||||||
"d3-color": "3.1.0",
|
"d3-color": "3.1.0",
|
||||||
"ejs": "3.1.9",
|
"ejs": "3.1.10",
|
||||||
"follow-redirects": "1.15.2",
|
"follow-redirects": "1.16.0",
|
||||||
"glob": "7.2.3",
|
"glob": "7.2.3",
|
||||||
"glob-parent": "6.0.2",
|
"glob-parent": "6.0.2",
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
"@types/lodash": "4.17.5",
|
"@types/lodash": "4.17.25",
|
||||||
"merge": "2.1.1",
|
"merge": "2.1.1",
|
||||||
"node-forge": "1.3.1",
|
"node-forge": "1.4.0",
|
||||||
"nth-check": "2.1.1",
|
"nth-check": "2.1.1",
|
||||||
"qs": "6.11.1",
|
"qs": "6.15.3",
|
||||||
"roarr": "7.0.4",
|
"roarr": "7.21.7",
|
||||||
"semver": "7.5.4",
|
"semver": "7.8.5",
|
||||||
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
|
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -38,13 +49,21 @@
|
|||||||
"build": "./node_modules/.bin/vue-cli-service build",
|
"build": "./node_modules/.bin/vue-cli-service build",
|
||||||
"clean": "./node_modules/@rancher/shell/scripts/clean",
|
"clean": "./node_modules/@rancher/shell/scripts/clean",
|
||||||
"lint": "./node_modules/.bin/eslint --max-warnings 0 --ext .js,.ts,.vue .",
|
"lint": "./node_modules/.bin/eslint --max-warnings 0 --ext .js,.ts,.vue .",
|
||||||
|
"lint:fix": "./node_modules/.bin/eslint --fix --max-warnings 0 --ext .js,.ts,.vue .",
|
||||||
"build-pkg": "./node_modules/@rancher/shell/scripts/build-pkg.sh",
|
"build-pkg": "./node_modules/@rancher/shell/scripts/build-pkg.sh",
|
||||||
"serve-pkgs": "./node_modules/@rancher/shell/scripts/serve-pkgs",
|
"serve-pkgs": "./node_modules/@rancher/shell/scripts/serve-pkgs",
|
||||||
"publish-pkgs": "./node_modules/@rancher/shell/scripts/extension/publish",
|
"publish-pkgs": "./node_modules/@rancher/shell/scripts/extension/publish",
|
||||||
"parse-tag-name": "./node_modules/@rancher/shell/scripts/extension/parse-tag-name"
|
"parse-tag-name": "./node_modules/@rancher/shell/scripts/extension/parse-tag-name",
|
||||||
|
"commitlint": "commitlint --edit",
|
||||||
|
"prepare": "husky",
|
||||||
|
"agents:generate": "./scripts/generate-agent-and-persona-mds.sh"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@commitlint/load": "^19.8.1",
|
||||||
|
"@commitlint/cli": "^19.8.1",
|
||||||
|
"@commitlint/config-conventional": "^19.8.1",
|
||||||
"eslint-plugin-node": "^11.1.0",
|
"eslint-plugin-node": "^11.1.0",
|
||||||
"eslint-plugin-promise": "^7.1.0"
|
"eslint-plugin-promise": "^7.1.0",
|
||||||
|
"husky": "^9.1.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
# harvester-ui-extension
|
||||||
|
|
||||||
|
The Harvester UI Extension is a Rancher extension that provides the user interface for [Harvester](https://harvesterhci.io) within the [Rancher Dashboard](https://github.com/rancher/dashboard).
|
||||||
|
|
||||||
|
> **Note:**
|
||||||
|
> This extension is available starting from **Rancher 2.10.0**. Ensure your Rancher version is **2.10.0 or later** to access Harvester integration.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
For Harvester UI extension installation instructions, please refer to the page **Rancher Integration** -> **Harvester UI Extension** in [official Harvester documentation](https://docs.harvesterhci.io).
|
||||||
|
|
||||||
|
## Development Setup
|
||||||
|
|
||||||
|
Ensure **Node.js v20 or later** is installed for development and debugging.
|
||||||
|
|
||||||
|
### Standalone Mode
|
||||||
|
|
||||||
|
Run the extension standalone with hot reload at `https://localhost:8005`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
yarn install
|
||||||
|
|
||||||
|
# Start the development server
|
||||||
|
RANCHER_ENV=harvester API=https://your-harvester-ip yarn dev
|
||||||
|
|
||||||
|
# Example with specific server version
|
||||||
|
RANCHER_ENV=harvester VUE_APP_SERVER_VERSION=v1.5.0 API=https://192.168.1.123 yarn dev
|
||||||
|
```
|
||||||
|
|
||||||
|
You may also define environment variables in a `.env` file:
|
||||||
|
|
||||||
|
```env
|
||||||
|
RANCHER_ENV=harvester
|
||||||
|
VUE_APP_SERVER_VERSION=v1.5.0
|
||||||
|
API=https://192.168.1.123
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rancher Integration Mode
|
||||||
|
|
||||||
|
To run as a Rancher extension, follow the [Rancher UI Extension Guide](https://extensions.rancher.io/extensions/next/extensions-getting-started#running-the-app).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
API=https://your-rancher-ip yarn dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commit Message Guidelines
|
||||||
|
|
||||||
|
This project uses [commit-lint](https://commitlint.js.org/) with [Conventional Commits](https://www.conventionalcommits.org/) to ensure consistent and meaningful commit messages.
|
||||||
|
|
||||||
|
### Commit Message Format
|
||||||
|
|
||||||
|
All commit messages must follow the conventional commit format:
|
||||||
|
|
||||||
|
```
|
||||||
|
<type>[optional scope]: <description>
|
||||||
|
|
||||||
|
[optional body]
|
||||||
|
|
||||||
|
[optional footer(s)]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Supported Types
|
||||||
|
|
||||||
|
- **feat**: New features
|
||||||
|
- **fix**: Bug fixes
|
||||||
|
- **docs**: Documentation changes
|
||||||
|
- **style**: Code style changes (formatting, missing semicolons, etc.)
|
||||||
|
- **refactor**: Code refactoring
|
||||||
|
- **perf**: Performance improvements
|
||||||
|
- **test**: Adding or updating tests
|
||||||
|
- **build**: Build system or external dependencies
|
||||||
|
- **ci**: CI/CD changes
|
||||||
|
- **chore**: Other changes that don't modify src or test files
|
||||||
|
- **revert**: Reverts a previous commit
|
||||||
|
- **wip**: Work in progress
|
||||||
|
- **deps**: Dependency updates
|
||||||
|
- **security**: Security fixes
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Feature
|
||||||
|
git commit -m "feat: add new virtual machine creation wizard"
|
||||||
|
|
||||||
|
# Bug fix
|
||||||
|
git commit -m "fix: resolve memory leak in VM console"
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
git commit -m "docs: update installation instructions"
|
||||||
|
|
||||||
|
# Breaking change
|
||||||
|
git commit -m "feat!: change API endpoint structure
|
||||||
|
|
||||||
|
BREAKING CHANGE: The /api/v1/vms endpoint has been replaced with /api/v2/vms"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Git Hooks
|
||||||
|
|
||||||
|
The project uses [Husky](https://typicode.github.io/husky/) to automatically validate commit messages and run linting before commits:
|
||||||
|
|
||||||
|
- **pre-commit**: Runs ESLint to ensure code quality
|
||||||
|
- **commit-msg**: Validates commit message format using commit-lint
|
||||||
|
|
||||||
|
These hooks are automatically installed when you run `yarn install`.
|
||||||
|
|
||||||
|
### Manual Validation
|
||||||
|
|
||||||
|
You can manually validate commit messages:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Validate the last commit
|
||||||
|
yarn commitlint
|
||||||
|
|
||||||
|
# Validate a specific commit
|
||||||
|
npx commitlint --from <commit-hash>
|
||||||
|
|
||||||
|
# Validate a range of commits
|
||||||
|
npx commitlint --from <start-hash> --to <end-hash>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Branch Structure
|
||||||
|
|
||||||
|
- **`main`** – Main development branch
|
||||||
|
- **`release-harvester-vX.Y`** – Stable release branches per version series
|
||||||
|
- **`vX.Y-head`** – Testing branches for ongoing changes to extension builds in each release series
|
||||||
|
|
||||||
|
> **Note:**
|
||||||
|
> The `vX.Y-head` branches are auto-generated and kept in sync with release branches. Use these for testing the latest changes in each version series.
|
||||||
|
|
||||||
|
## Testing Guidelines
|
||||||
|
|
||||||
|
### UI Extension Testing
|
||||||
|
|
||||||
|
To validate changes in a release series, switch to the appropriate `vX.Y-head` branch. For main branch testing, use `main-head`.
|
||||||
|
|
||||||
|
- Examples:
|
||||||
|
- Test `1.0.x` series → `v1.0-head`
|
||||||
|
- Test `1.5.x` series → `v1.5-head`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. Navigate to **Rancher UI** → **Local** → **App** → **Repositories**
|
||||||
|
2. Refresh the Harvester repository using the target `vX.Y-head` branch
|
||||||
|
3. Go to the **Extensions** page and install the desired version
|
||||||
|
|
||||||
|
### Standalone Mode Testing
|
||||||
|
|
||||||
|
To test the standalone UI, configure Harvester to load the UI from an external source.
|
||||||
|
|
||||||
|
- Examples of `ui-index`:
|
||||||
|
- Main branch → `https://releases.rancher.com/harvester-ui/dashboard/latest/index.html`
|
||||||
|
- Release series `1.5.x` → `https://releases.rancher.com/harvester-ui/dashboard/release-harvester-v1.5/index.html`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. Go to **Harvester UI** → **Advanced** → **Settings** → **UI**
|
||||||
|
2. Set **ui-source** to `External`
|
||||||
|
3. Set **ui-index** to the desired URL
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
If you want to contribute, start by reading this document, then visit our [Getting Started guide](https://extensions.rancher.io/extensions/next/extensions-getting-started) to learn how to develop and submit changes.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Copyright (c) 2014-2026 [SUSE, LLC.](https://www.suse.com/)
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
@@ -271,4 +271,9 @@ export default {
|
|||||||
.filter-label .v-popper__arrow-container {
|
.filter-label .v-popper__arrow-container {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fixed-header-actions .middle .filter + .btn-group {
|
||||||
|
display: inline-flex;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -116,6 +116,11 @@ export default {
|
|||||||
.vm-schedule-dropdown .v-popper__arrow-container {
|
.vm-schedule-dropdown .v-popper__arrow-container {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fixed-header-actions .middle .vm-schedule-filter + .btn-group {
|
||||||
|
display: inline-flex;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
<script>
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import MatchExpressions from '@shell/components/form/MatchExpressions';
|
||||||
|
import ResourceTable from '@shell/components/ResourceTable';
|
||||||
|
import { _EDIT } from '@shell/config/query-params';
|
||||||
|
import { convert, simplify, matching as selectorMatching } from '@shell/utils/selector';
|
||||||
|
import throttle from 'lodash/throttle';
|
||||||
|
import { NODE } from '@shell/config/types';
|
||||||
|
import { NAME, AGE } from '@shell/config/table-headers';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterNodeSelector',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Banner,
|
||||||
|
MatchExpressions,
|
||||||
|
ResourceTable,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: _EDIT,
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
this.updateMatchingResources();
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
return {
|
||||||
|
matchingResources: {
|
||||||
|
matched: 0,
|
||||||
|
matches: [],
|
||||||
|
none: true,
|
||||||
|
sample: null,
|
||||||
|
total: 0,
|
||||||
|
},
|
||||||
|
tableHeaders: [
|
||||||
|
NAME,
|
||||||
|
{
|
||||||
|
name: 'host-ip',
|
||||||
|
labelKey: 'tableHeaders.hostIp',
|
||||||
|
search: ['internalIp'],
|
||||||
|
value: 'internalIp',
|
||||||
|
sort: ['internalIp'],
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cpuManager',
|
||||||
|
labelKey: 'harvester.tableHeaders.cpuManager',
|
||||||
|
value: 'id',
|
||||||
|
formatter: 'HarvesterCPUPinning',
|
||||||
|
width: 150,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'diskState',
|
||||||
|
labelKey: 'tableHeaders.diskState',
|
||||||
|
value: 'diskState',
|
||||||
|
formatter: 'HarvesterDiskState',
|
||||||
|
width: 130,
|
||||||
|
},
|
||||||
|
AGE,
|
||||||
|
],
|
||||||
|
inStore,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
value: {
|
||||||
|
handler: 'updateMatchingResources',
|
||||||
|
deep: true,
|
||||||
|
},
|
||||||
|
allResourcesInScope() {
|
||||||
|
this.updateMatchingResources();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
schema() {
|
||||||
|
return this.$store.getters[`${ this.inStore }/schemaFor`](NODE);
|
||||||
|
},
|
||||||
|
|
||||||
|
selectorExpressions: {
|
||||||
|
get() {
|
||||||
|
return convert(
|
||||||
|
this.value.matchLabels || {},
|
||||||
|
this.value.matchExpressions || []
|
||||||
|
);
|
||||||
|
},
|
||||||
|
set(selectorExpressions) {
|
||||||
|
const { matchLabels, matchExpressions } = simplify(selectorExpressions);
|
||||||
|
|
||||||
|
this.value['matchLabels'] = matchLabels;
|
||||||
|
this.value['matchExpressions'] = matchExpressions;
|
||||||
|
this.updateMatchingResources();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
allNodes() {
|
||||||
|
return this.$store.getters[`${ this.inStore }/all`](NODE) || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
allResourcesInScope() {
|
||||||
|
return this.allNodes.length;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
updateMatchingResources: throttle(function() {
|
||||||
|
const expressions = this.selectorExpressions;
|
||||||
|
const allNodes = this.allNodes;
|
||||||
|
|
||||||
|
// Empty expressions with no key = no match
|
||||||
|
const hasValidExpression = expressions.length > 0 && expressions.every((e) => !!e.key);
|
||||||
|
|
||||||
|
if (!hasValidExpression) {
|
||||||
|
this.matchingResources = {
|
||||||
|
matched: 0,
|
||||||
|
matches: [],
|
||||||
|
none: true,
|
||||||
|
sample: null,
|
||||||
|
total: allNodes.length,
|
||||||
|
};
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matches = selectorMatching(allNodes, expressions, 'metadata.labels');
|
||||||
|
|
||||||
|
this.matchingResources = {
|
||||||
|
matched: matches.length,
|
||||||
|
matches,
|
||||||
|
none: matches.length === 0,
|
||||||
|
sample: matches[0]?.nameDisplay || null,
|
||||||
|
total: allNodes.length,
|
||||||
|
};
|
||||||
|
}, 100, { trailing: true })
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<MatchExpressions
|
||||||
|
v-model:value="selectorExpressions"
|
||||||
|
:mode="mode"
|
||||||
|
:show-remove="false"
|
||||||
|
:type="'node'"
|
||||||
|
:target-resources="allResourcesInScope"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<Banner :color="(matchingResources.none ? 'warning' : 'success')">
|
||||||
|
<span v-clean-html="t('generic.selectors.matchingResources.matchesSome', matchingResources)" />
|
||||||
|
</Banner>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<ResourceTable
|
||||||
|
:rows="matchingResources.matches"
|
||||||
|
:headers="tableHeaders"
|
||||||
|
key-field="id"
|
||||||
|
:table-actions="false"
|
||||||
|
:row-actions="false"
|
||||||
|
:schema="schema"
|
||||||
|
:groupable="false"
|
||||||
|
:search="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -55,8 +55,12 @@ export default {
|
|||||||
!this.latestUpgrade.isUpgradeFailed;
|
!this.latestUpgrade.isUpgradeFailed;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
canUpgrade() {
|
||||||
|
return this.versionOptions.length > 0 && !this.isUpgradeInProgress;
|
||||||
|
},
|
||||||
|
|
||||||
versionOptions() {
|
versionOptions() {
|
||||||
const versions = this.$store.getters['harvester/all'](HCI.VERSION);
|
const versions = this.$store.getters['harvester/all'](HCI.VERSION) || [];
|
||||||
|
|
||||||
return versions.map((V) => V.metadata.name);
|
return versions.map((V) => V.metadata.name);
|
||||||
},
|
},
|
||||||
@@ -154,14 +158,23 @@ export default {
|
|||||||
:cluster="currentCluster.nameDisplay"
|
:cluster="currentCluster.nameDisplay"
|
||||||
/>
|
/>
|
||||||
</h1>
|
</h1>
|
||||||
<button
|
<span
|
||||||
v-if="versionOptions.length && !isUpgradeInProgress"
|
v-clean-tooltip="{
|
||||||
type="button"
|
content: canUpgrade ? t('harvester.upgradePage.upgradeAvailable') : t('harvester.upgradePage.upgradeUnavailable'),
|
||||||
class="btn bg-warning btn-sm"
|
triggers: ['hover', 'focus', 'touch'],
|
||||||
@click="open"
|
}"
|
||||||
|
class="upgrade-btn-wrap"
|
||||||
|
:tabindex="canUpgrade ? -1 : 0"
|
||||||
>
|
>
|
||||||
<t k="harvester.upgradePage.upgrade" />
|
<button
|
||||||
</button>
|
:disabled="!canUpgrade"
|
||||||
|
type="button"
|
||||||
|
class="btn bg-warning btn-sm"
|
||||||
|
@click="open"
|
||||||
|
>
|
||||||
|
<t k="harvester.upgradePage.upgrade" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<ModalWithCard
|
<ModalWithCard
|
||||||
@@ -296,6 +309,18 @@ export default {
|
|||||||
line-height: 23px;
|
line-height: 23px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.upgrade-btn-wrap {
|
||||||
|
display: inline-flex;
|
||||||
|
|
||||||
|
button {
|
||||||
|
color: #000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
color: #000 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.currentVersion {
|
.currentVersion {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script>
|
<script>
|
||||||
import Collapse from '@shell/components/Collapse';
|
import Collapse from '@shell/components/Collapse';
|
||||||
import PercentageBar from '@shell/components/PercentageBar';
|
import PercentageBar from '@shell/components/PercentageBar';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterUpgradeProgressList',
|
name: 'HarvesterUpgradeProgressList',
|
||||||
@@ -25,13 +27,45 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
await this.$store.dispatch('harvester/findAll', { type: HCI.UPGRADE });
|
||||||
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return { open: true };
|
return { open: true };
|
||||||
},
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
showResumeButton() {
|
||||||
|
return this.title === 'Upgrading Node';
|
||||||
|
},
|
||||||
|
latestUpgradeCR() {
|
||||||
|
return this.$store.getters['harvester/all'](HCI.UPGRADE).find( (U) => U.isLatestUpgrade);
|
||||||
|
},
|
||||||
|
resumeUpgradePausedNodeEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('resumeUpgradePausedNode');
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
handleSwitch() {
|
handleSwitch() {
|
||||||
this.open = !this.open;
|
this.open = !this.open;
|
||||||
|
},
|
||||||
|
async resumeNodeUpgrade(nodeName) {
|
||||||
|
if (!this.latestUpgradeCR || !nodeName) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const upgradePauseMapString = this.latestUpgradeCR.metadata.annotations[HCI_ANNOTATIONS.NODE_UPGRADE_PAUSE_MAP] || '{}';
|
||||||
|
const upgradePauseMap = JSON.parse(upgradePauseMapString);
|
||||||
|
|
||||||
|
// update the upgrade CR annotation harvesterhci.io/node-upgrade-pause-map to unpause the node upgrade process
|
||||||
|
upgradePauseMap[`${ nodeName }`] = 'unpause';
|
||||||
|
this.latestUpgradeCR.setAnnotation(HCI_ANNOTATIONS.NODE_UPGRADE_PAUSE_MAP, JSON.stringify(upgradePauseMap));
|
||||||
|
await this.latestUpgradeCR.save();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`unable to update harvester upgrade CR annotations: ${ this.latestUpgradeCR.id }.`, e); // eslint-disable-line no-console
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -63,12 +97,28 @@ export default {
|
|||||||
v-for="(item, i) in list"
|
v-for="(item, i) in list"
|
||||||
:key="i"
|
:key="i"
|
||||||
>
|
>
|
||||||
<p>
|
<div class="upgrade-node-header">
|
||||||
{{ item.name }} <span
|
<div class="upgrade-node-title">
|
||||||
class="status"
|
<p>
|
||||||
:class="{ [item.state]: true }"
|
{{ item.name }}
|
||||||
>{{ item.state }}</span>
|
</p>
|
||||||
</p>
|
<span
|
||||||
|
class="status"
|
||||||
|
:class="{ [item.state]: true }"
|
||||||
|
>
|
||||||
|
{{ item.state }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="showResumeButton && resumeUpgradePausedNodeEnabled && item.state === 'Node-upgrade paused'"
|
||||||
|
type="button"
|
||||||
|
class="btn bg-info btn-sm"
|
||||||
|
data-testid="add-item"
|
||||||
|
@click="resumeNodeUpgrade(item.name)"
|
||||||
|
>
|
||||||
|
{{ t('action.resume') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<PercentageBar
|
<PercentageBar
|
||||||
:model-value="item.percent"
|
:model-value="item.percent"
|
||||||
preferred-direction="MORE"
|
preferred-direction="MORE"
|
||||||
@@ -102,10 +152,21 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.custom-content {
|
.custom-content {
|
||||||
margin-bottom: 14px;
|
.upgrade-node-title {
|
||||||
p {
|
flex: 1 0 80%;
|
||||||
|
margin-right: 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.upgrade-node-header {
|
||||||
|
display:flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
margin-bottom: 14px;
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
@@ -117,6 +178,8 @@ export default {
|
|||||||
}
|
}
|
||||||
.warning {
|
.warning {
|
||||||
color: var(--error);
|
color: var(--error);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
networkEntries: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
storageEntries: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="mappings-cell">
|
||||||
|
<div
|
||||||
|
v-for="(entry, idx) in networkEntries"
|
||||||
|
:key="'net-' + idx"
|
||||||
|
class="mapping-entry"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="icon icon-network"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<span>{{ entry }}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="(entry, idx) in storageEntries"
|
||||||
|
:key="'stor-' + idx"
|
||||||
|
class="mapping-entry"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="icon icon-datastore"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<span>{{ entry }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.mappings-cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapping-entry {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--muted);
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -122,7 +122,7 @@ export default {
|
|||||||
return this.$store.getters['currentCluster'].isLocal;
|
return this.$store.getters['currentCluster'].isLocal;
|
||||||
},
|
},
|
||||||
canEditClusterMembers() {
|
canEditClusterMembers() {
|
||||||
return this.normanClusterRTBSchema?.collectionMethods.find((x) => x.toLowerCase() === 'post');
|
return this.schema?.collectionMethods.find((x) => x.toLowerCase() === 'post');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import { allHash } from '@shell/utils/promise';
|
import { allHash } from '@shell/utils/promise';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
|
|
||||||
import Socket, {
|
import Socket, {
|
||||||
EVENT_CONNECTED,
|
EVENT_CONNECTED,
|
||||||
EVENT_CONNECTING,
|
EVENT_CONNECTING,
|
||||||
@@ -22,74 +21,98 @@ export default {
|
|||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
socket: null,
|
socket: null,
|
||||||
terminal: null,
|
terminal: null,
|
||||||
fitAddon: null,
|
textDecoder: null,
|
||||||
searchAddon: null,
|
fitAddon: null,
|
||||||
webglAddon: null,
|
searchAddon: null,
|
||||||
isOpen: false,
|
webglAddon: null,
|
||||||
isOpening: false,
|
onResize: null,
|
||||||
backlog: [],
|
isOpen: false,
|
||||||
firstTime: true,
|
isOpening: false,
|
||||||
queue: []
|
backlog: [],
|
||||||
|
firstTime: true,
|
||||||
|
queue: [],
|
||||||
|
isDraining: false,
|
||||||
|
drainScheduled: false,
|
||||||
|
drainTimer: null,
|
||||||
|
showSearch: false,
|
||||||
|
searchQuery: '',
|
||||||
|
searchOptions: {
|
||||||
|
caseSensitive: false,
|
||||||
|
regex: false,
|
||||||
|
wholeWord: false,
|
||||||
|
incremental: true,
|
||||||
|
decorations: {
|
||||||
|
matchBackground: '#B45309',
|
||||||
|
matchBorder: '#FDBA74',
|
||||||
|
matchOverviewRuler: '#FDBA74',
|
||||||
|
activeMatchBackground: '#EA580C',
|
||||||
|
activeMatchBorder: '#FED7AA',
|
||||||
|
activeMatchColorOverviewRuler: '#FED7AA',
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
serialConsoleUrl() {
|
||||||
|
return this.value?.getSerialConsolePath || '';
|
||||||
|
},
|
||||||
|
|
||||||
xtermConfig() {
|
xtermConfig() {
|
||||||
return {
|
return {
|
||||||
allowProposedApi: true,
|
allowProposedApi: true,
|
||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
|
cursorStyle: 'bar',
|
||||||
|
cursorWidth: 2,
|
||||||
|
scrollback: 5000,
|
||||||
useStyle: true,
|
useStyle: true,
|
||||||
fontSize: 12,
|
fontFamily: 'JetBrains Mono, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
||||||
|
fontSize: 16,
|
||||||
|
lineHeight: 1.3,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
|
||||||
queue: {
|
|
||||||
handler: debounce(async function(neu) {
|
|
||||||
if (neu.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const msg = await Promise.all(neu);
|
|
||||||
|
|
||||||
(msg || []).forEach((m) => {
|
|
||||||
this.terminal.write(m);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.queue = [];
|
|
||||||
}, 10),
|
|
||||||
deep: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
this.close();
|
this.close();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
serialConsoleUrl() {
|
||||||
|
if (this.terminal) {
|
||||||
|
this.connect();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
|
this.textDecoder = new TextDecoder('utf-8');
|
||||||
await this.setupTerminal();
|
await this.setupTerminal();
|
||||||
await this.connect();
|
await this.connect();
|
||||||
|
this.onResize = debounce(() => this.fit(), 100);
|
||||||
|
window.addEventListener('resize', this.onResize);
|
||||||
|
this.scheduleFit();
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async setupTerminal() {
|
async setupTerminal() {
|
||||||
const docStyle = getComputedStyle(document.querySelector('body'));
|
const docStyle = getComputedStyle(document.querySelector('body'));
|
||||||
const xterm = await import(/* webpackChunkName: "xterm" */ 'xterm');
|
const xterm = await import(/* webpackChunkName: "xterm" */ '@xterm/xterm');
|
||||||
|
|
||||||
const addons = await allHash({
|
const addons = await allHash({
|
||||||
fit: import(/* webpackChunkName: "xterm" */ 'xterm-addon-fit'),
|
fit: import(/* webpackChunkName: "xterm" */ '@xterm/addon-fit'),
|
||||||
webgl: import(/* webpackChunkName: "xterm" */ 'xterm-addon-webgl'),
|
webgl: import(/* webpackChunkName: "xterm" */ '@xterm/addon-webgl'),
|
||||||
weblinks: import(/* webpackChunkName: "xterm" */ 'xterm-addon-web-links'),
|
weblinks: import(/* webpackChunkName: "xterm" */ '@xterm/addon-web-links'),
|
||||||
search: import(/* webpackChunkName: "xterm" */ 'xterm-addon-search'),
|
search: import(/* webpackChunkName: "xterm" */ '@xterm/addon-search'),
|
||||||
|
canvas: import(/* webpackChunkName: "xterm" */ '@xterm/addon-canvas'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const terminal = new xterm.Terminal({
|
const terminal = new xterm.Terminal({
|
||||||
theme: {
|
theme: {
|
||||||
background: docStyle.getPropertyValue('--terminal-bg').trim(),
|
background: docStyle.getPropertyValue('--terminal-bg').trim(),
|
||||||
cursor: docStyle.getPropertyValue('--terminal-cursor').trim(),
|
cursor: docStyle.getPropertyValue('--terminal-text').trim(),
|
||||||
foreground: docStyle.getPropertyValue('--terminal-text').trim()
|
foreground: docStyle.getPropertyValue('--terminal-text').trim()
|
||||||
},
|
},
|
||||||
...this.xtermConfig,
|
...this.xtermConfig,
|
||||||
@@ -97,25 +120,48 @@ export default {
|
|||||||
|
|
||||||
this.fitAddon = new addons.fit.FitAddon();
|
this.fitAddon = new addons.fit.FitAddon();
|
||||||
this.searchAddon = new addons.search.SearchAddon();
|
this.searchAddon = new addons.search.SearchAddon();
|
||||||
|
|
||||||
try {
|
|
||||||
this.webglAddon = new addons.webgl.WebGlAddon();
|
|
||||||
} catch (e) {
|
|
||||||
// Some browsers (Safari) don't support the webgl renderer, so don't use it.
|
|
||||||
this.webglAddon = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
terminal.loadAddon(this.fitAddon);
|
terminal.loadAddon(this.fitAddon);
|
||||||
terminal.loadAddon(this.searchAddon);
|
terminal.loadAddon(this.searchAddon);
|
||||||
terminal.loadAddon(new addons.weblinks.WebLinksAddon());
|
terminal.loadAddon(new addons.weblinks.WebLinksAddon());
|
||||||
terminal.open(this.$refs.xterm);
|
terminal.open(this.$refs.xterm);
|
||||||
|
|
||||||
if ( this.webglAddon ) {
|
// The webgl renderer can fail without throwing: the context may be lost
|
||||||
|
// mid-session, or it can attach but silently never paint, leaving the
|
||||||
|
// terminal's helper textarea at 0x0. Detect both cases and fall back to the
|
||||||
|
// canvas renderer so the terminal stays interactive.
|
||||||
|
try {
|
||||||
|
this.webglAddon = new addons.webgl.WebglAddon();
|
||||||
|
|
||||||
|
this.webglAddon.onContextLoss(() => {
|
||||||
|
if (!this.isUnmounted && this.terminal) {
|
||||||
|
this.fallbackToCanvas(terminal, addons);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
terminal.loadAddon(this.webglAddon);
|
terminal.loadAddon(this.webglAddon);
|
||||||
|
|
||||||
|
// Detect the silent "attaches but never paints" failure after a render frame.
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (this.isUnmounted || !this.terminal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const textarea = this.$refs.xterm?.querySelector('.xterm-helper-textarea');
|
||||||
|
const hasPainted = textarea && textarea.getBoundingClientRect().height > 0;
|
||||||
|
|
||||||
|
if (this.webglAddon && !hasPainted) {
|
||||||
|
this.fallbackToCanvas(terminal, addons);
|
||||||
|
this.fit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// Some browsers (e.g. Safari) don't support the webgl renderer, so don't use it.
|
||||||
|
this.fallbackToCanvas(terminal, addons);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.fit();
|
this.fit();
|
||||||
this.flush();
|
this.flush();
|
||||||
|
this.scheduleDrainQueue();
|
||||||
|
|
||||||
terminal.onData((input) => {
|
terminal.onData((input) => {
|
||||||
const msg = this.str2ab(input);
|
const msg = this.str2ab(input);
|
||||||
@@ -123,9 +169,132 @@ export default {
|
|||||||
this.write(msg);
|
this.write(msg);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
terminal.attachCustomKeyEventHandler((event) => {
|
||||||
|
const key = event.key?.toLowerCase();
|
||||||
|
|
||||||
|
if ((event.ctrlKey || event.metaKey) && key === 'f') {
|
||||||
|
event.preventDefault();
|
||||||
|
this.openSearch();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === 'escape' && this.showSearch) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.closeSearch();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
this.terminal = terminal;
|
this.terminal = terminal;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
openSearch() {
|
||||||
|
this.showSearch = true;
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const input = this.$refs.searchInput;
|
||||||
|
|
||||||
|
if (input) {
|
||||||
|
input.focus();
|
||||||
|
input.select();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
closeSearch() {
|
||||||
|
this.showSearch = false;
|
||||||
|
this.searchQuery = '';
|
||||||
|
this.clearSearchHighlights();
|
||||||
|
this.terminal?.focus();
|
||||||
|
},
|
||||||
|
|
||||||
|
clearSearchHighlights() {
|
||||||
|
this.searchAddon?.clearActiveDecoration();
|
||||||
|
this.searchAddon?.clearDecorations();
|
||||||
|
this.terminal?.clearSelection();
|
||||||
|
|
||||||
|
// Disposing the search decorations doesn't repaint the webgl/canvas
|
||||||
|
// layer, so force a refresh to remove the lingering highlight.
|
||||||
|
if (this.terminal) {
|
||||||
|
this.terminal.refresh(0, this.terminal.rows - 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
findNext() {
|
||||||
|
if (!this.searchAddon || !this.searchQuery) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.searchAddon.findNext(this.searchQuery, this.searchOptions);
|
||||||
|
},
|
||||||
|
|
||||||
|
findPrevious() {
|
||||||
|
if (!this.searchAddon || !this.searchQuery) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.searchAddon.findPrevious(this.searchQuery, this.searchOptions);
|
||||||
|
},
|
||||||
|
|
||||||
|
onSearchInput() {
|
||||||
|
if (!this.searchQuery) {
|
||||||
|
this.clearSearchHighlights();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.findNext();
|
||||||
|
},
|
||||||
|
|
||||||
|
onSearchKeydown(event) {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (event.shiftKey) {
|
||||||
|
this.findPrevious();
|
||||||
|
} else {
|
||||||
|
this.findNext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
event.preventDefault();
|
||||||
|
this.closeSearch();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
scheduleFit() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
this.fit();
|
||||||
|
|
||||||
|
// Re-fit after layout settles to avoid undersized rows on first paint.
|
||||||
|
setTimeout(() => this.fit(), 120);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Dispose the webgl renderer (if any) and switch to the canvas renderer.
|
||||||
|
// Used when webgl fails to construct, loses its context, or silently never paints.
|
||||||
|
fallbackToCanvas(terminal, addons) {
|
||||||
|
if (this.isUnmounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.webglAddon?.dispose();
|
||||||
|
this.webglAddon = null;
|
||||||
|
|
||||||
|
if (!this.canvasAddon) {
|
||||||
|
this.canvasAddon = new addons.canvas.CanvasAddon();
|
||||||
|
terminal.loadAddon(this.canvasAddon);
|
||||||
|
}
|
||||||
|
this.fit();
|
||||||
|
},
|
||||||
|
|
||||||
str2ab(str) {
|
str2ab(str) {
|
||||||
const enc = new TextEncoder();
|
const enc = new TextEncoder();
|
||||||
|
|
||||||
@@ -140,12 +309,131 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
enqueueMessage(messagePromise) {
|
||||||
|
this.queue.push(messagePromise);
|
||||||
|
this.scheduleDrainQueue();
|
||||||
|
},
|
||||||
|
|
||||||
|
scheduleDrainQueue() {
|
||||||
|
if (this.drainScheduled || this.isDraining || !this.terminal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.drainScheduled = true;
|
||||||
|
|
||||||
|
const shouldDrainImmediately = this.queue.length <= 3;
|
||||||
|
|
||||||
|
const runDrain = () => {
|
||||||
|
this.drainScheduled = false;
|
||||||
|
this.drainTimer = null;
|
||||||
|
this.drainQueue();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (shouldDrainImmediately) {
|
||||||
|
runDrain();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.drainTimer = requestAnimationFrame(runDrain);
|
||||||
|
},
|
||||||
|
|
||||||
|
async decodeMessageData(data) {
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.textDecoder) {
|
||||||
|
this.textDecoder = new TextDecoder('utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data instanceof Blob) {
|
||||||
|
const buffer = await data.arrayBuffer();
|
||||||
|
|
||||||
|
return this.textDecoder.decode(new Uint8Array(buffer), { stream: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data instanceof ArrayBuffer) {
|
||||||
|
return this.textDecoder.decode(new Uint8Array(data), { stream: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ArrayBuffer.isView(data)) {
|
||||||
|
const view = data;
|
||||||
|
const bytes = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
|
||||||
|
|
||||||
|
return this.textDecoder.decode(bytes, { stream: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(data || '');
|
||||||
|
},
|
||||||
|
|
||||||
|
async drainQueue() {
|
||||||
|
if (this.isDraining || !this.terminal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isDraining = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (this.queue.length > 0) {
|
||||||
|
const pendingMessages = this.queue.splice(0, this.queue.length);
|
||||||
|
const settledMessages = await Promise.allSettled(pendingMessages);
|
||||||
|
const messages = settledMessages
|
||||||
|
.filter((result) => result.status === 'fulfilled')
|
||||||
|
.map((result) => result.value);
|
||||||
|
const output = messages.filter(Boolean).join('');
|
||||||
|
|
||||||
|
if (output) {
|
||||||
|
await this.writeOutputInChunks(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.queue.length > 0) {
|
||||||
|
await this.nextFrame();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.isDraining = false;
|
||||||
|
|
||||||
|
// If data arrived between the last while-check and releasing the lock,
|
||||||
|
// immediately schedule another drain so output never gets stuck.
|
||||||
|
if (this.queue.length > 0 && this.terminal) {
|
||||||
|
this.scheduleDrainQueue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async writeOutputInChunks(output) {
|
||||||
|
if (!output) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunkSize = output.length > 20000 ? 16000 : output.length > 8000 ? 8000 : output.length;
|
||||||
|
|
||||||
|
if (chunkSize === output.length) {
|
||||||
|
this.terminal.write(output);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let index = 0; index < output.length; index += chunkSize) {
|
||||||
|
this.terminal.write(output.slice(index, index + chunkSize));
|
||||||
|
|
||||||
|
if (index + chunkSize < output.length) {
|
||||||
|
await this.nextFrame();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
nextFrame() {
|
||||||
|
return new Promise((resolve) => requestAnimationFrame(resolve));
|
||||||
|
},
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
this.terminal.clear();
|
this.terminal.clear();
|
||||||
},
|
},
|
||||||
|
|
||||||
getSocketUrl() {
|
getSocketUrl() {
|
||||||
return `${ this.value?.getSerialConsolePath }`;
|
return this.serialConsoleUrl;
|
||||||
},
|
},
|
||||||
|
|
||||||
async connect() {
|
async connect() {
|
||||||
@@ -181,6 +469,7 @@ export default {
|
|||||||
this.fit();
|
this.fit();
|
||||||
this.flush();
|
this.flush();
|
||||||
}
|
}
|
||||||
|
this.scheduleFit();
|
||||||
|
|
||||||
if (this.firstTime) {
|
if (this.firstTime) {
|
||||||
this.socket.send(this.str2ab('\n'));
|
this.socket.send(this.str2ab('\n'));
|
||||||
@@ -195,7 +484,7 @@ export default {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.socket.addEventListener(EVENT_MESSAGE, (e) => {
|
this.socket.addEventListener(EVENT_MESSAGE, (e) => {
|
||||||
this.queue.push(e.detail.data.text());
|
this.enqueueMessage(this.decodeMessageData(e.detail.data));
|
||||||
});
|
});
|
||||||
|
|
||||||
this.socket.connect();
|
this.socket.connect();
|
||||||
@@ -218,22 +507,19 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.fitAddon.fit();
|
this.fitAddon.fit();
|
||||||
|
|
||||||
const { rows, cols } = this.fitAddon.proposeDimensions();
|
|
||||||
|
|
||||||
if ( !this.isOpen ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const message = JSON.stringify({
|
|
||||||
Width: cols,
|
|
||||||
Height: rows
|
|
||||||
});
|
|
||||||
|
|
||||||
this.socket.send(this.str2ab(message));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
|
if (this.onResize) {
|
||||||
|
window.removeEventListener('resize', this.onResize);
|
||||||
|
this.onResize = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.drainTimer) {
|
||||||
|
cancelAnimationFrame(this.drainTimer);
|
||||||
|
this.drainTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
if ( this.socket ) {
|
if ( this.socket ) {
|
||||||
this.socket.disconnect();
|
this.socket.disconnect();
|
||||||
}
|
}
|
||||||
@@ -248,6 +534,44 @@ export default {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="harvester-shell-container">
|
<div class="harvester-shell-container">
|
||||||
|
<div
|
||||||
|
v-if="showSearch"
|
||||||
|
class="shell-search"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
ref="searchInput"
|
||||||
|
v-model="searchQuery"
|
||||||
|
type="text"
|
||||||
|
class="shell-search-input"
|
||||||
|
placeholder="Search in terminal"
|
||||||
|
@input="onSearchInput"
|
||||||
|
@keydown="onSearchKeydown"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn role-secondary btn-sm shell-search-btn"
|
||||||
|
type="button"
|
||||||
|
title="Shift+Enter"
|
||||||
|
@click="findPrevious"
|
||||||
|
>
|
||||||
|
Prev
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn role-secondary btn-sm shell-search-btn"
|
||||||
|
type="button"
|
||||||
|
title="Enter"
|
||||||
|
@click="findNext"
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn role-secondary btn-sm shell-search-btn"
|
||||||
|
type="button"
|
||||||
|
title="Esc"
|
||||||
|
@click="closeSearch"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
ref="xterm"
|
ref="xterm"
|
||||||
class="shell-body"
|
class="shell-body"
|
||||||
@@ -257,18 +581,73 @@ export default {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../../../../node_modules/xterm/css/xterm.css';
|
@import '../../../../node_modules/@xterm/xterm/css/xterm.css';
|
||||||
|
|
||||||
body, #__nuxt, #__layout, MAIN {
|
body, #__nuxt, #__layout, MAIN {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.harvester-shell-container {
|
.harvester-shell-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
.shell-body, .terminal.xterm {
|
.resize-observer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell-search {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 10px;
|
||||||
|
z-index: 5;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: color-mix(in srgb, var(--body-bg) 88%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell-search-input {
|
||||||
|
min-width: 220px;
|
||||||
|
max-width: 420px;
|
||||||
|
flex: 0 1 320px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--body-bg);
|
||||||
|
color: var(--body-text);
|
||||||
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell-search-btn {
|
||||||
|
min-width: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell-body {
|
||||||
|
flex: 1 1 auto;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal.xterm,
|
||||||
|
.terminal.xterm .xterm-viewport,
|
||||||
|
.terminal.xterm .xterm-screen,
|
||||||
|
.terminal.xterm .xterm-scrollable-element {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal.xterm .xterm-scrollable-element {
|
||||||
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import ActionMenu from '@shell/components/ActionMenuShell';
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import AsyncButton from '@shell/components/AsyncButton';
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
import { HCI_ALLOWED_SETTINGS, HCI_SETTING } from '../config/settings';
|
import { HCI_ALLOWED_SETTINGS, HCI_SINGLE_CLUSTER_ALLOWED_SETTING, HCI_SETTING } from '../config/settings';
|
||||||
|
import { DOC } from '../config/doc-links';
|
||||||
|
import { docLink } from '../utils/feature-flags';
|
||||||
|
|
||||||
const CATEGORY = {
|
const CATEGORY = {
|
||||||
ui: [
|
ui: [
|
||||||
'branding',
|
'branding',
|
||||||
'ui-source',
|
'ui-source',
|
||||||
'ui-plugin-index',
|
|
||||||
'ui-index',
|
'ui-index',
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
@@ -19,6 +21,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
AsyncButton,
|
AsyncButton,
|
||||||
Banner,
|
Banner,
|
||||||
|
ActionMenu
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
@@ -30,15 +33,23 @@ export default {
|
|||||||
category: {
|
category: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
searchQuery: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
const categorySettings = this.filterCategorySettings();
|
const categorySettings = this.filterCategorySettings();
|
||||||
|
const filteredSettings = this.filterSearchSettings(categorySettings, this.searchQuery);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
HCI_SETTING,
|
HCI_SETTING,
|
||||||
categorySettings,
|
categorySettings,
|
||||||
|
filteredSettings,
|
||||||
|
originalHideMap: this.createHideMap(categorySettings)
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -48,12 +59,85 @@ export default {
|
|||||||
settings: {
|
settings: {
|
||||||
deep: true,
|
deep: true,
|
||||||
handler() {
|
handler() {
|
||||||
this['categorySettings'] = this.filterCategorySettings();
|
this.categorySettings = this.filterCategorySettings();
|
||||||
|
this.filteredSettings = this.filterSearchSettings(this.categorySettings, this.searchQuery);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
searchQuery: {
|
||||||
|
immediate: true,
|
||||||
|
handler(newQuery) {
|
||||||
|
const filtered = this.filterSearchSettings(this.categorySettings, newQuery);
|
||||||
|
|
||||||
|
this.filteredSettings = newQuery ? this.openJsonSettings(filtered) : filtered.map((s) => ({ ...s, hide: this.originalHideMap[s.id] ?? false }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
createHideMap(settings = []) {
|
||||||
|
const map = settings.reduce((acc, s) => {
|
||||||
|
acc[s.id] = s.hide ?? false;
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {} );
|
||||||
|
|
||||||
|
return map;
|
||||||
|
},
|
||||||
|
filterSearchSettings(settings, searchKey) {
|
||||||
|
if (!searchKey) {
|
||||||
|
return this.filterCategorySettings();
|
||||||
|
}
|
||||||
|
const searchQuery = searchKey.toLowerCase();
|
||||||
|
|
||||||
|
return settings.filter((setting) => {
|
||||||
|
const id = setting.id?.toLowerCase() || '';
|
||||||
|
|
||||||
|
// filter by id
|
||||||
|
if (id.includes(searchQuery) ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let description = this.t(setting.description, this.getDocLinkParams(setting) || {}, true)?.toLowerCase() || '';
|
||||||
|
|
||||||
|
// remove <a> tags from description
|
||||||
|
if (description.includes('<a')) {
|
||||||
|
description = description.replace(/<a[^>]*>(.*?)<\/a>/g, '$1');
|
||||||
|
}
|
||||||
|
// filter by description
|
||||||
|
if (description.includes(searchQuery)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter by customized value
|
||||||
|
if (setting.customized === true && setting.data?.value) {
|
||||||
|
const value = setting.data.value?.toLowerCase() || '';
|
||||||
|
|
||||||
|
return value.includes(searchQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter by json value
|
||||||
|
if (setting.kind === 'json' && setting.json) {
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(setting.json);
|
||||||
|
const jsonString = JSON.stringify(json).toLowerCase();
|
||||||
|
|
||||||
|
return jsonString.includes(searchQuery);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`${ setting.id }: wrong format`, e); // eslint-disable-line no-console
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter by default value
|
||||||
|
if (setting.data?.default) {
|
||||||
|
return setting.data?.default.includes(searchQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
filterCategorySettings() {
|
filterCategorySettings() {
|
||||||
return this.settings.filter((s) => {
|
return this.settings.filter((s) => {
|
||||||
if (!this.getFeatureEnabled(s.featureFlag)) {
|
if (!this.getFeatureEnabled(s.featureFlag)) {
|
||||||
@@ -74,25 +158,21 @@ export default {
|
|||||||
return id ? this.$store.getters['harvester-common/getFeatureEnabled'](id) : true;
|
return id ? this.$store.getters['harvester-common/getFeatureEnabled'](id) : true;
|
||||||
},
|
},
|
||||||
|
|
||||||
showActionMenu(e, setting) {
|
|
||||||
const actionElement = e.srcElement;
|
|
||||||
|
|
||||||
this.$store.commit(`action-menu/show`, {
|
|
||||||
resources: setting.data,
|
|
||||||
elem: actionElement
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
getSettingOption(id) {
|
getSettingOption(id) {
|
||||||
return HCI_ALLOWED_SETTINGS.find((setting) => setting.id === id);
|
return HCI_ALLOWED_SETTINGS.find((setting) => setting.id === id);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
openJsonSettings(settings) {
|
||||||
|
return settings.map((s) => s.hide ? { ...s, hide: false } : s);
|
||||||
|
},
|
||||||
|
|
||||||
toggleHide(s) {
|
toggleHide(s) {
|
||||||
this.categorySettings.find((setting) => {
|
const setting = this.filteredSettings.find((setting) => setting.id === s.id);
|
||||||
if (setting.id === s.id) {
|
|
||||||
setting.hide = !setting.hide;
|
if (setting) {
|
||||||
}
|
setting.hide = !setting.hide;
|
||||||
});
|
this.originalHideMap[setting.id] = setting.hide;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async testConnect(buttonDone, value) {
|
async testConnect(buttonDone, value) {
|
||||||
@@ -118,6 +198,19 @@ export default {
|
|||||||
}
|
}
|
||||||
buttonDone(false);
|
buttonDone(false);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getDocLinkParams(setting) {
|
||||||
|
const settingConfig = HCI_ALLOWED_SETTINGS[setting.id] || HCI_SINGLE_CLUSTER_ALLOWED_SETTING[setting.id];
|
||||||
|
|
||||||
|
if (settingConfig?.docPath) {
|
||||||
|
const version = this.$store.getters['harvester-common/getServerVersion']();
|
||||||
|
const url = docLink(DOC[settingConfig.docPath], version);
|
||||||
|
|
||||||
|
return { url };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -126,7 +219,7 @@ export default {
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
v-for="(setting, i) in categorySettings"
|
v-for="(setting, i) in filteredSettings"
|
||||||
:key="i"
|
:key="i"
|
||||||
class="advanced-setting mb-20"
|
class="advanced-setting mb-20"
|
||||||
>
|
>
|
||||||
@@ -148,7 +241,7 @@ export default {
|
|||||||
Experimental
|
Experimental
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<h2 v-clean-html="t(setting.description, {}, true)">
|
<h2 v-clean-html="t(setting.description, getDocLinkParams(setting) || {}, true)">
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -156,15 +249,12 @@ export default {
|
|||||||
:id="setting.id"
|
:id="setting.id"
|
||||||
class="action"
|
class="action"
|
||||||
>
|
>
|
||||||
<button
|
<ActionMenu
|
||||||
aria-haspopup="true"
|
:resource="setting.data"
|
||||||
aria-expanded="false"
|
:button-aria-label="t('advancedSettings.edit.label')"
|
||||||
type="button"
|
data-testid="action-button"
|
||||||
class="btn btn-sm role-multi-action actions"
|
button-role="tertiary"
|
||||||
@click="showActionMenu($event, setting)"
|
/>
|
||||||
>
|
|
||||||
<i class="icon icon-actions" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div value>
|
<div value>
|
||||||
@@ -221,6 +311,12 @@ export default {
|
|||||||
{{ setting.data.errMessage }}
|
{{ setting.data.errMessage }}
|
||||||
</Banner>
|
</Banner>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="filteredSettings.length === 0"
|
||||||
|
class="advanced-setting mb-20 no-search-match"
|
||||||
|
>
|
||||||
|
<p> {{ t('harvester.setting.noSearchMatch') }} </p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -271,4 +367,8 @@ export default {
|
|||||||
padding: 2px 10px;
|
padding: 2px 10px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.no-search-match {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export default {
|
|||||||
{{ t('harvester.upgradePage.upgradeInfo.tip') }}
|
{{ t('harvester.upgradePage.upgradeInfo.tip') }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p class="mb-5">
|
<p>
|
||||||
{{ t('harvester.upgradePage.upgradeInfo.moreNotes') }} <a
|
{{ t('harvester.upgradePage.upgradeInfo.moreNotes') }} <a
|
||||||
:href="releaseVersion"
|
:href="releaseVersion"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|||||||
@@ -55,14 +55,21 @@ export default {
|
|||||||
const host = window.location.host;
|
const host = window.location.host;
|
||||||
const prefix = window.location.pathname.replace(this.$route.path, '');
|
const prefix = window.location.pathname.replace(this.$route.path, '');
|
||||||
const params = this.$route?.params;
|
const params = this.$route?.params;
|
||||||
|
const popupWidth = Math.max(800, screen.width - 200);
|
||||||
|
const popupHeight = Math.max(600, screen.height - 200);
|
||||||
|
const popupLeft = Math.max(0, window.screenX + Math.floor((window.outerWidth - popupWidth) / 2));
|
||||||
|
const popupTop = Math.max(0, window.screenY + Math.floor((window.outerHeight - popupHeight) / 2));
|
||||||
|
|
||||||
const url = `https://${ host }${ prefix }/${ PRODUCT_NAME }/c/${ params.cluster }/console/${ uid }/${ type }`;
|
const url = `https://${ host }${ prefix }/${ PRODUCT_NAME }/c/${ params.cluster }/console/${ uid }/${ type }`;
|
||||||
|
|
||||||
window.open(
|
// Defer so v-select can finish closing the dropdown before the popup steals focus
|
||||||
url,
|
this.$nextTick(() => {
|
||||||
'_blank',
|
window.open(
|
||||||
`toolbars=0,width=${ screen.width - 200 },height=${ screen.height - 200 },left=0,top=0,noreferrer`
|
url,
|
||||||
);
|
'_blank',
|
||||||
|
`toolbars=0,width=${ popupWidth },height=${ popupHeight },left=${ popupLeft },top=${ popupTop },noreferrer`
|
||||||
|
);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
isEmpty(o) {
|
isEmpty(o) {
|
||||||
|
|||||||
@@ -257,8 +257,12 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
reconnect() {
|
||||||
|
this.$refs.novncConsole.reconnect();
|
||||||
|
},
|
||||||
|
|
||||||
softReboot() {
|
softReboot() {
|
||||||
this.vmResource.softrebootVM();
|
this.vmResource.doSoftReboot();
|
||||||
},
|
},
|
||||||
|
|
||||||
showKeysModal() {
|
showKeysModal() {
|
||||||
@@ -306,6 +310,13 @@ export default {
|
|||||||
{{ t("harvester.action.softreboot") }}
|
{{ t("harvester.action.softreboot") }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="btn btn-sm bg-primary"
|
||||||
|
@click="reconnect"
|
||||||
|
>
|
||||||
|
{{ t("harvester.action.reconnect") }}
|
||||||
|
</button>
|
||||||
|
|
||||||
<v-dropdown
|
<v-dropdown
|
||||||
v-if="!hideCustomKeysBar"
|
v-if="!hideCustomKeysBar"
|
||||||
ref="customKeyPopover"
|
ref="customKeyPopover"
|
||||||
@@ -372,3 +383,15 @@ export default {
|
|||||||
background: rgb(40, 40, 40);
|
background: rgb(40, 40, 40);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.vm-console .v-popper__arrow-container {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.vm-console .v-popper__popper{
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.vm-console .v-popper__inner{
|
||||||
|
overflow-y: visible;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
<script>
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import { NAMESPACE } from '@shell/config/types';
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterClusterPodSecurityStandard',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
RadioGroup,
|
||||||
|
LabeledSelect,
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
await this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE });
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
let enabled = false;
|
||||||
|
let whitelistedNamespaces = [];
|
||||||
|
let privilegedNamespaces = [];
|
||||||
|
let restrictedNamespaces = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(this.value.value || this.value.default || '{}');
|
||||||
|
|
||||||
|
enabled = !!parsed.enabled;
|
||||||
|
whitelistedNamespaces = parsed.whitelistedNamespacesList ? parsed.whitelistedNamespacesList.split(',') : [];
|
||||||
|
privilegedNamespaces = parsed.privilegedNamespacesList ? parsed.privilegedNamespacesList.split(',') : [];
|
||||||
|
restrictedNamespaces = parsed.restrictedNamespacesList ? parsed.restrictedNamespacesList.split(',') : [];
|
||||||
|
} catch (e) {
|
||||||
|
enabled = false;
|
||||||
|
whitelistedNamespaces = [];
|
||||||
|
privilegedNamespaces = [];
|
||||||
|
restrictedNamespaces = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled,
|
||||||
|
whitelistedNamespaces,
|
||||||
|
privilegedNamespaces,
|
||||||
|
restrictedNamespaces,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
enabledOptions() {
|
||||||
|
return [
|
||||||
|
{ label: this.t('generic.enabled'), value: true },
|
||||||
|
{ label: this.t('generic.disabled'), value: false },
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
allNamespaces() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
return this.$store.getters[`${ inStore }/all`](NAMESPACE).filter((ns) => !ns.isSystem).map((ns) => ns.id);
|
||||||
|
},
|
||||||
|
|
||||||
|
whitelistedOptions() {
|
||||||
|
const excluded = new Set([...this.privilegedNamespaces, ...this.restrictedNamespaces]);
|
||||||
|
|
||||||
|
return this.allNamespaces.filter((ns) => !excluded.has(ns));
|
||||||
|
},
|
||||||
|
|
||||||
|
privilegedOptions() {
|
||||||
|
const excluded = new Set([...this.whitelistedNamespaces, ...this.restrictedNamespaces]);
|
||||||
|
|
||||||
|
return this.allNamespaces.filter((ns) => !excluded.has(ns));
|
||||||
|
},
|
||||||
|
|
||||||
|
restrictedOptions() {
|
||||||
|
const excluded = new Set([...this.whitelistedNamespaces, ...this.privilegedNamespaces]);
|
||||||
|
|
||||||
|
return this.allNamespaces.filter((ns) => !excluded.has(ns));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
useDefault() {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(this.value.default || '{}');
|
||||||
|
|
||||||
|
this.enabled = !!parsed.enabled;
|
||||||
|
this.whitelistedNamespaces = parsed.whitelistedNamespacesList ? parsed.whitelistedNamespacesList.split(',') : [];
|
||||||
|
this.privilegedNamespaces = parsed.privilegedNamespacesList ? parsed.privilegedNamespacesList.split(',') : [];
|
||||||
|
this.restrictedNamespaces = parsed.restrictedNamespacesList ? parsed.restrictedNamespacesList.split(',') : [];
|
||||||
|
} catch (e) {
|
||||||
|
this.enabled = false;
|
||||||
|
this.whitelistedNamespaces = [];
|
||||||
|
this.privilegedNamespaces = [];
|
||||||
|
this.restrictedNamespaces = [];
|
||||||
|
}
|
||||||
|
this.save();
|
||||||
|
},
|
||||||
|
|
||||||
|
onUpdateEnabled() {
|
||||||
|
if (!this.enabled) {
|
||||||
|
this.whitelistedNamespaces = [];
|
||||||
|
this.privilegedNamespaces = [];
|
||||||
|
this.restrictedNamespaces = [];
|
||||||
|
}
|
||||||
|
this.save();
|
||||||
|
},
|
||||||
|
|
||||||
|
updateWhitelisted(selected) {
|
||||||
|
this.whitelistedNamespaces = selected;
|
||||||
|
this.save();
|
||||||
|
},
|
||||||
|
|
||||||
|
updatePrivileged(selected) {
|
||||||
|
this.privilegedNamespaces = selected;
|
||||||
|
this.save();
|
||||||
|
},
|
||||||
|
|
||||||
|
updateRestricted(selected) {
|
||||||
|
this.restrictedNamespaces = selected;
|
||||||
|
this.save();
|
||||||
|
},
|
||||||
|
|
||||||
|
save() {
|
||||||
|
this.value.value = JSON.stringify({
|
||||||
|
enabled: this.enabled,
|
||||||
|
whitelistedNamespacesList: this.whitelistedNamespaces.join(','),
|
||||||
|
privilegedNamespacesList: this.privilegedNamespaces.join(','),
|
||||||
|
restrictedNamespacesList: this.restrictedNamespaces.join(','),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<RadioGroup
|
||||||
|
v-model:value="enabled"
|
||||||
|
name="enabled"
|
||||||
|
:options="enabledOptions"
|
||||||
|
@update:value="onUpdateEnabled"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template v-if="enabled">
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="whitelistedNamespaces"
|
||||||
|
:label="t('harvester.setting.clusterPodSecurityStandard.whitelistedNamespaces.label')"
|
||||||
|
:options="whitelistedOptions"
|
||||||
|
:multiple="true"
|
||||||
|
:mode="mode"
|
||||||
|
@update:value="updateWhitelisted"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="privilegedNamespaces"
|
||||||
|
:label="t('harvester.setting.clusterPodSecurityStandard.privilegedNamespaces.label')"
|
||||||
|
:options="privilegedOptions"
|
||||||
|
:multiple="true"
|
||||||
|
:mode="mode"
|
||||||
|
@update:value="updatePrivileged"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="restrictedNamespaces"
|
||||||
|
:label="t('harvester.setting.clusterPodSecurityStandard.restrictedNamespaces.label')"
|
||||||
|
:options="restrictedOptions"
|
||||||
|
:multiple="true"
|
||||||
|
:mode="mode"
|
||||||
|
@update:value="updateRestricted"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
<script>
|
||||||
|
import MessageLink from '@shell/components/MessageLink';
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import { HCI_SETTING } from '../../config/settings';
|
||||||
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterEditClusterRegistrationURL',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
LabeledInput, MessageLink, Checkbox, Banner
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
let parseDefaultValue = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
parseDefaultValue = JSON.parse(this.value.value);
|
||||||
|
} catch (error) {
|
||||||
|
parseDefaultValue.url = this.value.value;
|
||||||
|
parseDefaultValue.insecureSkipTLSVerify = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
parseDefaultValue,
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
toCA() {
|
||||||
|
return `${ HCI_SETTING.ADDITIONAL_CA }?mode=edit`;
|
||||||
|
},
|
||||||
|
clusterRegistrationTLSVerifyEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('clusterRegistrationTLSVerify');
|
||||||
|
},
|
||||||
|
registrationURL: {
|
||||||
|
get() {
|
||||||
|
return this.clusterRegistrationTLSVerifyEnabled ? this.parseDefaultValue.url : this.parseDefaultValue;
|
||||||
|
},
|
||||||
|
|
||||||
|
set(value) {
|
||||||
|
if (this.clusterRegistrationTLSVerifyEnabled) {
|
||||||
|
this.parseDefaultValue.url = value;
|
||||||
|
} else {
|
||||||
|
this.parseDefaultValue = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
getDefaultValue() {
|
||||||
|
if (this.clusterRegistrationTLSVerifyEnabled) {
|
||||||
|
return { url: '', insecureSkipTLSVerify: false };
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateUrl() {
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
update() {
|
||||||
|
if (this.clusterRegistrationTLSVerifyEnabled) {
|
||||||
|
this.value['value'] = JSON.stringify(this.parseDefaultValue);
|
||||||
|
} else {
|
||||||
|
this.value['value'] = this.parseDefaultValue;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
useDefault() {
|
||||||
|
this.parseDefaultValue = this.getDefaultValue();
|
||||||
|
},
|
||||||
|
|
||||||
|
updateInsecureSkipTLSVerify(newValue) {
|
||||||
|
const { url = '' } = this.parseDefaultValue;
|
||||||
|
|
||||||
|
this.parseDefaultValue = { url, insecureSkipTLSVerify: newValue };
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="row"
|
||||||
|
>
|
||||||
|
<div class="col span-12">
|
||||||
|
<Banner color="info">
|
||||||
|
<MessageLink
|
||||||
|
:to="toCA"
|
||||||
|
target="_blank"
|
||||||
|
prefix-label="harvester.setting.clusterRegistrationUrl.tip.prefix"
|
||||||
|
middle-label="harvester.setting.clusterRegistrationUrl.tip.middle"
|
||||||
|
suffix-label="harvester.setting.clusterRegistrationUrl.tip.suffix"
|
||||||
|
/>
|
||||||
|
</Banner>
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="registrationURL"
|
||||||
|
class="mb-20"
|
||||||
|
:mode="mode"
|
||||||
|
:label="t('harvester.setting.clusterRegistrationUrl.url')"
|
||||||
|
@update:value="updateUrl"
|
||||||
|
/>
|
||||||
|
<div v-if="clusterRegistrationTLSVerifyEnabled">
|
||||||
|
<Checkbox
|
||||||
|
v-model:value="parseDefaultValue.insecureSkipTLSVerify"
|
||||||
|
class="check mb-5"
|
||||||
|
type="checkbox"
|
||||||
|
:label="t('harvester.setting.clusterRegistrationUrl.insecureSkipTLSVerify')"
|
||||||
|
@update:value="updateInsecureSkipTLSVerify"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,294 @@
|
|||||||
|
<script>
|
||||||
|
import { _EDIT } from '@shell/config/query-params';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import InfoBox from '@shell/components/InfoBox';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { CSI_DRIVER } from '../../types';
|
||||||
|
import { LONGHORN_DRIVER } from '@shell/config/types';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CSIOnlineExpandValidation',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Banner,
|
||||||
|
InfoBox,
|
||||||
|
LabeledSelect,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: _EDIT,
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
registerBeforeHook: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await allHash({ csiDrivers: this.$store.dispatch(`${ inStore }/findAll`, { type: CSI_DRIVER }) });
|
||||||
|
this.fetchError = null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch CSI drivers:', error); // eslint-disable-line no-console
|
||||||
|
this.fetchError = this.t(
|
||||||
|
'harvester.setting.csiOnlineExpandValidation.failedToLoadDrivers',
|
||||||
|
{ error: error.message || error },
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
configArr: [],
|
||||||
|
parseError: null,
|
||||||
|
fetchError: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
const initValue = this.value.value || this.value.default || '{}';
|
||||||
|
|
||||||
|
this.configArr = this.parseValue(initValue);
|
||||||
|
this.registerBeforeHook?.(this.willSave, 'willSave');
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
allErrors() {
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
if (this.fetchError) {
|
||||||
|
errors.push(this.fetchError);
|
||||||
|
}
|
||||||
|
if (this.parseError) {
|
||||||
|
errors.push(this.parseError);
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
},
|
||||||
|
|
||||||
|
csiDrivers() {
|
||||||
|
if (this.fetchError) return [];
|
||||||
|
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
return this.$store.getters[`${ inStore }/all`](CSI_DRIVER) || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
provisioners() {
|
||||||
|
const usedKeys = this.configArr.map(({ key }) => key);
|
||||||
|
|
||||||
|
return this.csiDrivers
|
||||||
|
.filter(({ name }) => !usedKeys.includes(name))
|
||||||
|
.map(({ name }) => name);
|
||||||
|
},
|
||||||
|
|
||||||
|
provisionerValue() {
|
||||||
|
return [
|
||||||
|
{ label: 'True', value: true },
|
||||||
|
{ label: 'False', value: false },
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
disableAdd() {
|
||||||
|
return this.parseError || this.fetchError || this.configArr.length >= this.csiDrivers.length;
|
||||||
|
},
|
||||||
|
|
||||||
|
disableConfigEditing() {
|
||||||
|
return this.parseError || this.fetchError;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
'value.value'(newVal, oldVal) {
|
||||||
|
if (newVal !== oldVal) {
|
||||||
|
this.configArr = this.parseValue(newVal || '{}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
_convertToBoolean(value) {
|
||||||
|
if (typeof value === 'boolean') return value;
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const lowerCaseValue = value.toLowerCase();
|
||||||
|
|
||||||
|
if (lowerCaseValue === 'true') return true;
|
||||||
|
if (lowerCaseValue === 'false') return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false; // default to false for any other string or non-boolean type
|
||||||
|
},
|
||||||
|
parseValue(raw) {
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(raw);
|
||||||
|
|
||||||
|
this.parseError = null;
|
||||||
|
|
||||||
|
return Object.entries(json).map(([key, value]) => ({
|
||||||
|
key,
|
||||||
|
value: this._convertToBoolean(value),
|
||||||
|
}));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[CSIOnlineExpandValidation] JSON Parsing Error:', raw, e); // eslint-disable-line no-console
|
||||||
|
this.parseError = this.t(
|
||||||
|
'harvester.setting.csiOnlineExpandValidation.invalidJsonFormat',
|
||||||
|
{ error: e.message },
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
stringifyConfig() {
|
||||||
|
const obj = {};
|
||||||
|
|
||||||
|
this.configArr.forEach(({ key, value }) => {
|
||||||
|
obj[key] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.configArr.length ? JSON.stringify(obj) : '';
|
||||||
|
},
|
||||||
|
|
||||||
|
update() {
|
||||||
|
this.value.value = this.stringifyConfig();
|
||||||
|
},
|
||||||
|
|
||||||
|
willSave() {
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
this.configArr.forEach(({ key }) => {
|
||||||
|
if (!key) {
|
||||||
|
errors.push(
|
||||||
|
this.t('validation.required', { key: this.t('harvester.setting.csiOnlineExpandValidation.provisioner') }, true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.value.value = this.stringifyConfig();
|
||||||
|
|
||||||
|
return errors.length ? Promise.reject(errors) : Promise.resolve();
|
||||||
|
},
|
||||||
|
|
||||||
|
useDefault() {
|
||||||
|
this.configArr = this.parseValue(this.value.default || '{}');
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
disableEdit(driverKey) {
|
||||||
|
return this.fetchError || driverKey === LONGHORN_DRIVER;
|
||||||
|
},
|
||||||
|
|
||||||
|
add() {
|
||||||
|
if (this.disableConfigEditing) return;
|
||||||
|
|
||||||
|
this.configArr.push({ key: '', value: true });
|
||||||
|
},
|
||||||
|
|
||||||
|
remove(index) {
|
||||||
|
if (this.disableConfigEditing) return;
|
||||||
|
|
||||||
|
this.configArr.splice(index, 1);
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
onValueChange(idx, newVal) {
|
||||||
|
if (this.disableConfigEditing) return;
|
||||||
|
|
||||||
|
const val = newVal === 'true' ? true : newVal === 'false' ? false : newVal;
|
||||||
|
|
||||||
|
this.configArr[idx].value = val;
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Banner
|
||||||
|
v-for="(errorMsg, index) in allErrors"
|
||||||
|
:key="index"
|
||||||
|
color="error"
|
||||||
|
>
|
||||||
|
{{ errorMsg }}
|
||||||
|
</Banner>
|
||||||
|
<InfoBox
|
||||||
|
v-for="(driver, idx) in configArr"
|
||||||
|
:key="idx"
|
||||||
|
class="box"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="role-link btn btn-sm remove"
|
||||||
|
type="button"
|
||||||
|
:disabled="disableEdit(driver.key)"
|
||||||
|
@click="remove(idx)"
|
||||||
|
>
|
||||||
|
<i class="icon icon-x" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="driver.key"
|
||||||
|
label-key="harvester.setting.csiOnlineExpandValidation.provisioner"
|
||||||
|
required
|
||||||
|
searchable
|
||||||
|
:mode="mode"
|
||||||
|
:disabled="disableEdit(driver.key)"
|
||||||
|
:options="provisioners"
|
||||||
|
@update:value="update"
|
||||||
|
@keydown.native.enter.prevent
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="driver.value"
|
||||||
|
:value="driver.value.toString()"
|
||||||
|
label-key="harvester.setting.csiOnlineExpandValidation.value"
|
||||||
|
required
|
||||||
|
searchable
|
||||||
|
:mode="mode"
|
||||||
|
:disabled="disableEdit(driver.key)"
|
||||||
|
:options="provisionerValue"
|
||||||
|
@update:value="val => onValueChange(idx, val)"
|
||||||
|
@keydown.native.enter.prevent
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</InfoBox>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="btn btn-sm role-primary"
|
||||||
|
:disabled="disableAdd"
|
||||||
|
@click="add"
|
||||||
|
>
|
||||||
|
{{ t('generic.add') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.box {
|
||||||
|
position: relative;
|
||||||
|
padding-top: 40px;
|
||||||
|
}
|
||||||
|
.remove {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
|
||||||
@@ -62,7 +61,7 @@ export default {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class="row"
|
class="row"
|
||||||
@update:value="update"
|
@input="update"
|
||||||
>
|
>
|
||||||
<div class="col span-12">
|
<div class="col span-12">
|
||||||
<LabeledInput
|
<LabeledInput
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
<script>
|
||||||
|
import UnitInput from '@shell/components/form/UnitInput';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterInstanceManagerResources',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
UnitInput,
|
||||||
|
Banner,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
value: '',
|
||||||
|
default: '{}'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const resources = this.parseJSON(this.value?.value) || this.parseJSON(this.value?.default) || {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
resources,
|
||||||
|
parseError: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
parseJSON(string) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(string);
|
||||||
|
} catch (e) {
|
||||||
|
this.parseError = this.t('harvester.setting.instanceManagerResources.parseError', { error: e.message });
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
update() {
|
||||||
|
if (!this.value) return;
|
||||||
|
|
||||||
|
const cpu = { ...this.resources?.cpu };
|
||||||
|
|
||||||
|
if (cpu.v1 !== null && cpu.v1 !== undefined) cpu.v1 = String(cpu.v1);
|
||||||
|
if (cpu.v2 !== null && cpu.v2 !== undefined) cpu.v2 = String(cpu.v2);
|
||||||
|
|
||||||
|
this.value.value = JSON.stringify({ ...this.resources, cpu });
|
||||||
|
},
|
||||||
|
|
||||||
|
useDefault() {
|
||||||
|
if (this.value?.default) {
|
||||||
|
this.resources = this.parseJSON(this.value.default) || {};
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Banner
|
||||||
|
v-if="parseError"
|
||||||
|
color="error"
|
||||||
|
>
|
||||||
|
{{ parseError }}
|
||||||
|
</Banner>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<UnitInput
|
||||||
|
v-model:value="resources.cpu.v1"
|
||||||
|
:label="t('harvester.setting.instanceManagerResources.v1')"
|
||||||
|
suffix="%"
|
||||||
|
:delay="0"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
required
|
||||||
|
:mode="mode"
|
||||||
|
class="mb-20"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
<UnitInput
|
||||||
|
v-model:value="resources.cpu.v2"
|
||||||
|
:label="t('harvester.setting.instanceManagerResources.v2')"
|
||||||
|
suffix="%"
|
||||||
|
:delay="0"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
required
|
||||||
|
:mode="mode"
|
||||||
|
class="mb-20"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
<script>
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
import UnitInput from '@shell/components/form/UnitInput';
|
||||||
|
import { MEBIBYTE } from '../../utils/unit';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'KubevirtMigration',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
LabeledInput,
|
||||||
|
UnitInput,
|
||||||
|
RadioGroup,
|
||||||
|
Banner
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
registerBeforeHook: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
value: '',
|
||||||
|
default: '{}'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const migration = this.parseJSON(this.value?.value) || this.parseJSON(this.value?.default) || {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
MEBIBYTE,
|
||||||
|
migration,
|
||||||
|
parseError: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.registerBeforeHook?.(this.willSave, 'willSave');
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
parseJSON(string) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(string);
|
||||||
|
} catch (e) {
|
||||||
|
this.parseError = this.t('kubevirtMigration.parseError', { error: e.message });
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error('Failed to parse JSON:', e.message);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateValue() {
|
||||||
|
if (this.value) {
|
||||||
|
this.value.value = JSON.stringify(this.migration);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
useDefault() {
|
||||||
|
if (this.value?.default) {
|
||||||
|
const defaultMigration = this.parseJSON(this.value.default) || {};
|
||||||
|
|
||||||
|
this.migration = defaultMigration;
|
||||||
|
this.updateValue();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async willSave() {
|
||||||
|
this.updateValue();
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Banner
|
||||||
|
v-if="parseError"
|
||||||
|
color="error"
|
||||||
|
>
|
||||||
|
{{ parseError }}
|
||||||
|
</Banner>
|
||||||
|
|
||||||
|
<div class="migration-field">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value.number="migration.parallelMigrationsPerCluster"
|
||||||
|
:label="t('harvester.setting.kubevirtMigration.parallelMigrationsPerCluster')"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
/>
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value.number="migration.parallelOutboundMigrationsPerNode"
|
||||||
|
:label="t('harvester.setting.kubevirtMigration.parallelOutboundMigrationsPerNode')"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
/>
|
||||||
|
<UnitInput
|
||||||
|
v-model:value="migration.bandwidthPerMigration"
|
||||||
|
min="0"
|
||||||
|
:label="t('harvester.setting.kubevirtMigration.bandwidthPerMigration')"
|
||||||
|
:mode="mode"
|
||||||
|
:suffix="MEBIBYTE"
|
||||||
|
:tooltip="t('harvester.setting.kubevirtMigration.bandwidthPerMigrationTooltip', _, true)"
|
||||||
|
/>
|
||||||
|
<UnitInput
|
||||||
|
v-model:value="migration.completionTimeoutPerGiB"
|
||||||
|
:label="t('harvester.setting.kubevirtMigration.completionTimeoutPerGiB')"
|
||||||
|
:mode="mode"
|
||||||
|
:suffix="migration.completionTimeoutPerGiB === 1 ? 'Second' : 'Seconds'"
|
||||||
|
min="10"
|
||||||
|
/>
|
||||||
|
<UnitInput
|
||||||
|
v-model:value="migration.progressTimeout"
|
||||||
|
:label="t('harvester.setting.kubevirtMigration.progressTimeout')"
|
||||||
|
:mode="mode"
|
||||||
|
:suffix="migration.progressTimeout === 1 ? 'Second' : 'Seconds'"
|
||||||
|
min="10"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-for="field in ['allowAutoConverge','allowPostCopy','unsafeMigrationOverride','allowWorkloadDisruption','disableTLS','matchSELinuxLevelOnMigration']"
|
||||||
|
:key="field"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="mb-5"
|
||||||
|
:for="field"
|
||||||
|
>{{ t(`harvester.setting.kubevirtMigration.${field}`) }}</label>
|
||||||
|
<RadioGroup
|
||||||
|
:id="field"
|
||||||
|
v-model:value="migration[field]"
|
||||||
|
:options="[
|
||||||
|
{ label: t('advancedSettings.edit.trueOption'), value: true },
|
||||||
|
{ label: t('advancedSettings.edit.falseOption'), value: false },
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.migration-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
<script>
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
import { SECRET } from '@shell/config/types';
|
||||||
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
|
import FileSelector, { createOnSelected } from '@shell/components/form/FileSelector';
|
||||||
|
import YamlEditor from '@shell/components/YamlEditor';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterRancherCluster',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
RadioGroup,
|
||||||
|
FileSelector,
|
||||||
|
YamlEditor
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
registerBeforeHook: {
|
||||||
|
type: Function,
|
||||||
|
required: false,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
let parseDefaultValue = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = this.value.value || this.value.default || '{}';
|
||||||
|
const parsed = JSON.parse(data);
|
||||||
|
|
||||||
|
parseDefaultValue = {
|
||||||
|
kubeConfig: '',
|
||||||
|
removeUpstreamClusterWhenNamespaceIsDeleted: parsed.removeUpstreamClusterWhenNamespaceIsDeleted || false
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
parseDefaultValue = {
|
||||||
|
kubeConfig: '',
|
||||||
|
removeUpstreamClusterWhenNamespaceIsDeleted: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
parseDefaultValue,
|
||||||
|
errors: [],
|
||||||
|
existingSecret: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async created() {
|
||||||
|
await this.checkExistingSecret();
|
||||||
|
if (this.registerBeforeHook) {
|
||||||
|
this.registerBeforeHook(this.willSave, 'willSave');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onKeySelected: createOnSelected('parseDefaultValue.kubeConfig'),
|
||||||
|
|
||||||
|
update() {
|
||||||
|
if (this.parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted) {
|
||||||
|
this.value['value'] = JSON.stringify({ removeUpstreamClusterWhenNamespaceIsDeleted: true });
|
||||||
|
} else {
|
||||||
|
this.value['value'] = this.value.default || '{}';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async checkExistingSecret() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
await this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET });
|
||||||
|
const secrets = this.$store.getters[`${ inStore }/all`](SECRET) || [];
|
||||||
|
|
||||||
|
this.existingSecret = secrets.find((secret) => secret.metadata.name === 'rancher-cluster-config' &&
|
||||||
|
secret.metadata.namespace === 'harvester-system'
|
||||||
|
);
|
||||||
|
|
||||||
|
// If the secret exists and has data, populate the kubeConfig
|
||||||
|
if (this.existingSecret?.data?.kubeConfig) {
|
||||||
|
const decodedContent = atob(this.existingSecret.data.kubeConfig);
|
||||||
|
|
||||||
|
this.parseDefaultValue.kubeConfig = decodedContent;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.update();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async createOrUpdateRancherKubeConfigSecret() {
|
||||||
|
this.errors = [];
|
||||||
|
// Check if kubeConfig is provided
|
||||||
|
if (!this.parseDefaultValue.kubeConfig) {
|
||||||
|
this.errors.push(this.t('validation.required', { key: this.t('harvester.setting.rancherCluster.kubeConfig') }, true));
|
||||||
|
|
||||||
|
return Promise.reject(this.errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let secret;
|
||||||
|
|
||||||
|
if (this.existingSecret) {
|
||||||
|
secret = this.existingSecret;
|
||||||
|
secret.setData('kubeConfig', this.parseDefaultValue.kubeConfig);
|
||||||
|
} else {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
secret = await this.$store.dispatch(`${ inStore }/create`, {
|
||||||
|
apiVersion: 'v1',
|
||||||
|
kind: 'Secret',
|
||||||
|
metadata: {
|
||||||
|
name: 'rancher-cluster-config',
|
||||||
|
namespace: 'harvester-system'
|
||||||
|
},
|
||||||
|
type: 'secret',
|
||||||
|
data: { kubeConfig: btoa(this.parseDefaultValue.kubeConfig) }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await secret.save();
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
} catch (err) {
|
||||||
|
this.errors = exceptionToErrorsArray(err);
|
||||||
|
|
||||||
|
return Promise.reject(this.errors);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteRancherKubeConfigSecret() {
|
||||||
|
if (this.existingSecret) {
|
||||||
|
this.existingSecret.remove();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async willSave() {
|
||||||
|
// Only create or update secret if enabled
|
||||||
|
if (this.parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted) {
|
||||||
|
await this.createOrUpdateRancherKubeConfigSecret();
|
||||||
|
} else {
|
||||||
|
await this.deleteRancherKubeConfigSecret();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
|
||||||
|
useDefault() {
|
||||||
|
this.parseDefaultValue = {
|
||||||
|
kubeConfig: '',
|
||||||
|
removeUpstreamClusterWhenNamespaceIsDeleted: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
'parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted'(val, oldVal) {
|
||||||
|
if (val && !oldVal && this.existingSecret?.data?.kubeConfig) {
|
||||||
|
// Populate kubeConfig with the existing secret value
|
||||||
|
this.parseDefaultValue.kubeConfig = atob(this.existingSecret.data.kubeConfig);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'parseDefaultValue.kubeConfig'(val) {
|
||||||
|
this.$refs.yaml?.updateValue(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="row mt-20">
|
||||||
|
<div class="col span-12">
|
||||||
|
<RadioGroup
|
||||||
|
v-model:value="parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted"
|
||||||
|
:label="t('harvester.setting.rancherCluster.removeUpstreamClusterWhenNamespaceIsDeleted')"
|
||||||
|
name="removeUpstreamClusterWhenNamespaceIsDeleted"
|
||||||
|
:options="[true, false]"
|
||||||
|
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted"
|
||||||
|
class="row mt-20"
|
||||||
|
>
|
||||||
|
<div class="col span-12">
|
||||||
|
<FileSelector
|
||||||
|
class="btn btn-sm bg-primary mb-10"
|
||||||
|
:label="t('generic.readFromFile')"
|
||||||
|
@selected="onKeySelected"
|
||||||
|
/>
|
||||||
|
<YamlEditor
|
||||||
|
ref="yaml"
|
||||||
|
v-model:value="parseDefaultValue.kubeConfig"
|
||||||
|
class="yaml-editor"
|
||||||
|
:editor-mode="mode === 'view' ? 'VIEW_CODE' : 'EDIT_CODE'"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
$yaml-height: 540px;
|
||||||
|
|
||||||
|
:deep() .yaml-editor{
|
||||||
|
flex: 1;
|
||||||
|
min-height: $yaml-height;
|
||||||
|
& .code-mirror .CodeMirror {
|
||||||
|
position: initial;
|
||||||
|
height: auto;
|
||||||
|
min-height: $yaml-height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,371 @@
|
|||||||
|
<script>
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
import ArrayList from '@shell/components/form/ArrayList';
|
||||||
|
import { isValidCIDR } from '@shell/utils/validators/cidr';
|
||||||
|
import { _EDIT } from '@shell/config/query-params';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { HCI } from '../../types';
|
||||||
|
import { NETWORK_TYPE } from '../../config/types';
|
||||||
|
|
||||||
|
const { L2VLAN, UNTAGGED } = NETWORK_TYPE;
|
||||||
|
const SHARE_STORAGE_NETWORK = 'share-storage-network';
|
||||||
|
const NETWORK = 'network';
|
||||||
|
|
||||||
|
const DEFAULT_DEDICATED_NETWORK = {
|
||||||
|
vlan: '',
|
||||||
|
clusterNetwork: '',
|
||||||
|
range: '',
|
||||||
|
exclude: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'RwxNetworkSetting',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
RadioGroup,
|
||||||
|
Banner,
|
||||||
|
ArrayList,
|
||||||
|
LabeledInput,
|
||||||
|
LabeledSelect,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
registerBeforeHook: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: _EDIT,
|
||||||
|
},
|
||||||
|
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
await allHash({
|
||||||
|
clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }),
|
||||||
|
vlanStatus: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VLAN_STATUS }),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
let enabled = false; // enabled / disabled options
|
||||||
|
let shareStorageNetwork = false; // shareStorageNetwork / dedicatedRwxNetwork options
|
||||||
|
let dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
|
||||||
|
let networkType = L2VLAN;
|
||||||
|
let exclude = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsedValue = JSON.parse(this.value.value || this.value.default || '{}');
|
||||||
|
const parsedNetwork = parsedValue?.[NETWORK] || parsedValue || {};
|
||||||
|
|
||||||
|
if (parsedValue && typeof parsedValue === 'object') {
|
||||||
|
shareStorageNetwork = !!parsedValue[SHARE_STORAGE_NETWORK];
|
||||||
|
networkType = 'vlan' in parsedNetwork ? L2VLAN : UNTAGGED;
|
||||||
|
dedicatedNetwork = {
|
||||||
|
vlan: parsedNetwork.vlan || '',
|
||||||
|
clusterNetwork: parsedNetwork.clusterNetwork || '',
|
||||||
|
range: parsedNetwork.range || '',
|
||||||
|
};
|
||||||
|
exclude = parsedNetwork?.exclude?.toString().split(',') || [];
|
||||||
|
enabled = shareStorageNetwork || !!(parsedNetwork.vlan || parsedNetwork.clusterNetwork || parsedNetwork.range);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
enabled = false;
|
||||||
|
shareStorageNetwork = false;
|
||||||
|
dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled,
|
||||||
|
shareStorageNetwork,
|
||||||
|
dedicatedNetwork,
|
||||||
|
networkType,
|
||||||
|
exclude,
|
||||||
|
defaultAddValue: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
if (this.registerBeforeHook) {
|
||||||
|
this.registerBeforeHook(this.willSave, 'willSave');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
showDedicatedNetworkConfig() {
|
||||||
|
return this.enabled && !this.shareStorageNetwork;
|
||||||
|
},
|
||||||
|
|
||||||
|
showVlan() {
|
||||||
|
return this.networkType === L2VLAN;
|
||||||
|
},
|
||||||
|
|
||||||
|
networkTypes() {
|
||||||
|
return [L2VLAN, UNTAGGED];
|
||||||
|
},
|
||||||
|
|
||||||
|
clusterNetworkOptions() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
|
||||||
|
const clusterNetworksOptions = this.networkType === UNTAGGED ? clusterNetworks.filter((network) => network.id !== 'mgmt') : clusterNetworks;
|
||||||
|
|
||||||
|
return clusterNetworksOptions.map((network) => {
|
||||||
|
const disabled = !network.isReadyForStorageNetwork;
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: disabled ? `${ network.id } (${ this.t('generic.notReady') })` : network.id,
|
||||||
|
value: network.id,
|
||||||
|
disabled,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onUpdateEnabled() {
|
||||||
|
if (!this.enabled) {
|
||||||
|
this.shareStorageNetwork = false;
|
||||||
|
this.dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
|
||||||
|
}
|
||||||
|
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
onUpdateNetworkType() {
|
||||||
|
if (this.shareStorageNetwork) {
|
||||||
|
this.dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
|
||||||
|
}
|
||||||
|
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
onUpdateDedicatedType(neu) {
|
||||||
|
this.dedicatedNetwork.clusterNetwork = '';
|
||||||
|
|
||||||
|
if (neu === L2VLAN) {
|
||||||
|
this.dedicatedNetwork.vlan = '';
|
||||||
|
} else {
|
||||||
|
delete this.dedicatedNetwork.vlan;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
inputVlan(neu) {
|
||||||
|
if (neu === '') {
|
||||||
|
this.dedicatedNetwork.vlan = '';
|
||||||
|
this.update();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newValue = Number(neu);
|
||||||
|
|
||||||
|
if (newValue > 4094) {
|
||||||
|
this.dedicatedNetwork.vlan = 4094;
|
||||||
|
} else if (newValue < 1) {
|
||||||
|
this.dedicatedNetwork.vlan = 1;
|
||||||
|
} else {
|
||||||
|
this.dedicatedNetwork.vlan = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
useDefault() {
|
||||||
|
this.enabled = false;
|
||||||
|
this.shareStorageNetwork = false;
|
||||||
|
this.dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
update() {
|
||||||
|
const value = { [SHARE_STORAGE_NETWORK]: false };
|
||||||
|
|
||||||
|
if (this.enabled && this.shareStorageNetwork) {
|
||||||
|
value[SHARE_STORAGE_NETWORK] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.showDedicatedNetworkConfig) {
|
||||||
|
value[NETWORK] = {};
|
||||||
|
|
||||||
|
if (this.showVlan) {
|
||||||
|
value[NETWORK].vlan = this.dedicatedNetwork.vlan;
|
||||||
|
}
|
||||||
|
|
||||||
|
value[NETWORK].clusterNetwork = this.dedicatedNetwork.clusterNetwork;
|
||||||
|
value[NETWORK].range = this.dedicatedNetwork.range;
|
||||||
|
|
||||||
|
const excludeList = this.exclude.filter((ip) => ip);
|
||||||
|
|
||||||
|
if (Array.isArray(excludeList) && excludeList.length > 0) {
|
||||||
|
value[NETWORK].exclude = excludeList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.value.value = JSON.stringify(value);
|
||||||
|
},
|
||||||
|
|
||||||
|
willSave() {
|
||||||
|
this.update();
|
||||||
|
|
||||||
|
if (!this.showDedicatedNetworkConfig) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
if (this.showVlan && !this.dedicatedNetwork.vlan) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.vlan') }, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.dedicatedNetwork.clusterNetwork) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.clusterNetwork') }, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.dedicatedNetwork.range) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.range.label') }, true));
|
||||||
|
} else if (!isValidCIDR(this.dedicatedNetwork.range)) {
|
||||||
|
errors.push(this.t('harvester.setting.storageNetwork.range.invalid', null, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.exclude) {
|
||||||
|
const hasInvalidCIDR = this.exclude.find((cidr) => {
|
||||||
|
return cidr && !isValidCIDR(cidr);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasInvalidCIDR) {
|
||||||
|
errors.push(this.t('harvester.setting.storageNetwork.exclude.invalid', null, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
return Promise.reject(errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="mode">
|
||||||
|
<Banner color="warning">
|
||||||
|
<t
|
||||||
|
k="harvester.setting.rwxNetwork.warning"
|
||||||
|
:raw="true"
|
||||||
|
/>
|
||||||
|
</Banner>
|
||||||
|
<RadioGroup
|
||||||
|
v-model:value="enabled"
|
||||||
|
class="mb-20"
|
||||||
|
name="rwx-network-enable"
|
||||||
|
:options="[true,false]"
|
||||||
|
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||||
|
@update:value="onUpdateEnabled"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RadioGroup
|
||||||
|
v-if="enabled"
|
||||||
|
v-model:value="shareStorageNetwork"
|
||||||
|
class="mb-20"
|
||||||
|
name="rwx-network-type"
|
||||||
|
:options="[true,false]"
|
||||||
|
:labels="[t('harvester.setting.rwxNetwork.shareStorageNetwork'), t('harvester.setting.rwxNetwork.dedicatedRwxNetwork')]"
|
||||||
|
@update:value="onUpdateNetworkType"
|
||||||
|
/>
|
||||||
|
<Banner
|
||||||
|
v-if="shareStorageNetwork"
|
||||||
|
class="mb-20"
|
||||||
|
color="warning"
|
||||||
|
>
|
||||||
|
<t
|
||||||
|
k="harvester.setting.rwxNetwork.shareStorageNetworkWarning"
|
||||||
|
:raw="true"
|
||||||
|
/>
|
||||||
|
</Banner>
|
||||||
|
<template v-if="showDedicatedNetworkConfig">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="networkType"
|
||||||
|
class="mb-20"
|
||||||
|
:options="networkTypes"
|
||||||
|
:mode="mode"
|
||||||
|
:label="t('harvester.fields.type')"
|
||||||
|
required
|
||||||
|
@update:value="onUpdateDedicatedType"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabeledInput
|
||||||
|
v-if="showVlan"
|
||||||
|
v-model:value.number="dedicatedNetwork.vlan"
|
||||||
|
type="number"
|
||||||
|
class="mb-20"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
placeholder="e.g. 1 - 4094"
|
||||||
|
label-key="harvester.setting.storageNetwork.vlan"
|
||||||
|
@update:value="inputVlan"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="dedicatedNetwork.clusterNetwork"
|
||||||
|
label-key="harvester.setting.storageNetwork.clusterNetwork"
|
||||||
|
class="mb-20"
|
||||||
|
required
|
||||||
|
:options="clusterNetworkOptions"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="dedicatedNetwork.range"
|
||||||
|
class="mb-5"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
:placeholder="t('harvester.setting.storageNetwork.range.placeholder')"
|
||||||
|
label-key="harvester.setting.storageNetwork.range.label"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ArrayList
|
||||||
|
v-model:value="exclude"
|
||||||
|
:show-header="true"
|
||||||
|
:default-add-value="defaultAddValue"
|
||||||
|
:mode="mode"
|
||||||
|
:add-label="t('harvester.setting.storageNetwork.exclude.addIp')"
|
||||||
|
class="mt-20"
|
||||||
|
@update:value="update"
|
||||||
|
>
|
||||||
|
<template #column-headers>
|
||||||
|
<div class="box mb-10">
|
||||||
|
<div class="key">
|
||||||
|
{{ t('harvester.setting.storageNetwork.exclude.label') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #columns="scope">
|
||||||
|
<div class="key">
|
||||||
|
<input
|
||||||
|
v-model="scope.row.value"
|
||||||
|
:placeholder="t('harvester.setting.storageNetwork.exclude.placeholder')"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ArrayList>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -64,6 +64,9 @@ export default {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
parsedDefaultValue = JSON.parse(this.value.value);
|
parsedDefaultValue = JSON.parse(this.value.value);
|
||||||
|
if (typeof parsedDefaultValue.exclusiveVlan !== 'boolean') {
|
||||||
|
parsedDefaultValue.exclusiveVlan = false;
|
||||||
|
}
|
||||||
networkType = 'vlan' in parsedDefaultValue ? L2VLAN : UNTAGGED; // backend doesn't provide networkType, so we check if vlan is provided instead
|
networkType = 'vlan' in parsedDefaultValue ? L2VLAN : UNTAGGED; // backend doesn't provide networkType, so we check if vlan is provided instead
|
||||||
openVlan = true;
|
openVlan = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -72,7 +75,8 @@ export default {
|
|||||||
vlan: '',
|
vlan: '',
|
||||||
clusterNetwork: '',
|
clusterNetwork: '',
|
||||||
range: '',
|
range: '',
|
||||||
exclude: []
|
exclude: [],
|
||||||
|
exclusiveVlan: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const exclude = parsedDefaultValue?.exclude?.toString().split(',') || [];
|
const exclude = parsedDefaultValue?.exclude?.toString().split(',') || [];
|
||||||
@@ -94,6 +98,10 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
showExclusiveVlan() {
|
||||||
|
return this.networkType === L2VLAN &&
|
||||||
|
Number(this.parsedDefaultValue.vlan) !== 1;
|
||||||
|
},
|
||||||
showVlan() {
|
showVlan() {
|
||||||
return this.networkType === L2VLAN;
|
return this.networkType === L2VLAN;
|
||||||
},
|
},
|
||||||
@@ -132,6 +140,18 @@ export default {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
exclusiveVlanOptions() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: this.t('generic.enabled'),
|
||||||
|
value: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.t('generic.disabled'),
|
||||||
|
value: false
|
||||||
|
}
|
||||||
|
];
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
@@ -174,7 +194,8 @@ export default {
|
|||||||
vlan: '',
|
vlan: '',
|
||||||
clusterNetwork: '',
|
clusterNetwork: '',
|
||||||
range: '',
|
range: '',
|
||||||
exclude: []
|
exclude: [],
|
||||||
|
exclusiveVlan: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -280,7 +301,15 @@ export default {
|
|||||||
label-key="harvester.setting.storageNetwork.vlan"
|
label-key="harvester.setting.storageNetwork.vlan"
|
||||||
@update:value="inputVlan"
|
@update:value="inputVlan"
|
||||||
/>
|
/>
|
||||||
|
<LabeledSelect
|
||||||
|
v-if="showExclusiveVlan"
|
||||||
|
v-model:value="parsedDefaultValue.exclusiveVlan"
|
||||||
|
class="mb-20"
|
||||||
|
:options="exclusiveVlanOptions"
|
||||||
|
:mode="mode"
|
||||||
|
label-key="harvester.setting.storageNetwork.exclusiveVlan"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
v-model:value="parsedDefaultValue.clusterNetwork"
|
v-model:value="parsedDefaultValue.clusterNetwork"
|
||||||
label-key="harvester.setting.storageNetwork.clusterNetwork"
|
label-key="harvester.setting.storageNetwork.clusterNetwork"
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import { NAMESPACE } from '@shell/config/types';
|
|||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
|
||||||
|
const SELECT_ALL = 'select_all';
|
||||||
|
const UNSELECT_ALL = 'unselect_all';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterBundleNamespaces',
|
name: 'HarvesterBundleNamespaces',
|
||||||
|
|
||||||
@@ -11,47 +14,92 @@ export default {
|
|||||||
mixins: [CreateEditView],
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
async fetch() {
|
async fetch() {
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
await this.$store.dispatch('harvester/findAll', { type: NAMESPACE });
|
await this.$store.dispatch('harvester/findAll', { type: NAMESPACE });
|
||||||
|
|
||||||
|
if (this.customSupportBundleFeatureEnabled) {
|
||||||
|
try {
|
||||||
|
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/namespaces?link=supportbundle');
|
||||||
|
const response = await this.$store.dispatch('harvester/request', { url });
|
||||||
|
|
||||||
|
this.defaultNamespaces = response.data || [];
|
||||||
|
} catch (error) {
|
||||||
|
this.defaultNamespaces = [];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.defaultNamespaces = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = false;
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
let namespaces = [];
|
|
||||||
const namespacesStr = this.value?.value || this.value?.default || '';
|
const namespacesStr = this.value?.value || this.value?.default || '';
|
||||||
|
const namespaces = namespacesStr ? namespacesStr.split(',') : [];
|
||||||
|
|
||||||
if (namespacesStr) {
|
return {
|
||||||
namespaces = namespacesStr.split(',');
|
namespaces,
|
||||||
}
|
defaultNamespaces: [],
|
||||||
|
loading: true
|
||||||
return { namespaces };
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
customSupportBundleFeatureEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('customSupportBundle');
|
||||||
|
},
|
||||||
|
|
||||||
|
allNamespaces() {
|
||||||
|
return this.$store.getters['harvester/all'](NAMESPACE).map((ns) => ns.id);
|
||||||
|
},
|
||||||
|
|
||||||
|
filteredNamespaces() {
|
||||||
|
if (!this.customSupportBundleFeatureEnabled) {
|
||||||
|
return this.allNamespaces;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultIds = this.defaultNamespaces.map((ns) => ns.id);
|
||||||
|
|
||||||
|
return this.allNamespaces.filter((ns) => !defaultIds.includes(ns));
|
||||||
|
},
|
||||||
|
|
||||||
namespaceOptions() {
|
namespaceOptions() {
|
||||||
return this.$store.getters['harvester/all'](NAMESPACE).map((N) => {
|
const mappedNamespaces = this.filteredNamespaces.map((ns) => ({ label: ns, value: ns }));
|
||||||
return {
|
|
||||||
label: N.id,
|
if (!this.customSupportBundleFeatureEnabled) {
|
||||||
value: N.id
|
return mappedNamespaces;
|
||||||
};
|
}
|
||||||
});
|
|
||||||
|
const allSelected =
|
||||||
|
this.namespaces.length === this.filteredNamespaces.length &&
|
||||||
|
this.filteredNamespaces.every((ns) => this.namespaces.includes(ns));
|
||||||
|
|
||||||
|
const controlOption = allSelected ? { label: this.t('harvester.modal.bundle.namespaces.unselectAll'), value: UNSELECT_ALL } : { label: this.t('harvester.modal.bundle.namespaces.selectAll'), value: SELECT_ALL };
|
||||||
|
|
||||||
|
return [controlOption, ...mappedNamespaces];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
update() {
|
update(selected) {
|
||||||
const namespaceStr = this.namespaces.join(',');
|
if (selected.includes(SELECT_ALL)) {
|
||||||
|
this.namespaces = [...this.filteredNamespaces];
|
||||||
|
} else if (selected.includes(UNSELECT_ALL)) {
|
||||||
|
this.namespaces = [];
|
||||||
|
} else {
|
||||||
|
this.namespaces = selected.filter((val) => val !== SELECT_ALL && val !== UNSELECT_ALL);
|
||||||
|
}
|
||||||
|
|
||||||
this.value['value'] = namespaceStr;
|
this.value.value = this.namespaces.join(',');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
'value.value': {
|
'value.value'(newVal) {
|
||||||
handler(neu) {
|
const raw = newVal || this.value.default || '';
|
||||||
if (neu === this.value.default || !neu) {
|
|
||||||
this.namespaces = [];
|
this.namespaces = raw ? raw.split(',') : [];
|
||||||
}
|
|
||||||
},
|
|
||||||
deep: true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -62,6 +110,7 @@ export default {
|
|||||||
<div class="col span-12">
|
<div class="col span-12">
|
||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
v-model:value="namespaces"
|
v-model:value="namespaces"
|
||||||
|
:loading="loading"
|
||||||
:multiple="true"
|
:multiple="true"
|
||||||
label-key="nameNsDescription.namespace.label"
|
label-key="nameNsDescription.namespace.label"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
<script>
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { NODE } from '@shell/config/types';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterUpgradeConfig',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
LabeledInput,
|
||||||
|
LabeledSelect,
|
||||||
|
RadioGroup
|
||||||
|
},
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const hash = { nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }) };
|
||||||
|
|
||||||
|
await allHash(hash);
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
let parseDefaultValue = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
parseDefaultValue = this.value.value ? JSON.parse(this.value.value) : JSON.parse(this.value.default);
|
||||||
|
} catch (error) {
|
||||||
|
parseDefaultValue = JSON.parse(this.value.default);
|
||||||
|
}
|
||||||
|
parseDefaultValue = this.normalizeValue(parseDefaultValue);
|
||||||
|
|
||||||
|
return {
|
||||||
|
parseDefaultValue,
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
strategyOptions() {
|
||||||
|
return [
|
||||||
|
{ value: 'sequential', label: 'sequential' },
|
||||||
|
{ value: 'skip', label: 'skip' },
|
||||||
|
{ value: 'parallel', label: 'parallel' }
|
||||||
|
];
|
||||||
|
},
|
||||||
|
nodeUpgradeOptions() {
|
||||||
|
return [
|
||||||
|
{ value: 'auto', label: 'auto' },
|
||||||
|
{ value: 'manual', label: 'manual' }
|
||||||
|
];
|
||||||
|
},
|
||||||
|
nodesOptions() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const nodes = this.$store.getters[`${ inStore }/all`](NODE);
|
||||||
|
|
||||||
|
return nodes.map((node) => ({ value: node.id, label: node.name }));
|
||||||
|
},
|
||||||
|
showPauseNodes() {
|
||||||
|
return this.parseDefaultValue.nodeUpgradeOption?.strategy?.mode === 'manual';
|
||||||
|
},
|
||||||
|
resumeUpgradePausedNodeEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('resumeUpgradePausedNode');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
normalizeValue(obj) {
|
||||||
|
// handle nodeUpgradeOption.strategy
|
||||||
|
if (obj?.nodeUpgradeOption?.strategy?.mode === 'auto') {
|
||||||
|
delete obj.nodeUpgradeOption.strategy.pauseNodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj?.nodeUpgradeOption?.strategy?.mode === 'manual') {
|
||||||
|
if (!Array.isArray(obj.nodeUpgradeOption.strategy.pauseNodes)) {
|
||||||
|
obj.nodeUpgradeOption.strategy.pauseNodes = this.nodesOptions.map((node) => node.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle imagePreloadOption.strategy
|
||||||
|
if (!obj.imagePreloadOption) {
|
||||||
|
obj.imagePreloadOption = { strategy: { type: 'sequential' } };
|
||||||
|
}
|
||||||
|
if (!obj.imagePreloadOption.strategy) {
|
||||||
|
obj.imagePreloadOption.strategy = { type: 'sequential' };
|
||||||
|
}
|
||||||
|
if (!obj.imagePreloadOption.strategy.type) {
|
||||||
|
obj.imagePreloadOption.strategy.type = 'sequential';
|
||||||
|
}
|
||||||
|
// Only set concurrency if type is 'parallel'
|
||||||
|
if (obj.imagePreloadOption.strategy.type === 'parallel') {
|
||||||
|
if (typeof obj.imagePreloadOption.strategy.concurrency !== 'number') {
|
||||||
|
obj.imagePreloadOption.strategy.concurrency = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
delete obj.imagePreloadOption.strategy.concurrency;
|
||||||
|
}
|
||||||
|
if (typeof obj.restoreVM !== 'boolean') {
|
||||||
|
obj.restoreVM = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
update() {
|
||||||
|
try {
|
||||||
|
// Clone to avoid mutating the form state
|
||||||
|
const valueToSave = JSON.parse(JSON.stringify(this.parseDefaultValue));
|
||||||
|
|
||||||
|
if (valueToSave.imagePreloadOption && valueToSave.imagePreloadOption.strategy) {
|
||||||
|
if (valueToSave.imagePreloadOption.strategy.type !== 'parallel') {
|
||||||
|
delete valueToSave.imagePreloadOption.strategy.concurrency;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.value['value'] = JSON.stringify(valueToSave, null, 2);
|
||||||
|
this.errors = [];
|
||||||
|
} catch (e) {
|
||||||
|
this.errors = ['Invalid JSON'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
value: {
|
||||||
|
handler(neu) {
|
||||||
|
let parseDefaultValue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
parseDefaultValue = JSON.parse(neu.value);
|
||||||
|
} catch (err) {
|
||||||
|
parseDefaultValue = JSON.parse(this.value.default);
|
||||||
|
}
|
||||||
|
parseDefaultValue = this.normalizeValue(parseDefaultValue);
|
||||||
|
this['parseDefaultValue'] = parseDefaultValue;
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="row"
|
||||||
|
@input="update"
|
||||||
|
>
|
||||||
|
<div class="col span-12">
|
||||||
|
<label class="mb-5"><b>{{ t('harvester.setting.upgrade.imagePreloadStrategy') }}</b></label>
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="parseDefaultValue.imagePreloadOption.strategy.type"
|
||||||
|
class="mb-20"
|
||||||
|
:mode="mode"
|
||||||
|
:label="t('harvester.setting.upgrade.strategyType')"
|
||||||
|
:options="strategyOptions"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
<LabeledInput
|
||||||
|
v-if="parseDefaultValue.imagePreloadOption.strategy.type === 'parallel'"
|
||||||
|
v-model:value.number="parseDefaultValue.imagePreloadOption.strategy.concurrency"
|
||||||
|
class="mb-20"
|
||||||
|
:mode="mode"
|
||||||
|
:label="t('harvester.setting.upgrade.concurrency')"
|
||||||
|
min="0"
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
<label class="mb-5"><b>{{ t('harvester.setting.upgrade.restoreVM') }}</b></label>
|
||||||
|
<RadioGroup
|
||||||
|
v-model:value="parseDefaultValue.restoreVM"
|
||||||
|
class="mb-20"
|
||||||
|
name="restoreVM"
|
||||||
|
:options="[true, false]"
|
||||||
|
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
<div v-if="resumeUpgradePausedNodeEnabled">
|
||||||
|
<label class="mb-5"><b>{{ t('harvester.setting.upgrade.nodeUpgradeOption') }}</b></label>
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="parseDefaultValue.nodeUpgradeOption.strategy.mode"
|
||||||
|
class="mb-20 label-select"
|
||||||
|
:mode="mode"
|
||||||
|
:label="t('harvester.setting.upgrade.strategy')"
|
||||||
|
:options="nodeUpgradeOptions"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
<LabeledSelect
|
||||||
|
v-if="showPauseNodes"
|
||||||
|
v-model:value="parseDefaultValue.nodeUpgradeOption.strategy.pauseNodes"
|
||||||
|
class="mb-20 label-select"
|
||||||
|
:clearable="true"
|
||||||
|
:multiple="true"
|
||||||
|
:mode="mode"
|
||||||
|
:label="t('harvester.setting.upgrade.pauseNodes')"
|
||||||
|
:options="nodesOptions"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="errors.length"
|
||||||
|
class="error"
|
||||||
|
>
|
||||||
|
{{ errors[0] }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.error {
|
||||||
|
color: #d9534f;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -59,7 +59,7 @@ export default {
|
|||||||
name="model"
|
name="model"
|
||||||
:options="[true,false]"
|
:options="[true,false]"
|
||||||
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||||
@input="update"
|
@update:value="update"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LabeledInput
|
<LabeledInput
|
||||||
@@ -69,6 +69,15 @@ export default {
|
|||||||
:mode="mode"
|
:mode="mode"
|
||||||
label-key="harvester.setting.vmForceDeletionPolicy.period"
|
label-key="harvester.setting.vmForceDeletionPolicy.period"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<LabeledInput
|
||||||
|
v-if="parseDefaultValue.enable"
|
||||||
|
v-model:value.number="parseDefaultValue.vmMigrationTimeout"
|
||||||
|
class="mb-20"
|
||||||
|
type="number"
|
||||||
|
:mode="mode"
|
||||||
|
label-key="harvester.setting.vmForceDeletionPolicy.vmMigrationTimeout"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,270 @@
|
|||||||
|
<script>
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import ArrayList from '@shell/components/form/ArrayList';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { isValidCIDR } from '@shell/utils/validators/cidr';
|
||||||
|
import { NODE } from '@shell/config/types';
|
||||||
|
import { _EDIT } from '@shell/config/query-params';
|
||||||
|
import { HCI } from '../../types';
|
||||||
|
|
||||||
|
const DEFAULT_NETWORK = {
|
||||||
|
clusterNetwork: '',
|
||||||
|
vlan: '',
|
||||||
|
range: '',
|
||||||
|
exclude: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'VMMigrationNetwork',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
LabeledInput,
|
||||||
|
LabeledSelect,
|
||||||
|
RadioGroup,
|
||||||
|
Banner,
|
||||||
|
ArrayList,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
registerBeforeHook: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: _EDIT,
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({ value: '' }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await allHash({
|
||||||
|
clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }),
|
||||||
|
vlanStatus: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VLAN_STATUS }),
|
||||||
|
nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.fetchError = null;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to fetch network data:', e); // eslint-disable-line no-console
|
||||||
|
this.fetchError = this.t('harvester.setting.vmMigrationNetwork.fetchError', { error: e.message || e }, true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const { parsed, enabled, parseError } = this.parseInitialValue();
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled,
|
||||||
|
network: { ...DEFAULT_NETWORK, ...parsed },
|
||||||
|
fetchError: null,
|
||||||
|
parseError,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.registerBeforeHook?.(this.willSave, 'willSave');
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
allErrors() {
|
||||||
|
return [this.fetchError, this.parseError].filter(Boolean);
|
||||||
|
},
|
||||||
|
|
||||||
|
clusterNetworkOptions() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const networks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
|
||||||
|
|
||||||
|
return networks.map((net) => ({
|
||||||
|
label: net.isReadyForStorageNetwork ? net.id : `${ net.id } (${ this.t('generic.notReady') })`,
|
||||||
|
value: net.id,
|
||||||
|
disabled: !net.isReadyForStorageNetwork,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
disableEdit() {
|
||||||
|
return !!(this.fetchError || this.parseError);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
parseInitialValue() {
|
||||||
|
let parsed = {};
|
||||||
|
let enabled = false;
|
||||||
|
let parseError = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (typeof this.value.value === 'string' && this.value.value.trim()) {
|
||||||
|
parsed = JSON.parse(this.value.value);
|
||||||
|
enabled = true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[VMMigrationNetwork] Failed to parse value:', e); // eslint-disable-line no-console
|
||||||
|
parseError = this.t('harvester.setting.vmMigrationNetwork.parseError', { error: e.message }, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(parsed.exclude)) {
|
||||||
|
parsed.exclude = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
parsed, enabled, parseError
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
clearErrors() {
|
||||||
|
this.fetchError = null;
|
||||||
|
this.parseError = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
inputVlan(val) {
|
||||||
|
this.network.vlan = val ? Math.min(4094, Math.max(1, Number(val))) : '';
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
useDefault() {
|
||||||
|
this.network = { ...DEFAULT_NETWORK };
|
||||||
|
this.value.value = '';
|
||||||
|
this.enabled = false;
|
||||||
|
this.clearErrors();
|
||||||
|
},
|
||||||
|
|
||||||
|
update() {
|
||||||
|
try {
|
||||||
|
this.value.value = this.enabled ? JSON.stringify({
|
||||||
|
...this.network,
|
||||||
|
exclude: (this.network.exclude || []).filter((e) => !!e?.trim()),
|
||||||
|
}) : '';
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to stringify network config:', e); // eslint-disable-line no-console
|
||||||
|
this.value.value = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
validateInputs() {
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
if (!this.network.clusterNetwork) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.setting.vmMigrationNetwork.clusterNetwork') }, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.network.range) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.setting.vmMigrationNetwork.range.label') }, true));
|
||||||
|
} else if (!isValidCIDR(this.network.range)) {
|
||||||
|
errors.push(this.t('harvester.setting.vmMigrationNetwork.range.invalid', null, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.network.vlan === '') {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.setting.vmMigrationNetwork.vlan') }, true));
|
||||||
|
} else {
|
||||||
|
const vlan = Number(this.network.vlan);
|
||||||
|
|
||||||
|
if (isNaN(vlan) || vlan < 1 || vlan > 4094) {
|
||||||
|
errors.push(this.t('validation.between', {
|
||||||
|
key: this.t('harvester.setting.vmMigrationNetwork.vlan'),
|
||||||
|
min: 1,
|
||||||
|
max: 4094,
|
||||||
|
}, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const cidr of this.network.exclude || []) {
|
||||||
|
if (cidr && !isValidCIDR(cidr)) {
|
||||||
|
errors.push(this.t('harvester.setting.storageNetwork.exclude.invalid', { value: cidr }, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
},
|
||||||
|
|
||||||
|
async willSave() {
|
||||||
|
if (!this.enabled) {
|
||||||
|
this.useDefault();
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.update();
|
||||||
|
|
||||||
|
const errors = this.validateInputs();
|
||||||
|
|
||||||
|
return errors.length ? Promise.reject(errors) : Promise.resolve();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Banner
|
||||||
|
v-for="(errorMsg, index) in allErrors"
|
||||||
|
:key="index"
|
||||||
|
color="error"
|
||||||
|
>
|
||||||
|
{{ errorMsg }}
|
||||||
|
</Banner>
|
||||||
|
<RadioGroup
|
||||||
|
v-model:value="enabled"
|
||||||
|
class="mb-20"
|
||||||
|
name="enableMigrationNetwork"
|
||||||
|
:options="[true, false]"
|
||||||
|
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
<template v-if="enabled">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="network.clusterNetwork"
|
||||||
|
required
|
||||||
|
label-key="harvester.setting.vmMigrationNetwork.clusterNetwork"
|
||||||
|
class="mb-20"
|
||||||
|
:mode="mode"
|
||||||
|
:options="clusterNetworkOptions"
|
||||||
|
:disabled="disableEdit"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value.number="network.vlan"
|
||||||
|
required
|
||||||
|
type="number"
|
||||||
|
class="mb-20"
|
||||||
|
:min="1"
|
||||||
|
:max="4094"
|
||||||
|
:mode="mode"
|
||||||
|
placeholder="e.g. 1 - 4094"
|
||||||
|
label-key="harvester.setting.vmMigrationNetwork.vlan"
|
||||||
|
:disabled="disableEdit"
|
||||||
|
@update:value="inputVlan"
|
||||||
|
/>
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="network.range"
|
||||||
|
required
|
||||||
|
class="mb-5"
|
||||||
|
:mode="mode"
|
||||||
|
:placeholder="t('harvester.setting.vmMigrationNetwork.range.placeholder')"
|
||||||
|
label-key="harvester.setting.vmMigrationNetwork.range.label"
|
||||||
|
:disabled="disableEdit"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
<ArrayList
|
||||||
|
v-model:value="network.exclude"
|
||||||
|
:show-header="true"
|
||||||
|
:default-add-value="''"
|
||||||
|
:mode="mode"
|
||||||
|
:add-disabled="disableEdit"
|
||||||
|
:add-label="t('harvester.setting.vmMigrationNetwork.exclude.addButton')"
|
||||||
|
:value-label="t('harvester.setting.vmMigrationNetwork.exclude.label')"
|
||||||
|
:value-placeholder="t('harvester.setting.storageNetwork.exclude.placeholder')"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,649 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch, toRefs } from 'vue';
|
||||||
|
import { useStore } from 'vuex';
|
||||||
|
import Loading from '@shell/components/Loading';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import { STORAGE_CLASS, NETWORK_ATTACHMENT } from '@shell/config/types';
|
||||||
|
import { useI18n } from '@shell/composables/useI18n';
|
||||||
|
import { randomStr } from '@shell/utils/string';
|
||||||
|
import { HCI } from '../../types';
|
||||||
|
import { VOLUME_MODE, ACCESS_MODE } from '../../config/types';
|
||||||
|
import { FORKLIFT_NAMESPACE } from '../../config/harvester-map';
|
||||||
|
import { buildNetworkMapEntries, buildStorageMapEntries } from '../../utils/forklift';
|
||||||
|
import { isInternalStorageClass } from '../../utils/storage-class';
|
||||||
|
import MappingColumn from './MappingColumn.vue';
|
||||||
|
import StorageDefaultsModal from './StorageDefaultsModal.vue';
|
||||||
|
|
||||||
|
const DEFAULT_VOLUME_MODE = VOLUME_MODE.FILE_SYSTEM;
|
||||||
|
const DEFAULT_ACCESS_MODES = [ACCESS_MODE.READ_WRITE_MANY];
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
providerName: { type: String, default: '' },
|
||||||
|
provider: { type: Object, default: null },
|
||||||
|
selectedVms: { type: Array, default: () => [] },
|
||||||
|
stepData: { type: Object, required: true },
|
||||||
|
useAllProviderData: { type: Boolean, default: false },
|
||||||
|
existingNetworkMap: { type: Object, default: null },
|
||||||
|
existingStorageMap: { type: Object, default: null },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['ready']);
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
const { t } = useI18n(store);
|
||||||
|
|
||||||
|
const vms = ref([]);
|
||||||
|
const harvesterNetworks = ref([]);
|
||||||
|
const storageClasses = ref([]);
|
||||||
|
const allNetworkMaps = ref([]);
|
||||||
|
const allStorageMaps = ref([]);
|
||||||
|
const errors = ref([]);
|
||||||
|
const loading = ref(true);
|
||||||
|
|
||||||
|
// Storage defaults edit modal state.
|
||||||
|
const showStorageDefaultsModal = ref(false);
|
||||||
|
const editingStorageEntry = ref(null);
|
||||||
|
|
||||||
|
const { networkEntries, storageEntries } = toRefs(props.stepData);
|
||||||
|
|
||||||
|
const NAMESPACE = FORKLIFT_NAMESPACE;
|
||||||
|
|
||||||
|
const harvesterNetworkOptions = computed(() => {
|
||||||
|
const options = [
|
||||||
|
{ label: t('harvester.addons.vmMigration.configureMappings.networkMapping.options.podNetworking'), value: 'pod' },
|
||||||
|
{ label: t('harvester.addons.vmMigration.configureMappings.networkMapping.options.ignored'), value: 'ignored' },
|
||||||
|
];
|
||||||
|
|
||||||
|
harvesterNetworks.value.forEach((net) => {
|
||||||
|
const ns = net.metadata?.namespace || '';
|
||||||
|
const name = net.metadata?.name || net.id;
|
||||||
|
const fullName = ns ? `${ ns }/${ name }` : name;
|
||||||
|
|
||||||
|
options.push({ label: fullName, value: fullName });
|
||||||
|
});
|
||||||
|
|
||||||
|
return options;
|
||||||
|
});
|
||||||
|
|
||||||
|
const storageClassOptions = computed(() => {
|
||||||
|
const options = storageClasses.value.filter((s) => !s.parameters?.backingImage).map((sc) => {
|
||||||
|
const value = sc.name;
|
||||||
|
let label = sc.isDefault ? `${ value } (${ t('generic.default') })` : value;
|
||||||
|
let disabled = false;
|
||||||
|
|
||||||
|
if (isInternalStorageClass(value)) {
|
||||||
|
label += ` (${ t('harvester.storage.internal.label') })`;
|
||||||
|
disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
disabled,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!options.find((o) => o.value === 'harvester-longhorn')) {
|
||||||
|
options.unshift({ label: 'harvester-longhorn', value: 'harvester-longhorn' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
});
|
||||||
|
|
||||||
|
const applyNetworkMapTargets = (mapSpec) => {
|
||||||
|
if (!mapSpec) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
networkEntries.value.forEach((entry) => {
|
||||||
|
const match = mapSpec.find(
|
||||||
|
(m) => m.source?.id === entry.id || m.source?.name === entry.name
|
||||||
|
);
|
||||||
|
|
||||||
|
if (match?.destination) {
|
||||||
|
const dest = match.destination;
|
||||||
|
|
||||||
|
if (dest.type === 'pod') {
|
||||||
|
entry.target = 'pod';
|
||||||
|
} else if (dest.type === 'ignored') {
|
||||||
|
entry.target = 'ignored';
|
||||||
|
} else if (dest.type === 'multus' && dest.name) {
|
||||||
|
entry.target = dest.namespace ? `${ dest.namespace }/${ dest.name }` : dest.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyStorageMapTargets = (mapSpec, { markOverridden = false, captureInherited = false } = {}) => {
|
||||||
|
if (!mapSpec) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
storageEntries.value.forEach((entry) => {
|
||||||
|
const match = mapSpec.find(
|
||||||
|
(m) => m.source?.id === entry.id || m.source?.name === entry.name
|
||||||
|
);
|
||||||
|
|
||||||
|
if (match?.destination?.storageClass) {
|
||||||
|
entry.target = match.destination.storageClass;
|
||||||
|
|
||||||
|
if (captureInherited) {
|
||||||
|
entry.inheritedFromProvider = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match.destination.volumeMode) {
|
||||||
|
entry.volumeMode = match.destination.volumeMode;
|
||||||
|
|
||||||
|
if (captureInherited) {
|
||||||
|
entry.inheritedVolumeMode = match.destination.volumeMode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match.destination.accessMode) {
|
||||||
|
entry.accessModes = [match.destination.accessMode];
|
||||||
|
|
||||||
|
if (captureInherited) {
|
||||||
|
entry.inheritedAccessModes = [match.destination.accessMode];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (markOverridden) {
|
||||||
|
entry.overridden = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyDefaultNetworkMap = () => {
|
||||||
|
const defaultMap = allNetworkMaps.value.find(
|
||||||
|
(nm) => nm.metadata.name === `${ props.providerName }-network-map-default`
|
||||||
|
);
|
||||||
|
|
||||||
|
applyNetworkMapTargets(defaultMap?.spec?.map);
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyDefaultStorageMap = () => {
|
||||||
|
const defaultMap = allStorageMaps.value.find(
|
||||||
|
(sm) => sm.metadata.name === `${ props.providerName }-storage-map-default`
|
||||||
|
);
|
||||||
|
|
||||||
|
applyStorageMapTargets(defaultMap?.spec?.map, { captureInherited: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
const allNetworksMapped = computed(() => networkEntries.value.length > 0 && networkEntries.value.every((e) => !!e.target));
|
||||||
|
const allStorageMapped = computed(() => storageEntries.value.length > 0 && storageEntries.value.every((e) => !!e.target));
|
||||||
|
|
||||||
|
// The "Inherited from provider" hint only appears in the migration plan wizard
|
||||||
|
// (where mappings inherit from the provider default), not on the provider creation page.
|
||||||
|
const inheritedProviderName = computed(() => (props.useAllProviderData ? '' : props.providerName));
|
||||||
|
|
||||||
|
const openStorageDefaults = (entry) => {
|
||||||
|
editingStorageEntry.value = entry;
|
||||||
|
showStorageDefaultsModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeStorageDefaults = () => {
|
||||||
|
showStorageDefaultsModal.value = false;
|
||||||
|
editingStorageEntry.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyStorageDefaults = ({ volumeMode, accessModes }) => {
|
||||||
|
if (editingStorageEntry.value) {
|
||||||
|
editingStorageEntry.value.volumeMode = volumeMode;
|
||||||
|
editingStorageEntry.value.accessModes = accessModes;
|
||||||
|
|
||||||
|
const inheritedVolumeMode = editingStorageEntry.value.inheritedVolumeMode;
|
||||||
|
const inheritedAccessMode = editingStorageEntry.value.inheritedAccessModes?.[0];
|
||||||
|
const selectedAccessMode = accessModes?.[0];
|
||||||
|
|
||||||
|
editingStorageEntry.value.overridden = volumeMode !== inheritedVolumeMode || selectedAccessMode !== inheritedAccessMode;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const canSave = computed(() => {
|
||||||
|
if (props.useAllProviderData) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return networkEntries.value.length > 0 && storageEntries.value.length > 0 &&
|
||||||
|
allNetworksMapped.value && allStorageMapped.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(canSave, (val) => {
|
||||||
|
emit('ready', val);
|
||||||
|
});
|
||||||
|
|
||||||
|
// After restore, check if ready
|
||||||
|
if (canSave.value) {
|
||||||
|
emit('ready', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildNetworkEntries = () => {
|
||||||
|
const networkMap = {};
|
||||||
|
|
||||||
|
vms.value.forEach((vm) => {
|
||||||
|
const vmName = vm.name || vm.id;
|
||||||
|
|
||||||
|
if (vm.networks && vm.networks.length > 0) {
|
||||||
|
vm.networks.forEach((net) => {
|
||||||
|
const netKey = net.id || net.name || 'default';
|
||||||
|
|
||||||
|
if (!networkMap[netKey]) {
|
||||||
|
networkMap[netKey] = {
|
||||||
|
name: net.name || t('harvester.addons.vmMigration.generic.unknown'),
|
||||||
|
id: net.id || '',
|
||||||
|
vlanId: net.vlanId || '',
|
||||||
|
target: '',
|
||||||
|
usedBy: [],
|
||||||
|
_key: `net-${ netKey }`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!networkMap[netKey].usedBy.includes(vmName)) {
|
||||||
|
networkMap[netKey].usedBy.push(vmName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
networkEntries.value = Object.values(networkMap);
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildStorageEntries = () => {
|
||||||
|
const datastoreMap = {};
|
||||||
|
|
||||||
|
vms.value.forEach((vm) => {
|
||||||
|
const vmName = vm.name || vm.id;
|
||||||
|
|
||||||
|
if (vm.disks && vm.disks.length > 0) {
|
||||||
|
vm.disks.forEach((disk) => {
|
||||||
|
const ds = disk.datastore;
|
||||||
|
|
||||||
|
if (ds && ds.id) {
|
||||||
|
if (!datastoreMap[ds.id]) {
|
||||||
|
datastoreMap[ds.id] = {
|
||||||
|
name: ds.name || t('harvester.addons.vmMigration.generic.unknown'),
|
||||||
|
id: ds.id,
|
||||||
|
type: ds.type || '',
|
||||||
|
capacity: 0,
|
||||||
|
target: '',
|
||||||
|
volumeMode: DEFAULT_VOLUME_MODE,
|
||||||
|
accessModes: [...DEFAULT_ACCESS_MODES],
|
||||||
|
inheritedVolumeMode: DEFAULT_VOLUME_MODE,
|
||||||
|
inheritedAccessModes: [...DEFAULT_ACCESS_MODES],
|
||||||
|
inheritedFromProvider: false,
|
||||||
|
overridden: false,
|
||||||
|
usedBy: [],
|
||||||
|
_key: `stor-${ ds.id }`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
datastoreMap[ds.id].capacity += disk.capacity || 0;
|
||||||
|
|
||||||
|
if (!datastoreMap[ds.id].usedBy.includes(vmName)) {
|
||||||
|
datastoreMap[ds.id].usedBy.push(vmName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
storageEntries.value = Object.values(datastoreMap);
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildNetworkEntriesFromProvider = (networksData) => {
|
||||||
|
networkEntries.value = (Array.isArray(networksData) ? networksData : []).map((net) => ({
|
||||||
|
name: net.name || net.id,
|
||||||
|
id: net.id || '',
|
||||||
|
vlanId: net.vlanId || '',
|
||||||
|
target: '',
|
||||||
|
usedBy: [],
|
||||||
|
_key: `net-${ net.id || net.name }`,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildStorageEntriesFromProvider = (datastoresData) => {
|
||||||
|
storageEntries.value = (Array.isArray(datastoresData) ? datastoresData : []).map((ds) => ({
|
||||||
|
name: ds.name || ds.id,
|
||||||
|
id: ds.id || '',
|
||||||
|
type: ds.type || '',
|
||||||
|
capacity: ds.capacity || 0,
|
||||||
|
target: '',
|
||||||
|
volumeMode: DEFAULT_VOLUME_MODE,
|
||||||
|
accessModes: [...DEFAULT_ACCESS_MODES],
|
||||||
|
inheritedVolumeMode: DEFAULT_VOLUME_MODE,
|
||||||
|
inheritedAccessModes: [...DEFAULT_ACCESS_MODES],
|
||||||
|
inheritedFromProvider: false,
|
||||||
|
overridden: false,
|
||||||
|
usedBy: [],
|
||||||
|
_key: `stor-${ ds.id || ds.name }`,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatStorageDetail = (entry) => {
|
||||||
|
const parts = [];
|
||||||
|
|
||||||
|
if (entry.type) {
|
||||||
|
parts.push(entry.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.capacity) {
|
||||||
|
const gb = entry.capacity / (1024 * 1024 * 1024);
|
||||||
|
|
||||||
|
if (gb >= 1024) {
|
||||||
|
parts.push(t('harvester.addons.vmMigration.generic.memoryTb', { value: (gb / 1024).toFixed(1) }));
|
||||||
|
} else {
|
||||||
|
parts.push(t('harvester.addons.vmMigration.generic.memoryGb', { value: Math.round(gb) }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join(' • ');
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildNetworkMapSpec = (providerRef) => ({
|
||||||
|
map: buildNetworkMapEntries(networkEntries.value, NAMESPACE),
|
||||||
|
provider: providerRef,
|
||||||
|
});
|
||||||
|
|
||||||
|
const buildStorageMapSpec = (providerRef) => ({
|
||||||
|
map: buildStorageMapEntries(storageEntries.value),
|
||||||
|
provider: providerRef,
|
||||||
|
});
|
||||||
|
|
||||||
|
const saveAndReturn = async() => {
|
||||||
|
const inStore = store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
const providerRef = {
|
||||||
|
source: {
|
||||||
|
apiVersion: 'forklift.konveyor.io/v1beta1',
|
||||||
|
kind: 'Provider',
|
||||||
|
name: props.providerName,
|
||||||
|
namespace: NAMESPACE,
|
||||||
|
},
|
||||||
|
destination: {
|
||||||
|
apiVersion: 'forklift.konveyor.io/v1beta1',
|
||||||
|
kind: 'Provider',
|
||||||
|
name: 'host',
|
||||||
|
namespace: NAMESPACE,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update existing maps in place (edit mode)
|
||||||
|
if (props.existingNetworkMap && props.existingStorageMap) {
|
||||||
|
props.existingNetworkMap.spec = buildNetworkMapSpec(providerRef);
|
||||||
|
await props.existingNetworkMap.save();
|
||||||
|
|
||||||
|
props.existingStorageMap.spec = buildStorageMapSpec(providerRef);
|
||||||
|
await props.existingStorageMap.save();
|
||||||
|
|
||||||
|
return {
|
||||||
|
networkMapName: props.existingNetworkMap.metadata.name,
|
||||||
|
storageMapName: props.existingStorageMap.metadata.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const providerOwnerRef = props.provider ? [{
|
||||||
|
apiVersion: 'forklift.konveyor.io/v1beta1',
|
||||||
|
kind: 'Provider',
|
||||||
|
name: props.provider.metadata.name,
|
||||||
|
uid: props.provider.metadata.uid,
|
||||||
|
blockOwnerDeletion: true,
|
||||||
|
}] : [];
|
||||||
|
|
||||||
|
const networkMap = await store.dispatch(`${ inStore }/create`, {
|
||||||
|
type: HCI.FORKLIFT_NETWORK_MAP,
|
||||||
|
metadata: {
|
||||||
|
name: `${ props.providerName }-network-map-${ props.useAllProviderData ? 'default' : randomStr(5).toLowerCase() }`,
|
||||||
|
namespace: NAMESPACE,
|
||||||
|
ownerReferences: providerOwnerRef,
|
||||||
|
},
|
||||||
|
spec: buildNetworkMapSpec(providerRef),
|
||||||
|
});
|
||||||
|
|
||||||
|
await networkMap.save();
|
||||||
|
|
||||||
|
const storageMap = await store.dispatch(`${ inStore }/create`, {
|
||||||
|
type: HCI.FORKLIFT_STORAGE_MAP,
|
||||||
|
metadata: {
|
||||||
|
name: `${ props.providerName }-storage-map-${ props.useAllProviderData ? 'default' : randomStr(5).toLowerCase() }`,
|
||||||
|
namespace: NAMESPACE,
|
||||||
|
ownerReferences: providerOwnerRef,
|
||||||
|
},
|
||||||
|
spec: buildStorageMapSpec(providerRef),
|
||||||
|
});
|
||||||
|
|
||||||
|
await storageMap.save();
|
||||||
|
|
||||||
|
return { networkMapName: networkMap.metadata.name, storageMapName: storageMap.metadata.name };
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ saveMappings: saveAndReturn });
|
||||||
|
|
||||||
|
const init = async() => {
|
||||||
|
const inStore = store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
try {
|
||||||
|
harvesterNetworks.value = await store.dispatch(`${ inStore }/findAll`, { type: NETWORK_ATTACHMENT });
|
||||||
|
} catch (e) {
|
||||||
|
harvesterNetworks.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
storageClasses.value = await store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS });
|
||||||
|
} catch (e) {
|
||||||
|
storageClasses.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
allNetworkMaps.value = await store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_NETWORK_MAP });
|
||||||
|
} catch (e) {
|
||||||
|
allNetworkMaps.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
allStorageMaps.value = await store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_STORAGE_MAP });
|
||||||
|
} catch (e) {
|
||||||
|
allStorageMaps.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (networkEntries.value.length === 0 || storageEntries.value.length === 0) {
|
||||||
|
try {
|
||||||
|
const providerUid = props.provider?.metadata?.uid;
|
||||||
|
const providerType = props.provider?.spec?.type || 'vsphere';
|
||||||
|
const baseUrl = store.getters['harvester-common/getHarvesterClusterUrl'](
|
||||||
|
`v1/harvester/providers/${ providerType }/${ providerUid }`
|
||||||
|
);
|
||||||
|
|
||||||
|
const [networksData, datastoresData] = await Promise.all([
|
||||||
|
store.dispatch(`${ inStore }/request`, { url: `${ baseUrl }/networks` }).catch(() => []),
|
||||||
|
store.dispatch(`${ inStore }/request`, { url: `${ baseUrl }/datastores?detail=1` }).catch(() => []),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (props.useAllProviderData) {
|
||||||
|
if (networkEntries.value.length === 0) {
|
||||||
|
buildNetworkEntriesFromProvider(networksData);
|
||||||
|
}
|
||||||
|
if (storageEntries.value.length === 0) {
|
||||||
|
buildStorageEntriesFromProvider(datastoresData);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
vms.value = props.selectedVms;
|
||||||
|
|
||||||
|
const networkNameMap = (Array.isArray(networksData) ? networksData : []).reduce((map, n) => {
|
||||||
|
map[n.id] = n.name;
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}, {});
|
||||||
|
const datastoreInfoMap = (Array.isArray(datastoresData) ? datastoresData : []).reduce((map, d) => {
|
||||||
|
map[d.id] = { name: d.name, type: d.type || '' };
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
vms.value = vms.value.map((vm) => {
|
||||||
|
const resolved = { ...vm };
|
||||||
|
|
||||||
|
if (resolved.networks) {
|
||||||
|
resolved.networks = resolved.networks.map((n) => ({
|
||||||
|
...n,
|
||||||
|
name: n.name || networkNameMap[n.id] || n.id,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resolved.disks) {
|
||||||
|
resolved.disks = resolved.disks.map((d) => {
|
||||||
|
const dsInfo = d.datastore ? datastoreInfoMap[d.datastore.id] : null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...d,
|
||||||
|
datastore: d.datastore ? {
|
||||||
|
...d.datastore,
|
||||||
|
name: d.datastore.name || dsInfo?.name || d.datastore.id,
|
||||||
|
type: dsInfo?.type || '',
|
||||||
|
} : d.datastore,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolved;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (networkEntries.value.length === 0) {
|
||||||
|
buildNetworkEntries();
|
||||||
|
}
|
||||||
|
if (storageEntries.value.length === 0) {
|
||||||
|
buildStorageEntries();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
errors.value = [e?.message || t('harvester.addons.vmMigration.errors.failedResolveDetails')];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasExistingNetworkTargets = networkEntries.value.some((e) => !!e.target);
|
||||||
|
const hasExistingStorageTargets = storageEntries.value.some((e) => !!e.target);
|
||||||
|
|
||||||
|
if (!hasExistingNetworkTargets) {
|
||||||
|
if (props.existingNetworkMap?.spec?.map) {
|
||||||
|
applyNetworkMapTargets(props.existingNetworkMap.spec.map);
|
||||||
|
} else {
|
||||||
|
applyDefaultNetworkMap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasExistingStorageTargets) {
|
||||||
|
if (props.existingStorageMap?.spec?.map) {
|
||||||
|
applyStorageMapTargets(props.existingStorageMap.spec.map, { markOverridden: true });
|
||||||
|
} else {
|
||||||
|
applyDefaultStorageMap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
init();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Loading v-if="loading" />
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="configure-mappings"
|
||||||
|
>
|
||||||
|
<Banner
|
||||||
|
v-for="(err, i) in errors"
|
||||||
|
:key="i"
|
||||||
|
color="error"
|
||||||
|
:label="err"
|
||||||
|
/>
|
||||||
|
<p class="text-deemphasized line-height-20">
|
||||||
|
{{ t('harvester.addons.vmMigration.configureMappings.description') }}
|
||||||
|
</p>
|
||||||
|
<div class="mappings-columns">
|
||||||
|
<MappingColumn
|
||||||
|
:title="t('harvester.addons.vmMigration.configureMappings.networkMapping.title')"
|
||||||
|
:description="t('harvester.addons.vmMigration.configureMappings.networkMapping.description')"
|
||||||
|
:entries="networkEntries"
|
||||||
|
:options="harvesterNetworkOptions"
|
||||||
|
:placeholder="t('harvester.addons.vmMigration.configureMappings.networkMapping.placeholder')"
|
||||||
|
:show-used-by="!useAllProviderData"
|
||||||
|
:clearable="useAllProviderData"
|
||||||
|
>
|
||||||
|
<template #source-detail="{ entry }">
|
||||||
|
<span class="source-detail text-deemphasized">{{ t('harvester.addons.vmMigration.generic.vlan', { id: entry.vlanId || '0' }) }}</span>
|
||||||
|
</template>
|
||||||
|
</MappingColumn>
|
||||||
|
|
||||||
|
<MappingColumn
|
||||||
|
:title="t('harvester.addons.vmMigration.configureMappings.storageMapping.title')"
|
||||||
|
:description="t('harvester.addons.vmMigration.configureMappings.storageMapping.description')"
|
||||||
|
:entries="storageEntries"
|
||||||
|
:options="storageClassOptions"
|
||||||
|
:placeholder="t('harvester.addons.vmMigration.configureMappings.storageMapping.placeholder')"
|
||||||
|
:show-used-by="!useAllProviderData"
|
||||||
|
:clearable="useAllProviderData"
|
||||||
|
:show-volume-settings="true"
|
||||||
|
:inherited-provider-name="inheritedProviderName"
|
||||||
|
@edit-defaults="openStorageDefaults"
|
||||||
|
>
|
||||||
|
<template #source-detail="{ entry }">
|
||||||
|
<span
|
||||||
|
v-if="entry.type || entry.capacity"
|
||||||
|
class="source-detail text-deemphasized"
|
||||||
|
>{{ formatStorageDetail(entry) }}</span>
|
||||||
|
</template>
|
||||||
|
</MappingColumn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<StorageDefaultsModal
|
||||||
|
v-if="showStorageDefaultsModal && editingStorageEntry"
|
||||||
|
:storage-class-name="editingStorageEntry.target"
|
||||||
|
:provider-name="providerName"
|
||||||
|
:show-inherited="!!inheritedProviderName"
|
||||||
|
:volume-mode="editingStorageEntry.volumeMode"
|
||||||
|
:access-modes="editingStorageEntry.accessModes"
|
||||||
|
:inherited-volume-mode="editingStorageEntry.inheritedVolumeMode"
|
||||||
|
:inherited-access-modes="editingStorageEntry.inheritedAccessModes"
|
||||||
|
@apply="applyStorageDefaults"
|
||||||
|
@close="closeStorageDefaults"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.mappings-columns {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 32px;
|
||||||
|
|
||||||
|
@media only screen and (min-width: map-get($breakpoints, '--viewport-7')) {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (min-width: map-get($breakpoints, '--viewport-9')) {
|
||||||
|
gap: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (min-width: map-get($breakpoints, '--viewport-12')) {
|
||||||
|
gap: 64px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.configure-mappings {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
|
||||||
|
.line-height-20 {
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-detail {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,573 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch, toRefs } from 'vue';
|
||||||
|
import { useStore } from 'vuex';
|
||||||
|
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import Loading from '@shell/components/Loading';
|
||||||
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import { SECRET } from '@shell/config/types';
|
||||||
|
import { randomStr } from '@shell/utils/string';
|
||||||
|
import { useI18n } from '@shell/composables/useI18n';
|
||||||
|
import { HCI } from '../../types';
|
||||||
|
import { FORKLIFT_NAMESPACE } from '../../config/harvester-map';
|
||||||
|
import { decodeSecretValue } from '../../utils/forklift';
|
||||||
|
|
||||||
|
const CREATE_NEW = '__create_new__';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
stepData: { type: Object, required: true },
|
||||||
|
createOnly: { type: Boolean, default: false },
|
||||||
|
editMode: { type: Boolean, default: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['complete', 'ready', 'form-valid', 'testing']);
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
const { t } = useI18n(store);
|
||||||
|
|
||||||
|
const allProviders = ref([]);
|
||||||
|
const allSecrets = ref([]);
|
||||||
|
const errors = ref([]);
|
||||||
|
const testBtnRef = ref(null);
|
||||||
|
const loading = ref(true);
|
||||||
|
const testing = ref(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
selectedProvider,
|
||||||
|
providerName,
|
||||||
|
url,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
skipTlsVerify,
|
||||||
|
vddkInitImage,
|
||||||
|
testPassed,
|
||||||
|
testResult,
|
||||||
|
testError,
|
||||||
|
createdProvider,
|
||||||
|
createdSecret,
|
||||||
|
} = toRefs(props.stepData);
|
||||||
|
|
||||||
|
const isExistingProvider = computed(() => selectedProvider.value !== CREATE_NEW);
|
||||||
|
const isFormValid = computed(() => !!providerName.value && !!url.value && !!username.value && !!password.value);
|
||||||
|
|
||||||
|
const providerOptions = computed(() => {
|
||||||
|
const options = [
|
||||||
|
{ label: t('harvester.addons.vmMigration.configureProvider.createNew'), value: CREATE_NEW }
|
||||||
|
];
|
||||||
|
|
||||||
|
allProviders.value.forEach((p) => {
|
||||||
|
if (!p.spec?.url) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const secretRef = p.spec?.secret;
|
||||||
|
|
||||||
|
if (secretRef) {
|
||||||
|
const secret = allSecrets.value.find(
|
||||||
|
(s) => s.metadata.name === secretRef.name && s.metadata.namespace === secretRef.namespace
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!secret?.data?.user || !secret?.data?.password) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
options.push({
|
||||||
|
label: p.metadata.name,
|
||||||
|
value: p.metadata.name,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return options;
|
||||||
|
});
|
||||||
|
|
||||||
|
// When the provider selection changes, populate or clear the fields
|
||||||
|
watch(selectedProvider, (val) => {
|
||||||
|
testPassed.value = false;
|
||||||
|
testResult.value = null;
|
||||||
|
testError.value = null;
|
||||||
|
|
||||||
|
if (val === CREATE_NEW) {
|
||||||
|
providerName.value = '';
|
||||||
|
url.value = '';
|
||||||
|
username.value = '';
|
||||||
|
password.value = '';
|
||||||
|
skipTlsVerify.value = false;
|
||||||
|
vddkInitImage.value = '';
|
||||||
|
createdProvider.value = null;
|
||||||
|
createdSecret.value = null;
|
||||||
|
} else {
|
||||||
|
const provider = allProviders.value.find(
|
||||||
|
(p) => p.metadata.name === val && p.metadata.namespace === FORKLIFT_NAMESPACE
|
||||||
|
);
|
||||||
|
|
||||||
|
if (provider) {
|
||||||
|
providerName.value = provider.metadata.name;
|
||||||
|
url.value = provider.spec?.url || '';
|
||||||
|
vddkInitImage.value = provider.spec?.settings?.vddkInitImage || '';
|
||||||
|
|
||||||
|
const secretRef = provider.spec?.secret;
|
||||||
|
|
||||||
|
if (secretRef) {
|
||||||
|
const secret = allSecrets.value.find(
|
||||||
|
(s) => s.metadata.name === secretRef.name && s.metadata.namespace === secretRef.namespace
|
||||||
|
);
|
||||||
|
|
||||||
|
if (secret?.data) {
|
||||||
|
username.value = decodeSecretValue(secret.data.user);
|
||||||
|
password.value = decodeSecretValue(secret.data.password);
|
||||||
|
skipTlsVerify.value = decodeSecretValue(secret.data.insecureSkipVerify) === 'true';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createdProvider.value = provider;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset test state when any field changes (only for create new)
|
||||||
|
watch([providerName, url, username, password], () => {
|
||||||
|
if (!isExistingProvider.value) {
|
||||||
|
testPassed.value = false;
|
||||||
|
testResult.value = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Emit ready/complete when testPassed changes
|
||||||
|
watch(testPassed, (val) => {
|
||||||
|
emit('ready', val);
|
||||||
|
if (val) {
|
||||||
|
emit('complete', { providerName: providerName.value, provider: createdProvider.value });
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
// Emit form-valid when form validity changes
|
||||||
|
watch(isFormValid, (val) => {
|
||||||
|
emit('form-valid', val);
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
watch(testing, (val) => {
|
||||||
|
emit('testing', val);
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
const pollProviderReady = async(name) => {
|
||||||
|
const inStore = store.getters['currentProduct'].inStore;
|
||||||
|
const namespace = FORKLIFT_NAMESPACE;
|
||||||
|
const maxAttempts = 15;
|
||||||
|
let attempts = 0;
|
||||||
|
|
||||||
|
while (attempts < maxAttempts) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
|
attempts++;
|
||||||
|
|
||||||
|
const refreshed = await store.dispatch(`${ inStore }/find`, {
|
||||||
|
type: HCI.FORKLIFT_PROVIDER,
|
||||||
|
id: `${ namespace }/${ name }`,
|
||||||
|
opt: { force: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
const conditions = refreshed?.status?.conditions || [];
|
||||||
|
const connectionCondition = conditions.find((c) => c.type === 'ConnectionTestSucceeded');
|
||||||
|
const readyCondition = conditions.find((c) => c.type === 'Ready');
|
||||||
|
|
||||||
|
if (connectionCondition) {
|
||||||
|
if (connectionCondition.status === 'True') {
|
||||||
|
return { connected: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { connected: false, errorMsg: connectionCondition.message || t('harvester.addons.vmMigration.errors.connectionFailed') };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (readyCondition) {
|
||||||
|
if (readyCondition.status === 'True') {
|
||||||
|
return { connected: true };
|
||||||
|
} else if (readyCondition.status === 'False') {
|
||||||
|
return { connected: false, errorMsg: readyCondition.message || t('harvester.addons.vmMigration.errors.providerNotReady') };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { connected: false, errorMsg: '' };
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePollResult = ({ connected, errorMsg }, buttonCb) => {
|
||||||
|
if (connected) {
|
||||||
|
testPassed.value = true;
|
||||||
|
testResult.value = t('harvester.addons.vmMigration.configureProvider.testSuccess');
|
||||||
|
testing.value = false;
|
||||||
|
buttonCb(true);
|
||||||
|
} else {
|
||||||
|
testError.value = errorMsg || t('harvester.addons.vmMigration.configureProvider.testTimeout');
|
||||||
|
testing.value = false;
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanupCreatedResources = async() => {
|
||||||
|
if (createdProvider.value) {
|
||||||
|
try {
|
||||||
|
await createdProvider.value.remove();
|
||||||
|
} catch (e) {}
|
||||||
|
createdProvider.value = null;
|
||||||
|
createdSecret.value = null;
|
||||||
|
} else if (createdSecret.value) {
|
||||||
|
try {
|
||||||
|
await createdSecret.value.remove();
|
||||||
|
} catch (e) {}
|
||||||
|
createdSecret.value = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const testConnection = async(buttonCb) => {
|
||||||
|
testResult.value = null;
|
||||||
|
testError.value = null;
|
||||||
|
testing.value = true;
|
||||||
|
|
||||||
|
if (!providerName.value || !url.value || !username.value || !password.value) {
|
||||||
|
testError.value = t('harvester.addons.vmMigration.configureProvider.testMissingFields');
|
||||||
|
testing.value = false;
|
||||||
|
buttonCb(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inStore = store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
// Edit mode: update existing provider URL + secret, then poll
|
||||||
|
if (props.editMode && createdProvider.value) {
|
||||||
|
try {
|
||||||
|
createdProvider.value.spec.url = url.value;
|
||||||
|
|
||||||
|
const vddkImage = vddkInitImage.value?.trim();
|
||||||
|
|
||||||
|
if (vddkImage) {
|
||||||
|
createdProvider.value.spec.settings = {
|
||||||
|
...(createdProvider.value.spec.settings || {}),
|
||||||
|
vddkInitImage: vddkImage,
|
||||||
|
};
|
||||||
|
} else if (createdProvider.value.spec.settings) {
|
||||||
|
delete createdProvider.value.spec.settings.vddkInitImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
await createdProvider.value.save();
|
||||||
|
|
||||||
|
const secretRef = createdProvider.value.spec?.secret;
|
||||||
|
|
||||||
|
if (secretRef && createdSecret.value) {
|
||||||
|
createdSecret.value.data = {
|
||||||
|
user: btoa(username.value),
|
||||||
|
password: btoa(password.value),
|
||||||
|
insecureSkipVerify: btoa(String(skipTlsVerify.value)),
|
||||||
|
url: btoa(url.value),
|
||||||
|
};
|
||||||
|
await createdSecret.value.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePollResult(await pollProviderReady(providerName.value), buttonCb);
|
||||||
|
} catch (err) {
|
||||||
|
testError.value = err.message || t('harvester.addons.vmMigration.configureProvider.testFailed');
|
||||||
|
testing.value = false;
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For existing providers, just poll for Ready/ConnectionTestSucceeded status
|
||||||
|
if (isExistingProvider.value) {
|
||||||
|
try {
|
||||||
|
handlePollResult(await pollProviderReady(providerName.value), buttonCb);
|
||||||
|
} catch (err) {
|
||||||
|
testError.value = err.message || t('harvester.addons.vmMigration.configureProvider.testFailed');
|
||||||
|
testing.value = false;
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For new providers, create provider + secret then poll
|
||||||
|
try {
|
||||||
|
await cleanupCreatedResources();
|
||||||
|
|
||||||
|
const namespace = FORKLIFT_NAMESPACE;
|
||||||
|
const secretName = `${ providerName.value }-creds-${ randomStr(4).toLowerCase() }`;
|
||||||
|
|
||||||
|
const spec = {
|
||||||
|
type: 'vsphere',
|
||||||
|
url: url.value,
|
||||||
|
secret: {
|
||||||
|
name: secretName,
|
||||||
|
namespace,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const vddkImage = vddkInitImage.value?.trim();
|
||||||
|
|
||||||
|
if (vddkImage) {
|
||||||
|
spec.settings = { vddkInitImage: vddkImage };
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = await store.dispatch(`${ inStore }/create`, {
|
||||||
|
type: HCI.FORKLIFT_PROVIDER,
|
||||||
|
metadata: {
|
||||||
|
name: providerName.value,
|
||||||
|
namespace,
|
||||||
|
},
|
||||||
|
spec,
|
||||||
|
});
|
||||||
|
|
||||||
|
await provider.save();
|
||||||
|
createdProvider.value = provider;
|
||||||
|
|
||||||
|
const newSecret = await store.dispatch(`${ inStore }/create`, {
|
||||||
|
type: SECRET,
|
||||||
|
metadata: {
|
||||||
|
name: secretName,
|
||||||
|
namespace,
|
||||||
|
labels: { 'ui.forklift/created-for-resource-type': 'forklift.konveyor.io.provider' },
|
||||||
|
ownerReferences: [
|
||||||
|
{
|
||||||
|
apiVersion: 'forklift.konveyor.io/v1beta1',
|
||||||
|
kind: 'Provider',
|
||||||
|
name: provider.metadata.name,
|
||||||
|
uid: provider.metadata.uid,
|
||||||
|
blockOwnerDeletion: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
newSecret['_type'] = 'Opaque';
|
||||||
|
newSecret['data'] = {
|
||||||
|
user: btoa(username.value),
|
||||||
|
password: btoa(password.value),
|
||||||
|
insecureSkipVerify: btoa(String(skipTlsVerify.value)),
|
||||||
|
url: btoa(url.value),
|
||||||
|
};
|
||||||
|
|
||||||
|
await newSecret.save();
|
||||||
|
createdSecret.value = newSecret;
|
||||||
|
|
||||||
|
const result = await pollProviderReady(providerName.value);
|
||||||
|
|
||||||
|
if (!result.connected) {
|
||||||
|
await cleanupCreatedResources();
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePollResult(result, buttonCb);
|
||||||
|
} catch (err) {
|
||||||
|
await cleanupCreatedResources();
|
||||||
|
testError.value = err.message || t('harvester.addons.vmMigration.configureProvider.testFailed');
|
||||||
|
testing.value = false;
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const init = async() => {
|
||||||
|
const inStore = store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
try {
|
||||||
|
allProviders.value = await store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_PROVIDER });
|
||||||
|
|
||||||
|
allSecrets.value = await store.dispatch(`${ inStore }/findAll`, {
|
||||||
|
type: SECRET,
|
||||||
|
opt: { labelSelector: `ui.forklift/created-for-resource-type=${ HCI.FORKLIFT_PROVIDER }` }
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
errors.value = [e?.message || t('harvester.addons.vmMigration.errors.failedLoadProviders')];
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
init();
|
||||||
|
|
||||||
|
const clickTestButton = () => {
|
||||||
|
testBtnRef.value?.$el?.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ testConnection, clickTestButton });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Loading v-if="loading" />
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="configure-provider-step"
|
||||||
|
>
|
||||||
|
<p class="text-deemphasized line-height-20">
|
||||||
|
{{ t('harvester.addons.vmMigration.configureProvider.description') }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="configure-provider-step-content">
|
||||||
|
<h3 class="table-title m-0">
|
||||||
|
<b>{{ t('harvester.addons.vmMigration.configureProvider.connectionDetails') }}</b>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<Banner
|
||||||
|
class="requirements-banner m-0"
|
||||||
|
color="info"
|
||||||
|
>
|
||||||
|
<div class="requirements-banner-content">
|
||||||
|
<span class="requirements-banner-title">{{ t('harvester.addons.vmMigration.configureProvider.requirementsTitle') }}</span>
|
||||||
|
<br>
|
||||||
|
<span>{{ t('harvester.addons.vmMigration.configureProvider.requirementsText') }}</span>
|
||||||
|
</div>
|
||||||
|
</Banner>
|
||||||
|
|
||||||
|
<div class="configure-provider-step-form">
|
||||||
|
<div v-if="!createOnly && !editMode">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="selectedProvider"
|
||||||
|
required
|
||||||
|
:label="t('harvester.addons.vmMigration.configureProvider.providerSelect')"
|
||||||
|
:options="providerOptions"
|
||||||
|
:reduce="(opt) => opt.value"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="!isExistingProvider || editMode"
|
||||||
|
>
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="providerName"
|
||||||
|
:label="t('harvester.addons.vmMigration.configureProvider.name')"
|
||||||
|
:disabled="editMode"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="url"
|
||||||
|
:label="t('harvester.addons.vmMigration.configureProvider.urlLabel')"
|
||||||
|
:placeholder="t('harvester.addons.vmMigration.configureProvider.urlPlaceholder')"
|
||||||
|
:disabled="isExistingProvider && !editMode"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<p class="text-deemphasized mt-5">
|
||||||
|
{{ t('harvester.addons.vmMigration.configureProvider.urlHint') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="vddkInitImage"
|
||||||
|
:label="t('harvester.addons.vmMigration.configureProvider.vddkImageLabel')"
|
||||||
|
:placeholder="t('harvester.addons.vmMigration.configureProvider.vddkImagePlaceholder')"
|
||||||
|
:tooltip="t('harvester.addons.vmMigration.configureProvider.vddkImageTooltip')"
|
||||||
|
:disabled="isExistingProvider && !editMode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="username"
|
||||||
|
:label="t('harvester.addons.vmMigration.fields.username')"
|
||||||
|
:disabled="isExistingProvider && !editMode"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="password"
|
||||||
|
type="password"
|
||||||
|
:label="t('harvester.addons.vmMigration.fields.password')"
|
||||||
|
:disabled="isExistingProvider && !editMode"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Checkbox
|
||||||
|
v-model:value="skipTlsVerify"
|
||||||
|
:label="t('harvester.addons.vmMigration.configureProvider.skipSsl')"
|
||||||
|
:disabled="isExistingProvider && !editMode"
|
||||||
|
/>
|
||||||
|
<p class="text-deemphasized ml-20">
|
||||||
|
{{ t('harvester.addons.vmMigration.configureProvider.skipSslHint') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<AsyncButton
|
||||||
|
ref="testBtnRef"
|
||||||
|
mode="test"
|
||||||
|
:disabled="!isFormValid"
|
||||||
|
:action-label="t('harvester.addons.vmMigration.configureProvider.testConnection.action')"
|
||||||
|
:waiting-label="t('harvester.addons.vmMigration.configureProvider.testConnection.waiting')"
|
||||||
|
:success-label="t('harvester.addons.vmMigration.configureProvider.testConnection.success')"
|
||||||
|
:error-label="t('harvester.addons.vmMigration.configureProvider.testConnection.error')"
|
||||||
|
:action-color="'role-secondary'"
|
||||||
|
:waiting-color="'role-disabled'"
|
||||||
|
@click="testConnection"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Banner
|
||||||
|
v-if="testResult"
|
||||||
|
color="success"
|
||||||
|
>
|
||||||
|
{{ testResult }}
|
||||||
|
</Banner>
|
||||||
|
<Banner
|
||||||
|
v-if="testError"
|
||||||
|
color="error"
|
||||||
|
>
|
||||||
|
{{ testError }}
|
||||||
|
</Banner>
|
||||||
|
<Banner
|
||||||
|
v-for="(err, i) in errors"
|
||||||
|
:key="i"
|
||||||
|
color="error"
|
||||||
|
>
|
||||||
|
{{ err }}
|
||||||
|
</Banner>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.requirements-banner {
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
.requirements-banner-title {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.configure-provider-step {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
|
||||||
|
.line-height-20 {
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.configure-provider-step-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.configure-provider-step-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.checkbox-label) {
|
||||||
|
color: var(--body-text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useStore } from 'vuex';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import { RcItemCard } from '@components/RcItemCard';
|
||||||
|
import { useI18n } from '@shell/composables/useI18n';
|
||||||
|
import { VOLUME_MODE } from '../../config/types';
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
const { t } = useI18n(store);
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
title: { type: String, required: true },
|
||||||
|
description: { type: String, default: '' },
|
||||||
|
entries: { type: Array, default: () => [] },
|
||||||
|
options: { type: Array, default: () => [] },
|
||||||
|
placeholder: { type: String, default: '' },
|
||||||
|
showUsedBy: { type: Boolean, default: false },
|
||||||
|
clearable: { type: Boolean, default: false },
|
||||||
|
// Storage-specific: show the volume/access mode defaults row with an Edit action.
|
||||||
|
showVolumeSettings: { type: Boolean, default: false },
|
||||||
|
// When set, the defaults row shows an "Inherited from provider" hint (migration plan wizard only).
|
||||||
|
inheritedProviderName: { type: String, default: '' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['edit-defaults']);
|
||||||
|
|
||||||
|
// Only offer "Remove Map" for entries that already have a target selected;
|
||||||
|
// entries without a selection just show the regular options.
|
||||||
|
const optionsFor = (entry) => {
|
||||||
|
if (!props.clearable || !entry.target) {
|
||||||
|
return props.options;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: t('harvester.addons.vmMigration.configureMappings.removeMap'),
|
||||||
|
value: null,
|
||||||
|
kind: 'highlighted',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'divider',
|
||||||
|
disabled: true,
|
||||||
|
kind: 'divider',
|
||||||
|
},
|
||||||
|
...props.options,
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatModes = (entry) => {
|
||||||
|
const volumeMode = entry.volumeMode || VOLUME_MODE.FILE_SYSTEM;
|
||||||
|
const accessModes = (entry.accessModes || []).join(', ');
|
||||||
|
|
||||||
|
return t('harvester.addons.vmMigration.storageDefaults.summary', { volumeMode, accessModes });
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="mapping-column">
|
||||||
|
<div>
|
||||||
|
<h3 class="mapping-section-title">
|
||||||
|
{{ title }}
|
||||||
|
</h3>
|
||||||
|
<p
|
||||||
|
v-if="description"
|
||||||
|
class="text-deemphasized line-height-20"
|
||||||
|
>
|
||||||
|
{{ description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<RcItemCard
|
||||||
|
v-for="entry in entries"
|
||||||
|
:id="entry._key"
|
||||||
|
:key="entry._key"
|
||||||
|
:variant="'small'"
|
||||||
|
:header="{}"
|
||||||
|
class="bg-light-gray"
|
||||||
|
>
|
||||||
|
<template #item-card-content>
|
||||||
|
<div class="card-content-column">
|
||||||
|
<div class="card-content-row">
|
||||||
|
<div class="mapping-source">
|
||||||
|
<span class="source-name">{{ entry.name }}</span>
|
||||||
|
<slot
|
||||||
|
name="source-detail"
|
||||||
|
:entry="entry"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div :class="['mapping-arrow', entry.target ? 'text-success' : 'text-deemphasized']">
|
||||||
|
<i
|
||||||
|
class="icon icon-right-arrow-alt"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mapping-target">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="entry.target"
|
||||||
|
:options="optionsFor(entry)"
|
||||||
|
:placeholder="placeholder+'...'"
|
||||||
|
:searchable="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="showVolumeSettings && entry.target"
|
||||||
|
class="storage-defaults-row"
|
||||||
|
>
|
||||||
|
<div class="storage-defaults">
|
||||||
|
<div class="storage-defaults-info">
|
||||||
|
<span class="storage-defaults-summary">{{ formatModes(entry) }}</span>
|
||||||
|
<span
|
||||||
|
v-if="inheritedProviderName && entry.inheritedFromProvider && !entry.overridden"
|
||||||
|
class="text-deemphasized storage-defaults-inherited"
|
||||||
|
>
|
||||||
|
{{ t('harvester.addons.vmMigration.storageDefaults.inherited', { provider: inheritedProviderName }) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
role="button"
|
||||||
|
class="storage-defaults-edit"
|
||||||
|
@click.prevent="emit('edit-defaults', entry)"
|
||||||
|
>
|
||||||
|
{{ t('harvester.addons.vmMigration.storageDefaults.edit') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="showUsedBy && entry.usedBy && entry.usedBy.length">
|
||||||
|
<span class="used-by">
|
||||||
|
{{ t('harvester.addons.vmMigration.generic.usedBy') }} <b>{{ entry.usedBy.join(', ') }}</b>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</RcItemCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.mapping-column {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
:deep(.item-card-header) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapping-section-title {
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 5px 0;
|
||||||
|
line-height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content-column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapping-source {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 2;
|
||||||
|
min-width: 100px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
|
||||||
|
.source-name {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-detail {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.used-by {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapping-arrow {
|
||||||
|
font-size: 22px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapping-target {
|
||||||
|
flex: 3;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-height-20 {
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.storage-defaults-row {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.storage-defaults {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--body-bg);
|
||||||
|
|
||||||
|
.storage-defaults-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.storage-defaults-summary {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.storage-defaults-inherited {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.storage-defaults-edit {
|
||||||
|
flex-shrink: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-light-gray {
|
||||||
|
background-color: var(--category-active) !important;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,674 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch } from 'vue';
|
||||||
|
import { useStore } from 'vuex';
|
||||||
|
import Loading from '@shell/components/Loading';
|
||||||
|
import SortableTable from '@shell/components/SortableTable';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import { BadgeState } from '@components/BadgeState';
|
||||||
|
import { RcDropdown, RcDropdownItem, RcDropdownTrigger } from '@components/RcDropdown';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import { useI18n } from '@shell/composables/useI18n';
|
||||||
|
import { NAMESPACE } from '@shell/config/types';
|
||||||
|
import { HCI } from '../../types';
|
||||||
|
import { FORKLIFT_NAMESPACE } from '../../config/harvester-map';
|
||||||
|
import {
|
||||||
|
FORKLIFT_API_VERSION, buildNetworkMapEntries, buildStorageMapEntries, bytesToGB, mbToGB
|
||||||
|
} from '../../utils/forklift';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
providerName: { type: String, default: '' },
|
||||||
|
provider: { type: Object, default: null },
|
||||||
|
selectedVms: { type: Array, default: () => [] },
|
||||||
|
mappingEntries: { type: Object, default: null },
|
||||||
|
stepData: { type: Object, required: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['ready', 'edit-selection', 'edit-mappings', 'remove-vm']);
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
const { t } = useI18n(store);
|
||||||
|
|
||||||
|
const vms = computed(() => props.selectedVms);
|
||||||
|
const networkMappings = ref([]);
|
||||||
|
const storageMappings = ref([]);
|
||||||
|
const planName = ref('');
|
||||||
|
const targetNamespace = ref('');
|
||||||
|
const namespaceOptions = ref([]);
|
||||||
|
const existingProviderNames = ref([]);
|
||||||
|
const existingPlanNames = ref([]);
|
||||||
|
const errors = ref([]);
|
||||||
|
const loading = ref(true);
|
||||||
|
|
||||||
|
// Restore persisted state
|
||||||
|
planName.value = props.stepData.planName;
|
||||||
|
targetNamespace.value = props.stepData.targetNamespace || '';
|
||||||
|
|
||||||
|
const NS = FORKLIFT_NAMESPACE;
|
||||||
|
|
||||||
|
const nameConflictError = computed(() => {
|
||||||
|
const name = (planName.value || '').trim();
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingProviderNames.value.includes(name)) {
|
||||||
|
return t('harvester.addons.vmMigration.reviewMigration.planNameProviderConflict', { name });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingPlanNames.value.includes(name)) {
|
||||||
|
return t('harvester.addons.vmMigration.reviewMigration.planNamePlanConflict', { name });
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(planName, (val) => {
|
||||||
|
props.stepData.planName = val;
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
watch(targetNamespace, (val) => {
|
||||||
|
props.stepData.targetNamespace = val;
|
||||||
|
});
|
||||||
|
|
||||||
|
watch([planName, targetNamespace, nameConflictError], () => {
|
||||||
|
emit('ready', !!planName.value && !!targetNamespace.value && !nameConflictError.value);
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
const formatNetworkTarget = (target = '') => {
|
||||||
|
if (target === 'pod') {
|
||||||
|
return t('harvester.addons.vmMigration.configureMappings.networkMapping.options.podNetworking');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target === 'ignored') {
|
||||||
|
return t('harvester.addons.vmMigration.generic.ignored');
|
||||||
|
}
|
||||||
|
|
||||||
|
return target;
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalVCpu = computed(() => vms.value.reduce((sum, vm) => sum + (vm.cpuCount || vm.numCPU || 0), 0));
|
||||||
|
|
||||||
|
const totalMemoryGB = computed(() => {
|
||||||
|
const totalMB = vms.value.reduce((sum, vm) => sum + (vm.memoryMB || vm.memory || 0), 0);
|
||||||
|
|
||||||
|
return mbToGB(totalMB);
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalStorageGB = computed(() => {
|
||||||
|
let totalBytes = 0;
|
||||||
|
|
||||||
|
vms.value.forEach((vm) => {
|
||||||
|
if (vm.disks && vm.disks.length > 0) {
|
||||||
|
totalBytes += vm.disks.reduce((sum, d) => sum + (d.capacity || 0), 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return bytesToGB(totalBytes);
|
||||||
|
});
|
||||||
|
|
||||||
|
const vmRows = computed(() => {
|
||||||
|
return vms.value.map((vm) => {
|
||||||
|
const cpus = vm.cpuCount || vm.numCPU || 0;
|
||||||
|
const memMB = vm.memoryMB || vm.memory || 0;
|
||||||
|
const memGB = memMB ? t('harvester.addons.vmMigration.generic.memoryGb', { value: mbToGB(memMB) }) : '-';
|
||||||
|
|
||||||
|
let totalDiskBytes = 0;
|
||||||
|
|
||||||
|
if (vm.disks && vm.disks.length > 0) {
|
||||||
|
totalDiskBytes = vm.disks.reduce((sum, d) => sum + (d.capacity || 0), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const diskDisplay = totalDiskBytes ? t('harvester.addons.vmMigration.generic.memoryGb', { value: bytesToGB(totalDiskBytes) }) : '-';
|
||||||
|
const os = vm.guestName || vm.guestOS || vm.os || '-';
|
||||||
|
const rawPowerState = vm.powerState || vm.status?.phase || '-';
|
||||||
|
const powerState = rawPowerState.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/^./, (c) => c.toUpperCase());
|
||||||
|
const powerStateClass = powerState.toLowerCase().includes('on') || powerState.toLowerCase().includes('running') ? 'power-on' : 'power-off';
|
||||||
|
|
||||||
|
const vmNetworkMappings = networkMappings.value.filter((m) => m.usedBy && m.usedBy.includes(vm.name || vm.id));
|
||||||
|
const vmStorageMappings = storageMappings.value.filter((m) => m.usedBy && m.usedBy.includes(vm.name || vm.id));
|
||||||
|
|
||||||
|
const networkMappingSort = vmNetworkMappings.map((m) => `${ m.source } ${ formatNetworkTarget(m.target) }`).join(', ');
|
||||||
|
const storageMappingSort = vmStorageMappings.map((m) => `${ m.source } ${ m.target }`).join(', ');
|
||||||
|
|
||||||
|
return {
|
||||||
|
_key: vm.id || vm.vmId || vm.metadata?.name,
|
||||||
|
id: vm.id,
|
||||||
|
name: vm.name || vm.id,
|
||||||
|
identifier: vm.id || vm.vmId || vm.metadata?.name || '-',
|
||||||
|
os,
|
||||||
|
resources: t('harvester.addons.vmMigration.generic.vCpu', { count: cpus }),
|
||||||
|
resourcesSub: `${ memGB } • ${ diskDisplay }`,
|
||||||
|
powerState,
|
||||||
|
powerStateClass,
|
||||||
|
networkMappings: vmNetworkMappings,
|
||||||
|
storageMappings: vmStorageMappings,
|
||||||
|
networkMapping: networkMappingSort,
|
||||||
|
storageMapping: storageMappingSort,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const vmHeaders = [
|
||||||
|
{
|
||||||
|
name: 'vmName',
|
||||||
|
labelKey: 'harvester.addons.vmMigration.reviewMigration.columns.vmName',
|
||||||
|
value: 'name',
|
||||||
|
sort: ['name'],
|
||||||
|
subLabel: t('harvester.addons.vmMigration.generic.identifier'),
|
||||||
|
width: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'os',
|
||||||
|
labelKey: 'harvester.addons.vmMigration.reviewMigration.columns.os',
|
||||||
|
value: 'os',
|
||||||
|
sort: ['os'],
|
||||||
|
width: 140,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'resources',
|
||||||
|
labelKey: 'harvester.addons.vmMigration.reviewMigration.columns.resources',
|
||||||
|
value: 'resources',
|
||||||
|
sort: false,
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'powerState',
|
||||||
|
labelKey: 'harvester.addons.vmMigration.reviewMigration.columns.powerState',
|
||||||
|
value: 'powerState',
|
||||||
|
sort: ['powerState'],
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'networkMapping',
|
||||||
|
labelKey: 'harvester.addons.vmMigration.reviewMigration.columns.networkMapping',
|
||||||
|
value: 'networkMapping',
|
||||||
|
sort: ['networkMapping'],
|
||||||
|
subLabel: t('harvester.addons.vmMigration.reviewMigration.columns.sourceTarget'),
|
||||||
|
width: 220,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'storageMapping',
|
||||||
|
labelKey: 'harvester.addons.vmMigration.reviewMigration.columns.storageMapping',
|
||||||
|
value: 'storageMapping',
|
||||||
|
sort: ['storageMapping'],
|
||||||
|
subLabel: t('harvester.addons.vmMigration.reviewMigration.columns.sourceTarget'),
|
||||||
|
width: 220,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'actions',
|
||||||
|
label: ' ',
|
||||||
|
align: 'right',
|
||||||
|
sort: false,
|
||||||
|
width: 40,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const onEditSelection = () => emit('edit-selection');
|
||||||
|
const onEditMappings = () => emit('edit-mappings');
|
||||||
|
const onRemoveVm = (id) => emit('remove-vm', id);
|
||||||
|
|
||||||
|
const startMigrationAction = async() => {
|
||||||
|
const inStore = store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
const providerRef = {
|
||||||
|
source: {
|
||||||
|
apiVersion: FORKLIFT_API_VERSION,
|
||||||
|
kind: 'Provider',
|
||||||
|
name: props.providerName,
|
||||||
|
namespace: NS,
|
||||||
|
},
|
||||||
|
destination: {
|
||||||
|
apiVersion: FORKLIFT_API_VERSION,
|
||||||
|
kind: 'Provider',
|
||||||
|
name: 'host',
|
||||||
|
namespace: NS,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const networkMapName = `${ planName.value }-network-map`;
|
||||||
|
const storageMapName = `${ planName.value }-storage-map`;
|
||||||
|
|
||||||
|
const networkMap = await store.dispatch(`${ inStore }/create`, {
|
||||||
|
type: HCI.FORKLIFT_NETWORK_MAP,
|
||||||
|
metadata: { name: networkMapName, namespace: NS },
|
||||||
|
spec: { map: buildNetworkMapEntries(props.mappingEntries?.networkEntries || [], NS), provider: providerRef },
|
||||||
|
});
|
||||||
|
|
||||||
|
await networkMap.save();
|
||||||
|
|
||||||
|
const storageMap = await store.dispatch(`${ inStore }/create`, {
|
||||||
|
type: HCI.FORKLIFT_STORAGE_MAP,
|
||||||
|
metadata: { name: storageMapName, namespace: NS },
|
||||||
|
spec: { map: buildStorageMapEntries(props.mappingEntries?.storageEntries || []), provider: providerRef },
|
||||||
|
});
|
||||||
|
|
||||||
|
await storageMap.save();
|
||||||
|
|
||||||
|
const plan = await store.dispatch(`${ inStore }/create`, {
|
||||||
|
type: HCI.FORKLIFT_PLAN,
|
||||||
|
metadata: { name: planName.value, namespace: NS },
|
||||||
|
spec: {
|
||||||
|
provider: providerRef,
|
||||||
|
map: {
|
||||||
|
network: {
|
||||||
|
apiVersion: FORKLIFT_API_VERSION,
|
||||||
|
kind: 'NetworkMap',
|
||||||
|
name: networkMapName,
|
||||||
|
namespace: NS,
|
||||||
|
},
|
||||||
|
storage: {
|
||||||
|
apiVersion: FORKLIFT_API_VERSION,
|
||||||
|
kind: 'StorageMap',
|
||||||
|
name: storageMapName,
|
||||||
|
namespace: NS,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
targetNamespace: targetNamespace.value,
|
||||||
|
vms: vms.value.map((vm) => ({ id: vm.id, name: vm.name || vm.id })),
|
||||||
|
warm: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await plan.save();
|
||||||
|
|
||||||
|
// Kick off the first migration through the model so the Migration payload
|
||||||
|
// lives in a single place (also reused by the dashboard start/restart action).
|
||||||
|
await plan.startMigration();
|
||||||
|
};
|
||||||
|
|
||||||
|
const init = async() => {
|
||||||
|
const inStore = store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const namespaces = await store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE });
|
||||||
|
|
||||||
|
namespaceOptions.value = namespaces.filter((ns) => !ns.isSystem).map((ns) => ns.metadata.name).sort().map((name) => ({ label: name, value: name }));
|
||||||
|
} catch (e) {
|
||||||
|
namespaceOptions.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_PROVIDER });
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
existingProviderNames.value = (store.getters[`${ inStore }/all`](HCI.FORKLIFT_PROVIDER) || [])
|
||||||
|
.map((p) => p.metadata?.name)
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await store.dispatch(`${ inStore }/findAll`, { type: HCI.FORKLIFT_PLAN });
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
existingPlanNames.value = (store.getters[`${ inStore }/all`](HCI.FORKLIFT_PLAN) || [])
|
||||||
|
.map((p) => p.metadata?.name)
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
if (props.mappingEntries) {
|
||||||
|
networkMappings.value = (props.mappingEntries.networkEntries || []).map((entry) => ({
|
||||||
|
source: entry.name || entry.id,
|
||||||
|
target: entry.target || '',
|
||||||
|
usedBy: entry.usedBy || [],
|
||||||
|
}));
|
||||||
|
|
||||||
|
storageMappings.value = (props.mappingEntries.storageEntries || []).map((entry) => ({
|
||||||
|
source: entry.name || entry.id,
|
||||||
|
target: entry.target || '',
|
||||||
|
usedBy: entry.usedBy || [],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
init();
|
||||||
|
|
||||||
|
defineExpose({ startMigration: startMigrationAction });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Loading v-if="loading" />
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="review-migration"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="review-migration-content"
|
||||||
|
>
|
||||||
|
<Banner
|
||||||
|
v-if="nameConflictError"
|
||||||
|
color="error"
|
||||||
|
:label="nameConflictError"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Migration Details Summary -->
|
||||||
|
<div class="migration-details">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3 class="section-title m-0">
|
||||||
|
{{ t('harvester.addons.vmMigration.reviewMigration.migrationDetails') }}
|
||||||
|
</h3>
|
||||||
|
<span class="section-description text-deemphasized">
|
||||||
|
{{ t('harvester.addons.vmMigration.reviewMigration.migrationDetailsDescription') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="name-row">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="planName"
|
||||||
|
class="name-input"
|
||||||
|
:label="t('harvester.addons.vmMigration.reviewMigration.planName')"
|
||||||
|
:placeholder="t('harvester.addons.vmMigration.reviewMigration.planNamePlaceholder')"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="targetNamespace"
|
||||||
|
class="namespace-select"
|
||||||
|
:label="t('harvester.addons.vmMigration.reviewMigration.targetNamespace')"
|
||||||
|
:placeholder="t('harvester.addons.vmMigration.reviewMigration.targetNamespacePlaceholder')"
|
||||||
|
:options="namespaceOptions"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="summary-row">
|
||||||
|
<div class="migration-mode-column">
|
||||||
|
<span class="detail-label">{{ t('harvester.addons.vmMigration.reviewMigration.migrationMode') }}</span>
|
||||||
|
<span class="cold-migration-badge">{{ t('harvester.addons.vmMigration.reviewMigration.coldMigration') }}</span>
|
||||||
|
<span class="text-deemphasized">{{ t('harvester.addons.vmMigration.reviewMigration.coldMigrationDescription') }}</span>
|
||||||
|
<span class="text-deemphasized">{{ t('harvester.addons.vmMigration.reviewMigration.coldMigrationHint') }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats-column">
|
||||||
|
<span class="detail-label">{{ t('harvester.addons.vmMigration.reviewMigration.provider') }}</span>
|
||||||
|
<span class="detail-value">{{ providerName }}</span>
|
||||||
|
<span class="detail-label">{{ t('harvester.addons.vmMigration.reviewMigration.totalVms') }}</span>
|
||||||
|
<span class="detail-value">{{ vms.length }}</span>
|
||||||
|
<span class="detail-label">{{ t('harvester.addons.vmMigration.reviewMigration.vcpu') }}</span>
|
||||||
|
<span class="detail-value">{{ totalVCpu }}</span>
|
||||||
|
<span class="detail-label">{{ t('harvester.addons.vmMigration.reviewMigration.memory') }}</span>
|
||||||
|
<span class="detail-value">{{ t('harvester.addons.vmMigration.generic.memoryGb', { value: totalMemoryGB }) }}</span>
|
||||||
|
<span class="detail-label">{{ t('harvester.addons.vmMigration.reviewMigration.storage') }}</span>
|
||||||
|
<span class="detail-value">{{ t('harvester.addons.vmMigration.generic.memoryGb', { value: totalStorageGB }) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Virtual Machines -->
|
||||||
|
<div class="vm-section">
|
||||||
|
<SortableTable
|
||||||
|
class="vm-sortable-table"
|
||||||
|
:rows="vmRows"
|
||||||
|
:headers="vmHeaders"
|
||||||
|
:search="false"
|
||||||
|
:table-actions="false"
|
||||||
|
:row-actions="false"
|
||||||
|
:groupable="false"
|
||||||
|
:paging="true"
|
||||||
|
key-field="_key"
|
||||||
|
>
|
||||||
|
<template #header-left>
|
||||||
|
<h4 class="section-title m-0">
|
||||||
|
{{ t('harvester.addons.vmMigration.reviewMigration.virtualMachines') }} ({{ vms.length }})
|
||||||
|
</h4>
|
||||||
|
</template>
|
||||||
|
<template #header-right>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="edit-selection-link"
|
||||||
|
@click.prevent="onEditSelection"
|
||||||
|
>
|
||||||
|
{{ t('harvester.addons.vmMigration.reviewMigration.editSelection') }}
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
<template #cell:vmName="{ row }">
|
||||||
|
<div class="vm-name-cell">
|
||||||
|
<span class="vm-name">{{ row.name }}</span>
|
||||||
|
<span class="vm-id text-deemphasized">{{ row.identifier }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #cell:resources="{ row }">
|
||||||
|
<span>{{ row.resources }}</span><br>
|
||||||
|
<span class="text-deemphasized">{{ row.resourcesSub }}</span>
|
||||||
|
</template>
|
||||||
|
<template #cell:powerState="{ row }">
|
||||||
|
<BadgeState
|
||||||
|
:label="row.powerState"
|
||||||
|
:color="row.powerStateClass === 'power-on' ? 'bg-warning' : 'bg-darker'"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #cell:networkMapping="{ row }">
|
||||||
|
<template v-if="row.networkMappings.length">
|
||||||
|
<div
|
||||||
|
v-for="(m, i) in row.networkMappings"
|
||||||
|
:key="'net-' + i"
|
||||||
|
class="mapping-cell"
|
||||||
|
>
|
||||||
|
<span class="mapping-source">{{ m.source }}</span>
|
||||||
|
<span class="mapping-target text-deemphasized">→ {{ formatNetworkTarget(m.target) }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="text-deemphasized"
|
||||||
|
>-</span>
|
||||||
|
</template>
|
||||||
|
<template #cell:storageMapping="{ row }">
|
||||||
|
<template v-if="row.storageMappings.length">
|
||||||
|
<div
|
||||||
|
v-for="(m, i) in row.storageMappings"
|
||||||
|
:key="'stor-' + i"
|
||||||
|
class="mapping-cell"
|
||||||
|
>
|
||||||
|
<span class="mapping-source">{{ m.source }}</span>
|
||||||
|
<span class="mapping-target text-deemphasized">→ {{ m.target }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="text-deemphasized"
|
||||||
|
>-</span>
|
||||||
|
</template>
|
||||||
|
<template #cell:actions="{ row }">
|
||||||
|
<rc-dropdown :aria-label="t('harvester.addons.vmMigration.reviewMigration.actions.menuLabel')">
|
||||||
|
<rc-dropdown-trigger
|
||||||
|
variant="ghost"
|
||||||
|
:aria-label="t('harvester.addons.vmMigration.reviewMigration.actions.menuLabel')"
|
||||||
|
>
|
||||||
|
<i class="icon icon-actions" />
|
||||||
|
</rc-dropdown-trigger>
|
||||||
|
<template #dropdownCollection>
|
||||||
|
<rc-dropdown-item @click="onEditMappings">
|
||||||
|
{{ t('harvester.addons.vmMigration.reviewMigration.actions.editMappings') }}
|
||||||
|
</rc-dropdown-item>
|
||||||
|
<rc-dropdown-item
|
||||||
|
class="text-error"
|
||||||
|
@click="onRemoveVm(row.id)"
|
||||||
|
>
|
||||||
|
{{ t('harvester.addons.vmMigration.reviewMigration.actions.removeFromPlan') }}
|
||||||
|
</rc-dropdown-item>
|
||||||
|
</template>
|
||||||
|
</rc-dropdown>
|
||||||
|
</template>
|
||||||
|
<template #no-rows>
|
||||||
|
<tr class="no-rows">
|
||||||
|
<td
|
||||||
|
:colspan="vmHeaders.length"
|
||||||
|
class="text-center"
|
||||||
|
>
|
||||||
|
{{ t('harvester.addons.vmMigration.reviewMigration.noVmSelected') }}
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="edit-selection-link"
|
||||||
|
@click.prevent="onEditSelection"
|
||||||
|
>{{ t('harvester.addons.vmMigration.reviewMigration.noVmSelectedLink') }}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</SortableTable>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error banner -->
|
||||||
|
<Banner
|
||||||
|
v-for="(err, i) in errors"
|
||||||
|
:key="i"
|
||||||
|
color="error"
|
||||||
|
:label="err"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.review-migration {
|
||||||
|
gap: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.review-migration-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px clamp(12px, 1vw, 96px);
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px clamp(12px, 1vw, 96px);
|
||||||
|
line-height: 20px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.section-description {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.migration-mode-column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
.cold-migration-badge {
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
opacity: 0.8;
|
||||||
|
font-weight: 600;
|
||||||
|
background: rgba(224, 191, 90, 0.16);
|
||||||
|
color: #E0BF5A;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
.theme-light & {
|
||||||
|
background: rgba(168, 128, 26, 0.16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-column {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto auto;
|
||||||
|
justify-content: start;
|
||||||
|
align-content: start;
|
||||||
|
column-gap: clamp(24px, 4vw, 64px);
|
||||||
|
row-gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-value {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-selection-link {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--link);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vm-sortable-table {
|
||||||
|
:deep(.fixed-header-actions) {
|
||||||
|
align-items: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(table) {
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(td) {
|
||||||
|
vertical-align: middle;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(th:first-child),
|
||||||
|
:deep(td:first-child) {
|
||||||
|
padding-left: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vm-name-cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.vm-name {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vm-id {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapping-cell {
|
||||||
|
.mapping-target {
|
||||||
|
margin-left: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapping-cell + .mapping-cell {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-title {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.migration-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vm-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,668 @@
|
|||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
ref, computed, watch, nextTick, onBeforeUnmount, toRefs
|
||||||
|
} from 'vue';
|
||||||
|
import { useStore } from 'vuex';
|
||||||
|
import Loading from '@shell/components/Loading';
|
||||||
|
import SortableTable from '@shell/components/SortableTable';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import { BadgeState } from '@components/BadgeState';
|
||||||
|
import { useI18n } from '@shell/composables/useI18n';
|
||||||
|
import { bytesToGB, mbToGB } from '../../utils/forklift';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
providerName: { type: String, default: '' },
|
||||||
|
provider: { type: Object, default: null },
|
||||||
|
stepData: { type: Object, required: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['complete', 'loading']);
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
const { t } = useI18n(store);
|
||||||
|
|
||||||
|
const { discoveredVMs, selectedVMIds, tableRows } = toRefs(props.stepData);
|
||||||
|
|
||||||
|
const selectedVMs = ref([]);
|
||||||
|
const loading = ref(true);
|
||||||
|
// Restored from stepData so friendly names survive step navigation / remount.
|
||||||
|
const networkMap = ref(props.stepData.networkMap || {});
|
||||||
|
const datastoreMap = ref(props.stepData.datastoreMap || {});
|
||||||
|
const sortableTableRef = ref(null);
|
||||||
|
const allVMsSelected = ref(false);
|
||||||
|
const errors = ref([]);
|
||||||
|
let skipNextSelectionEvent = false;
|
||||||
|
|
||||||
|
const lastFetchedAt = ref(null);
|
||||||
|
const now = ref(Date.now());
|
||||||
|
|
||||||
|
const formatElapsed = (ms) => {
|
||||||
|
const totalMinutes = Math.floor(ms / 60000);
|
||||||
|
const hours = Math.floor(totalMinutes / 60);
|
||||||
|
const minutes = totalMinutes % 60;
|
||||||
|
|
||||||
|
if (hours > 0 && minutes > 0) {
|
||||||
|
return t('harvester.addons.vmMigration.generic.elapsed.hoursAndMinutes', { hours, minutes });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hours > 0) {
|
||||||
|
return t('harvester.addons.vmMigration.generic.elapsed.hours', { hours });
|
||||||
|
}
|
||||||
|
|
||||||
|
return t('harvester.addons.vmMigration.generic.elapsed.minutes', { minutes: Math.max(1, minutes) });
|
||||||
|
};
|
||||||
|
|
||||||
|
const lastSyncedTime = computed(() => {
|
||||||
|
if (!lastFetchedAt.value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatElapsed(now.value - lastFetchedAt.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const nowTimer = setInterval(() => {
|
||||||
|
now.value = Date.now();
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
clearInterval(nowTimer);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Restore selection state from stepData
|
||||||
|
if (selectedVMIds.value.size > 0) {
|
||||||
|
selectedVMs.value = discoveredVMs.value.filter((vm) => selectedVMIds.value.has(vm.id));
|
||||||
|
allVMsSelected.value = selectedVMIds.value.size === discoveredVMs.value.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const vmCount = computed(() => discoveredVMs.value.length);
|
||||||
|
const selectedCount = computed(() => selectedVMs.value.length);
|
||||||
|
|
||||||
|
const showSelectAllBanner = computed(() => {
|
||||||
|
if (allVMsSelected.value) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pagedRows = sortableTableRef.value?.pagedRows || [];
|
||||||
|
|
||||||
|
if (pagedRows.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allPageSelected = pagedRows.every((row) => selectedVMIds.value.has(row._original?.id));
|
||||||
|
|
||||||
|
return allPageSelected && tableRows.value.length > pagedRows.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
const theadElement = computed(() => sortableTableRef.value?.$el?.querySelector('thead'));
|
||||||
|
|
||||||
|
const headers = ref([
|
||||||
|
{
|
||||||
|
name: 'vmName',
|
||||||
|
labelKey: 'harvester.addons.vmMigration.selectVms.columns.vmName',
|
||||||
|
value: 'vmName',
|
||||||
|
sort: ['vmName'],
|
||||||
|
subLabel: t('harvester.addons.vmMigration.generic.identifier'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'os',
|
||||||
|
labelKey: 'harvester.addons.vmMigration.selectVms.columns.os',
|
||||||
|
value: 'os',
|
||||||
|
sort: ['os'],
|
||||||
|
width: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'resources',
|
||||||
|
labelKey: 'harvester.addons.vmMigration.selectVms.columns.resources',
|
||||||
|
value: 'resources',
|
||||||
|
sort: false,
|
||||||
|
width: 140,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'powerState',
|
||||||
|
labelKey: 'harvester.addons.vmMigration.selectVms.columns.powerState',
|
||||||
|
value: 'powerState',
|
||||||
|
sort: ['powerState'],
|
||||||
|
width: 130,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'network',
|
||||||
|
labelKey: 'harvester.addons.vmMigration.selectVms.columns.network',
|
||||||
|
value: 'network',
|
||||||
|
sort: ['network'],
|
||||||
|
subLabel: t('harvester.addons.vmMigration.generic.identifier'),
|
||||||
|
width: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'datastore',
|
||||||
|
labelKey: 'harvester.addons.vmMigration.selectVms.columns.datastore',
|
||||||
|
value: 'datastore',
|
||||||
|
sort: ['datastore'],
|
||||||
|
subLabel: t('harvester.addons.vmMigration.generic.identifier'),
|
||||||
|
width: 200,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const buildTableRows = () => {
|
||||||
|
return discoveredVMs.value.map((vm) => {
|
||||||
|
const cpus = vm.cpuCount || vm.numCPU || '-';
|
||||||
|
const memMB = vm.memoryMB || vm.memory || 0;
|
||||||
|
const memGB = memMB ? t('harvester.addons.vmMigration.generic.memoryGb', { value: mbToGB(memMB) }) : '-';
|
||||||
|
|
||||||
|
let totalDiskBytes = 0;
|
||||||
|
|
||||||
|
if (vm.disks && vm.disks.length > 0) {
|
||||||
|
totalDiskBytes = vm.disks.reduce((sum, d) => sum + (d.capacity || 0), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const diskDisplay = totalDiskBytes ? t('harvester.addons.vmMigration.generic.memoryGb', { value: bytesToGB(totalDiskBytes) }) : '-';
|
||||||
|
const rawPowerState = vm.powerState || vm.status?.phase || '-';
|
||||||
|
const powerState = rawPowerState.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/^./, (c) => c.toUpperCase());
|
||||||
|
|
||||||
|
let networks = [];
|
||||||
|
|
||||||
|
if (vm.networks && vm.networks.length > 0) {
|
||||||
|
networks = vm.networks.map((n) => ({
|
||||||
|
name: n.name || networkMap.value[n.id] || n.id || n,
|
||||||
|
id: n.id || '',
|
||||||
|
vlanId: n.vlanId || '',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const datastores = [];
|
||||||
|
|
||||||
|
if (vm.disks && vm.disks.length > 0) {
|
||||||
|
const seen = new Set();
|
||||||
|
|
||||||
|
vm.disks.forEach((d) => {
|
||||||
|
const dsId = d.datastore?.id || d.datastore?.name;
|
||||||
|
|
||||||
|
if (dsId && !seen.has(dsId)) {
|
||||||
|
seen.add(dsId);
|
||||||
|
datastores.push({
|
||||||
|
name: d.datastore?.name || datastoreMap.value[dsId] || dsId,
|
||||||
|
id: d.datastore?.id || '',
|
||||||
|
type: d.datastore?.type || '',
|
||||||
|
capacity: d.capacity || 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
_original: vm,
|
||||||
|
_key: vm.id || vm.vmId || vm.metadata?.name,
|
||||||
|
selectedSort: selectedVMIds.value.has(vm.id) ? 0 : 1,
|
||||||
|
vmName: vm.name || vm.metadata?.name || '-',
|
||||||
|
vmId: vm.id || vm.vmId || vm.metadata?.name || '-',
|
||||||
|
os: vm.guestName || vm.guestOS || vm.os || '-',
|
||||||
|
resourcesDisplay: t('harvester.addons.vmMigration.generic.vCpu', { count: cpus }),
|
||||||
|
resourcesSub: `${ memGB } • ${ diskDisplay }`,
|
||||||
|
powerState,
|
||||||
|
powerStateClass: powerState.toLowerCase().includes('on') || powerState.toLowerCase().includes('running') ? 'power-on' : 'power-off',
|
||||||
|
networks,
|
||||||
|
network: networks.map((n) => n.name).join(', ') || '-',
|
||||||
|
datastores,
|
||||||
|
datastore: datastores.map((d) => d.name).join(', ') || '-',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSelect = (rows) => {
|
||||||
|
if (skipNextSelectionEvent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentPageRows = sortableTableRef.value?.pagedRows || [];
|
||||||
|
const selectedKeys = new Set(rows.map((r) => r._key));
|
||||||
|
|
||||||
|
currentPageRows.forEach((row) => {
|
||||||
|
const vmId = row._original?.id;
|
||||||
|
|
||||||
|
if (selectedKeys.has(row._key)) {
|
||||||
|
selectedVMIds.value.add(vmId);
|
||||||
|
} else {
|
||||||
|
selectedVMIds.value.delete(vmId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
allVMsSelected.value = selectedVMIds.value.size === discoveredVMs.value.length;
|
||||||
|
selectedVMs.value = discoveredVMs.value.filter((vm) => selectedVMIds.value.has(vm.id));
|
||||||
|
|
||||||
|
emit('complete', { selectedVMs: selectedVMs.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearSelection = () => {
|
||||||
|
allVMsSelected.value = false;
|
||||||
|
selectedVMIds.value = new Set();
|
||||||
|
selectedVMs.value = [];
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
const table = sortableTableRef.value;
|
||||||
|
|
||||||
|
if (table) {
|
||||||
|
table.clearSelection();
|
||||||
|
|
||||||
|
if (typeof table.setPage === 'function') {
|
||||||
|
table.setPage(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectAllVMs = () => {
|
||||||
|
allVMsSelected.value = true;
|
||||||
|
selectedVMIds.value = new Set(discoveredVMs.value.map((vm) => vm.id));
|
||||||
|
selectedVMs.value = discoveredVMs.value.slice();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Moves currently-selected VMs to the first page(s) and re-checks them.
|
||||||
|
const sortSelectedToFront = () => {
|
||||||
|
if (selectedVMIds.value.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tableRows.value.forEach((row) => {
|
||||||
|
row.selectedSort = selectedVMIds.value.has(row._original?.id) ? 0 : 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
headers.value = headers.value.map((h) => (
|
||||||
|
h.name === 'vmName' ? { ...h, sort: ['selectedSort', 'vmName'] } : h
|
||||||
|
));
|
||||||
|
|
||||||
|
skipNextSelectionEvent = true;
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
const table = sortableTableRef.value;
|
||||||
|
|
||||||
|
if (!table) {
|
||||||
|
skipNextSelectionEvent = false;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof table.changeSort === 'function') {
|
||||||
|
table.changeSort('vmName', false);
|
||||||
|
} else if (typeof table.setPage === 'function') {
|
||||||
|
table.setPage(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
const rowsToReselect = (table.pagedRows || []).filter((row) => selectedVMIds.value.has(row._original?.id));
|
||||||
|
|
||||||
|
if (rowsToReselect.length > 0) {
|
||||||
|
table.update(rowsToReselect, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
skipNextSelectionEvent = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => sortableTableRef.value?.page,
|
||||||
|
() => {
|
||||||
|
skipNextSelectionEvent = true;
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
const table = sortableTableRef.value;
|
||||||
|
|
||||||
|
if (!table) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowsToReselect = (table.pagedRows || []).filter((row) => selectedVMIds.value.has(row._original?.id));
|
||||||
|
|
||||||
|
if (rowsToReselect.length > 0) {
|
||||||
|
table.update(rowsToReselect, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
skipNextSelectionEvent = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const refreshing = ref(false);
|
||||||
|
|
||||||
|
const isLoading = computed(() => loading.value || refreshing.value);
|
||||||
|
|
||||||
|
watch(isLoading, (val) => {
|
||||||
|
emit('loading', val);
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
const fetchVMs = async() => {
|
||||||
|
if (!props.provider) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inStore = store.getters['currentProduct'].inStore;
|
||||||
|
const providerUid = props.provider.metadata.uid;
|
||||||
|
const providerType = props.provider.spec?.type || 'vsphere';
|
||||||
|
const baseUrl = store.getters['harvester-common/getHarvesterClusterUrl'](
|
||||||
|
`v1/harvester/providers/${ providerType }/${ providerUid }`
|
||||||
|
);
|
||||||
|
|
||||||
|
const [vmsResp, networksResp, datastoresResp] = await Promise.all([
|
||||||
|
store.dispatch(`${ inStore }/request`, { url: `${ baseUrl }/vms` }),
|
||||||
|
store.dispatch(`${ inStore }/request`, { url: `${ baseUrl }/networks` }).catch(() => []),
|
||||||
|
store.dispatch(`${ inStore }/request`, { url: `${ baseUrl }/datastores` }).catch(() => []),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (Array.isArray(vmsResp)) {
|
||||||
|
discoveredVMs.value = vmsResp;
|
||||||
|
} else if (vmsResp?.data && Array.isArray(vmsResp.data)) {
|
||||||
|
discoveredVMs.value = vmsResp.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const networks = Array.isArray(networksResp) ? networksResp : (networksResp?.data || []);
|
||||||
|
const datastores = Array.isArray(datastoresResp) ? datastoresResp : (datastoresResp?.data || []);
|
||||||
|
|
||||||
|
networkMap.value = networks.reduce((map, n) => {
|
||||||
|
map[n.id] = n.name;
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}, {});
|
||||||
|
datastoreMap.value = datastores.reduce((map, d) => {
|
||||||
|
map[d.id] = d.name;
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
props.stepData.networkMap = networkMap.value;
|
||||||
|
props.stepData.datastoreMap = datastoreMap.value;
|
||||||
|
|
||||||
|
lastFetchedAt.value = Date.now();
|
||||||
|
tableRows.value = buildTableRows();
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshVMs = async() => {
|
||||||
|
refreshing.value = true;
|
||||||
|
skipNextSelectionEvent = true;
|
||||||
|
|
||||||
|
const previousSelectedIds = new Set(selectedVMIds.value);
|
||||||
|
|
||||||
|
try {
|
||||||
|
errors.value = [];
|
||||||
|
await fetchVMs();
|
||||||
|
} catch (e) {
|
||||||
|
discoveredVMs.value = [];
|
||||||
|
tableRows.value = [];
|
||||||
|
errors.value = [e?.message || t('harvester.addons.vmMigration.errors.failedRefreshVms')];
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedVMIds.value = new Set(
|
||||||
|
discoveredVMs.value
|
||||||
|
.filter((vm) => previousSelectedIds.has(vm.id))
|
||||||
|
.map((vm) => vm.id)
|
||||||
|
);
|
||||||
|
selectedVMs.value = discoveredVMs.value.filter((vm) => selectedVMIds.value.has(vm.id));
|
||||||
|
allVMsSelected.value = selectedVMIds.value.size > 0 && selectedVMIds.value.size === discoveredVMs.value.length;
|
||||||
|
|
||||||
|
props.stepData.discoveredVMs = discoveredVMs.value;
|
||||||
|
props.stepData.selectedVMIds = selectedVMIds.value;
|
||||||
|
props.stepData.tableRows = tableRows.value;
|
||||||
|
emit('complete', { selectedVMs: selectedVMs.value });
|
||||||
|
refreshing.value = false;
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
const table = sortableTableRef.value;
|
||||||
|
|
||||||
|
if (table) {
|
||||||
|
const rowsToReselect = (table.pagedRows || []).filter((row) => selectedVMIds.value.has(row._original?.id));
|
||||||
|
|
||||||
|
if (rowsToReselect.length > 0) {
|
||||||
|
table.update(rowsToReselect, []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
skipNextSelectionEvent = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const init = async() => {
|
||||||
|
if (discoveredVMs.value.length > 0) {
|
||||||
|
if (!lastFetchedAt.value) {
|
||||||
|
lastFetchedAt.value = Date.now();
|
||||||
|
}
|
||||||
|
tableRows.value = buildTableRows();
|
||||||
|
loading.value = false;
|
||||||
|
|
||||||
|
// Returning to the step with an existing selection: bring selected VMs forward and re-check them.
|
||||||
|
await nextTick();
|
||||||
|
sortSelectedToFront();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetchVMs();
|
||||||
|
} catch (e) {
|
||||||
|
discoveredVMs.value = [];
|
||||||
|
errors.value = [e?.message || t('harvester.addons.vmMigration.errors.failedLoadVms')];
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Emit initial complete if VMs are already selected (restored from stepData)
|
||||||
|
if (selectedVMs.value.length > 0) {
|
||||||
|
emit('complete', { selectedVMs: selectedVMs.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Loading v-if="loading || refreshing" />
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="select-vms-step"
|
||||||
|
>
|
||||||
|
<Banner
|
||||||
|
v-for="(err, i) in errors"
|
||||||
|
:key="i"
|
||||||
|
color="error"
|
||||||
|
:label="err"
|
||||||
|
/>
|
||||||
|
<p class="text-deemphasized line-height-20">
|
||||||
|
{{ t('harvester.addons.vmMigration.selectVms.discovered', { count: vmCount }) }}
|
||||||
|
<router-link
|
||||||
|
v-if="provider"
|
||||||
|
class="provider-link"
|
||||||
|
:to="provider._detailLocation"
|
||||||
|
>
|
||||||
|
{{ providerName }}
|
||||||
|
</router-link>
|
||||||
|
<br>
|
||||||
|
<span
|
||||||
|
v-if="lastSyncedTime"
|
||||||
|
>
|
||||||
|
{{ t('harvester.addons.vmMigration.selectVms.lastSynced', { time: lastSyncedTime }) }}
|
||||||
|
<a
|
||||||
|
role="button"
|
||||||
|
class="text-bold"
|
||||||
|
:class="{ disabled: refreshing }"
|
||||||
|
@click.prevent="refreshVMs"
|
||||||
|
>
|
||||||
|
{{ t('harvester.addons.vmMigration.selectVms.refreshNow') }}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<!-- Discovered VMs table -->
|
||||||
|
<SortableTable
|
||||||
|
ref="sortableTableRef"
|
||||||
|
:rows="tableRows"
|
||||||
|
:headers="headers"
|
||||||
|
:search="true"
|
||||||
|
:table-actions="true"
|
||||||
|
:row-actions="false"
|
||||||
|
:groupable="false"
|
||||||
|
:paging="true"
|
||||||
|
key-field="_key"
|
||||||
|
@selection="onSelect"
|
||||||
|
>
|
||||||
|
<template #header-left>
|
||||||
|
<div class="vm-table-title">
|
||||||
|
<h3 class="m-0">
|
||||||
|
{{ t('harvester.addons.vmMigration.selectVms.availableVms') }}
|
||||||
|
</h3>
|
||||||
|
<span class="selected-actions">
|
||||||
|
<span class="text-deemphasized">
|
||||||
|
{{ selectedCount }} {{ t('harvester.addons.vmMigration.selectVms.selected') }}
|
||||||
|
</span>
|
||||||
|
<template v-if="selectedCount > 0">
|
||||||
|
<span class="text-deemphasized">|</span>
|
||||||
|
<a
|
||||||
|
role="button"
|
||||||
|
@click.prevent="clearSelection"
|
||||||
|
>
|
||||||
|
{{ t('harvester.addons.vmMigration.selectVms.clearAll') }}
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #cell:vmName="{ row }">
|
||||||
|
<div class="vm-name-cell">
|
||||||
|
<span class="vm-name">{{ row.vmName }}</span>
|
||||||
|
<span class="vm-id text-deemphasized">{{ row.vmId }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #cell:resources="{ row }">
|
||||||
|
<span>{{ row.resourcesDisplay }}</span><br>
|
||||||
|
<span class="text-deemphasized">{{ row.resourcesSub }}</span>
|
||||||
|
</template>
|
||||||
|
<template #cell:powerState="{ row }">
|
||||||
|
<BadgeState
|
||||||
|
:label="row.powerState"
|
||||||
|
:color="row.powerStateClass === 'power-on' ? 'bg-success' : 'bg-darker'"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #cell:network="{ row }">
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
v-for="(net, i) in row.networks"
|
||||||
|
:key="i"
|
||||||
|
:class="{ 'mt-4': i > 0 }"
|
||||||
|
>
|
||||||
|
<span>{{ net.name }}</span><br>
|
||||||
|
<span class="text-deemphasized">{{ net.id }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #cell:datastore="{ row }">
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
v-for="(ds, i) in row.datastores"
|
||||||
|
:key="i"
|
||||||
|
:class="{ 'mt-4': i > 0 }"
|
||||||
|
>
|
||||||
|
<span>{{ ds.name }}</span><br>
|
||||||
|
<span class="text-deemphasized">{{ ds.id }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</SortableTable>
|
||||||
|
|
||||||
|
<Teleport
|
||||||
|
v-if="showSelectAllBanner && theadElement"
|
||||||
|
:to="theadElement"
|
||||||
|
>
|
||||||
|
<tr class="select-all-banner-row">
|
||||||
|
<td
|
||||||
|
:colspan="headers.length + 1"
|
||||||
|
class="select-all-banner-cell"
|
||||||
|
>
|
||||||
|
<template v-if="allVMsSelected">
|
||||||
|
<span>{{ t('harvester.addons.vmMigration.selectVms.selectAllBanner.allSelected') }}</span>
|
||||||
|
<a
|
||||||
|
role="button"
|
||||||
|
@click.prevent="clearSelection"
|
||||||
|
>
|
||||||
|
{{ t('harvester.addons.vmMigration.selectVms.selectAllBanner.clearSelection') }}
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span>{{ t('harvester.addons.vmMigration.selectVms.selectAllBanner.pageOnly') }}</span>
|
||||||
|
<a
|
||||||
|
role="button"
|
||||||
|
@click.prevent="selectAllVMs"
|
||||||
|
>
|
||||||
|
{{ t('harvester.addons.vmMigration.selectVms.selectAllBanner.selectAll', { count: vmCount }) }}
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</Teleport>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.vm-table-title {
|
||||||
|
.text-deemphasized {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
color: var(--muted);
|
||||||
|
cursor: default;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vm-name-cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
.vm-name {
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vm-id {
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-vms-step {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
.line-height-20 {
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.select-all-banner-row) {
|
||||||
|
.select-all-banner-cell {
|
||||||
|
text-align: center;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background-color: var(--sortable-table-header-bg);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--primary);
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-left: 5px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useStore } from 'vuex';
|
||||||
|
import { Card } from '@components/Card';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import AppModal from '@shell/components/AppModal';
|
||||||
|
import { useI18n } from '@shell/composables/useI18n';
|
||||||
|
import { VOLUME_MODE, ACCESS_MODE } from '../../config/types';
|
||||||
|
|
||||||
|
const VOLUME_MODE_OPTIONS = [VOLUME_MODE.FILE_SYSTEM, VOLUME_MODE.BLOCK];
|
||||||
|
const ACCESS_MODE_OPTIONS = [ACCESS_MODE.READ_WRITE_ONCE, ACCESS_MODE.READ_WRITE_MANY, ACCESS_MODE.READ_ONLY_MANY];
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
storageClassName: { type: String, default: '' },
|
||||||
|
providerName: { type: String, default: '' },
|
||||||
|
showInherited: { type: Boolean, default: false },
|
||||||
|
volumeMode: { type: String, default: VOLUME_MODE.FILE_SYSTEM },
|
||||||
|
accessModes: { type: Array, default: () => [ACCESS_MODE.READ_WRITE_MANY] },
|
||||||
|
inheritedVolumeMode: { type: String, default: VOLUME_MODE.FILE_SYSTEM },
|
||||||
|
inheritedAccessModes: { type: Array, default: () => [ACCESS_MODE.READ_WRITE_MANY] },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['apply', 'close']);
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
const { t } = useI18n(store);
|
||||||
|
|
||||||
|
const localVolumeMode = ref(props.volumeMode || VOLUME_MODE.FILE_SYSTEM);
|
||||||
|
const localAccessMode = ref(props.accessModes?.[0] || ACCESS_MODE.READ_WRITE_MANY);
|
||||||
|
|
||||||
|
const volumeModeOptions = VOLUME_MODE_OPTIONS.map((value) => ({ label: value, value }));
|
||||||
|
const accessModeOptions = ACCESS_MODE_OPTIONS.map((value) => ({ label: value, value }));
|
||||||
|
|
||||||
|
const apply = () => {
|
||||||
|
emit('apply', {
|
||||||
|
volumeMode: localVolumeMode.value,
|
||||||
|
accessModes: [localAccessMode.value],
|
||||||
|
});
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Repopulate the dropdowns with the provider default values; the user then applies.
|
||||||
|
const reset = () => {
|
||||||
|
localVolumeMode.value = props.inheritedVolumeMode || VOLUME_MODE.FILE_SYSTEM;
|
||||||
|
localAccessMode.value = props.inheritedAccessModes?.[0] || ACCESS_MODE.READ_WRITE_MANY;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<app-modal
|
||||||
|
class="storage-defaults-modal"
|
||||||
|
name="storageDefaultsDialog"
|
||||||
|
:width="620"
|
||||||
|
height="auto"
|
||||||
|
:click-to-close="false"
|
||||||
|
@close="cancel"
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
class="storage-defaults-card"
|
||||||
|
:show-highlight-border="false"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
<h4 class="text-default-text">
|
||||||
|
{{ t('harvester.addons.vmMigration.storageDefaults.title', { name: storageClassName }) }}
|
||||||
|
</h4>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<p class="text-deemphasized description">
|
||||||
|
<template v-if="showInherited">
|
||||||
|
{{ t('harvester.addons.vmMigration.storageDefaults.descriptionInherited', { name: storageClassName, provider: providerName }) }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ t('harvester.addons.vmMigration.storageDefaults.description', { name: storageClassName }) }}
|
||||||
|
</template>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="settings-box">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="localVolumeMode"
|
||||||
|
class="volume-mode"
|
||||||
|
:label="t('harvester.addons.vmMigration.storageDefaults.volumeMode')"
|
||||||
|
:options="volumeModeOptions"
|
||||||
|
:searchable="false"
|
||||||
|
:clearable="false"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="localAccessMode"
|
||||||
|
class="access-mode"
|
||||||
|
:label="t('harvester.addons.vmMigration.storageDefaults.accessMode')"
|
||||||
|
:options="accessModeOptions"
|
||||||
|
:searchable="false"
|
||||||
|
:clearable="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #actions>
|
||||||
|
<div class="buttons">
|
||||||
|
<button
|
||||||
|
class="btn role-secondary"
|
||||||
|
@click="cancel"
|
||||||
|
>
|
||||||
|
{{ t('harvester.addons.vmMigration.storageDefaults.cancel') }}
|
||||||
|
</button>
|
||||||
|
<div class="right-buttons">
|
||||||
|
<button
|
||||||
|
v-if="showInherited"
|
||||||
|
class="btn role-secondary mr-10"
|
||||||
|
@click="reset"
|
||||||
|
>
|
||||||
|
{{ t('harvester.addons.vmMigration.storageDefaults.resetToProviderDefault') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn role-primary"
|
||||||
|
:disabled="!localAccessMode"
|
||||||
|
@click="apply"
|
||||||
|
>
|
||||||
|
{{ showInherited ? t('harvester.addons.vmMigration.storageDefaults.applyOverride') : t('harvester.addons.vmMigration.storageDefaults.apply') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</app-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.storage-defaults-modal {
|
||||||
|
z-index: 45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.storage-defaults-card {
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-box {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: var(--category-active);
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume-mode {
|
||||||
|
max-width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.access-mode {
|
||||||
|
max-width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.right-buttons {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
// suffix of doc link, see utils/feature-flags.js how to get complete doc link
|
// suffix of doc link, see utils/feature-flags.js how to get complete doc link
|
||||||
export const DOC = {
|
export const DOC = {
|
||||||
CONSOLE_URL: `/host/#remote-console`,
|
CONSOLE_URL: `/host/#remote-console`,
|
||||||
RANCHER_INTEGRATION_URL: `/rancher/rancher-integration`,
|
RANCHER_INTEGRATION_URL: `/rancher/rancher-integration`,
|
||||||
STORAGE_NETWORK_EXAMPLE: `/advanced/storagenetwork#configuration-example`,
|
KSMTUNED_MODE: `/host/#ksmtuned-mode`,
|
||||||
KSMTUNED_MODE: `/host/#ksmtuned-mode`,
|
UPGRADE_URL: `/upgrade/index`,
|
||||||
UPGRADE_URL: `/upgrade/index`
|
UPGRADE_CONFIG_URL: `/advanced/index#upgrade-config`,
|
||||||
|
STORAGE_NETWORK_EXAMPLE: `/advanced/storagenetwork#configuration-example`,
|
||||||
|
SUPPORT_BUNDLE_NAMESPACES: `/advanced/index/#support-bundle-namespaces`,
|
||||||
|
VPC_CONFIGURATION_EXAMPLES: `/networking/kubeovn-vpc#vpc-peering-configuration-examples`,
|
||||||
|
NETWORK_POLICY: `/networking/kubeovn-vm-isolation/#network-policies`,
|
||||||
|
TRANSPARENT_HUGEPAGES: `https://docs.kernel.org/admin-guide/mm/transhuge.html`,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,69 +1,96 @@
|
|||||||
// https://github.com/harvester/dashboard/releases/tag/v1.3.0
|
import semver from 'semver';
|
||||||
const featuresV130 = [
|
|
||||||
'supportHarvesterClusterVersion'
|
|
||||||
];
|
|
||||||
|
|
||||||
// https://github.com/harvester/dashboard/releases/tag/v1.3.1
|
const FEATURE_FLAGS = {
|
||||||
const featuresV131 = [
|
'v1.3.0': [
|
||||||
...featuresV130,
|
'supportHarvesterClusterVersion'
|
||||||
'autoRotateRke2CertsSetting',
|
],
|
||||||
'supportBundleNodeCollectionTimeoutSetting'
|
'v1.3.1': [
|
||||||
];
|
'autoRotateRke2CertsSetting',
|
||||||
|
'supportBundleNodeCollectionTimeoutSetting'
|
||||||
// https://github.com/harvester/dashboard/releases/tag/v1.3.2
|
],
|
||||||
const featuresV132 = [
|
'v1.3.2': [
|
||||||
...featuresV131,
|
'kubeconfigDefaultTokenTTLMinutesSetting',
|
||||||
'kubeconfigDefaultTokenTTLMinutesSetting',
|
'improveMaintenanceMode',
|
||||||
'improveMaintenanceMode',
|
],
|
||||||
];
|
'v1.3.3': [],
|
||||||
|
'v1.4.0': [
|
||||||
// TODO: add v1.3.3 official release note
|
'cpuPinning',
|
||||||
// https://github.com/harvester/dashboard/releases/tag/v1.3.3-dev-20250105
|
'usbPassthrough',
|
||||||
const featuresV133 = [
|
'volumeEncryption',
|
||||||
...featuresV132,
|
'schedulingVMBackup',
|
||||||
];
|
'vmSnapshotQuota',
|
||||||
|
'longhornV2LVMSupport',
|
||||||
// https://github.com/harvester/dashboard/releases/tag/v1.4.0
|
'improveMaintenanceMode',
|
||||||
const featuresV140 = [
|
'upgradeConfigSetting'
|
||||||
...featuresV133,
|
],
|
||||||
'cpuPinning',
|
'v1.4.1': [],
|
||||||
'usbPassthrough',
|
'v1.4.2': [
|
||||||
'volumeEncryption',
|
'refreshIntervalInSecond',
|
||||||
'schedulingVMBackup',
|
'allowEmptySnapshotClassName'
|
||||||
'vmSnapshotQuota',
|
],
|
||||||
'longhornV2LVMSupport',
|
'v1.4.3': [],
|
||||||
'improveMaintenanceMode',
|
'v1.5.0': [
|
||||||
];
|
'tpmPersistentState',
|
||||||
|
'efiPersistentState',
|
||||||
// https://github.com/harvester/dashboard/releases/tag/v1.4.1
|
'untaggedNetworkSetting',
|
||||||
const featuresV141 = [
|
'skipSingleReplicaDetachedVol',
|
||||||
...featuresV140
|
'thirdPartyStorage',
|
||||||
];
|
'liveMigrationProgress'
|
||||||
|
],
|
||||||
// TODO: add v1.4.2 official release note
|
'v1.5.1': [],
|
||||||
const featuresV142 = [
|
'v1.6.0': [
|
||||||
...featuresV141,
|
'customSupportBundle',
|
||||||
'refreshIntervalInSecond',
|
'csiOnlineExpandValidation',
|
||||||
'allowEmptySnapshotClassName'
|
'vmNetworkMigration',
|
||||||
];
|
'kubeovnVpcSubnet',
|
||||||
|
'rancherClusterSetting',
|
||||||
// TODO: add v1.5.0 official release note
|
'cpuMemoryHotplug',
|
||||||
const featuresV150 = [
|
'cdiSettings',
|
||||||
...featuresV142,
|
'vmCloneRunStrategy',
|
||||||
'tpmPersistentState',
|
],
|
||||||
'efiPersistentState',
|
'v1.6.1': [],
|
||||||
'untaggedNetworkSetting',
|
'v1.7.0': [
|
||||||
'skipSingleReplicaDetachedVol',
|
'vmMachineTypeAuto',
|
||||||
'thirdPartyStorage'
|
'lhV2VolExpansion',
|
||||||
];
|
'l2VlanTrunkMode',
|
||||||
|
'kubevirtMigration',
|
||||||
export const RELEASE_FEATURES = {
|
'hotplugNic',
|
||||||
'v1.3.0': featuresV130,
|
'resumeUpgradePausedNode',
|
||||||
'v1.3.1': featuresV131,
|
],
|
||||||
'v1.3.2': featuresV132,
|
'v1.7.1': [],
|
||||||
'v1.3.3': featuresV133,
|
'v1.8.0': [
|
||||||
'v1.4.0': featuresV140,
|
'hotplugCdRom',
|
||||||
'v1.4.1': featuresV141,
|
'supportBundleFileNameSetting',
|
||||||
'v1.4.2': featuresV142,
|
'clusterRegistrationTLSVerify',
|
||||||
'v1.5.0': featuresV150
|
'vGPUAsPCIDevice',
|
||||||
|
'instanceManagerResourcesSetting',
|
||||||
|
'rwxNetworkSetting',
|
||||||
|
'createPVCWithDataVolume',
|
||||||
|
'clusterPodSecurityStandardSetting',
|
||||||
|
],
|
||||||
|
'v1.8.1': [],
|
||||||
|
'v1.9.0': [
|
||||||
|
'supportFilesystem',
|
||||||
|
'disableResourcePooling',
|
||||||
|
'expandOnlineEncryptedVolume',
|
||||||
|
'longhornV2HugepageSettings',
|
||||||
|
'staticIPForVM',
|
||||||
|
'fsFreezeDeadline',
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const generateFeatureFlags = () => {
|
||||||
|
const versions = [...Object.keys(FEATURE_FLAGS)].filter((version) => semver.valid(version)).sort(semver.compare);
|
||||||
|
|
||||||
|
const generatedFlags = {};
|
||||||
|
|
||||||
|
versions.forEach((version, index) => {
|
||||||
|
const previousVersion = versions[index - 1];
|
||||||
|
|
||||||
|
generatedFlags[version] = previousVersion ? [...generatedFlags[previousVersion], ...FEATURE_FLAGS[version]] : [...FEATURE_FLAGS[version]];
|
||||||
|
});
|
||||||
|
|
||||||
|
return generatedFlags;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RELEASE_FEATURES = generateFeatureFlags();
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
LOGGING,
|
LOGGING,
|
||||||
STORAGE_CLASS,
|
STORAGE_CLASS,
|
||||||
SECRET,
|
SECRET,
|
||||||
|
NETWORK_POLICY
|
||||||
} from '@shell/config/types';
|
} from '@shell/config/types';
|
||||||
import { HCI, VOLUME_SNAPSHOT } from '../types';
|
import { HCI, VOLUME_SNAPSHOT } from '../types';
|
||||||
import {
|
import {
|
||||||
@@ -24,6 +25,7 @@ import {
|
|||||||
CONFIGURED_PROVIDERS,
|
CONFIGURED_PROVIDERS,
|
||||||
SUB_TYPE,
|
SUB_TYPE,
|
||||||
ADDRESS,
|
ADDRESS,
|
||||||
|
DESCRIPTION,
|
||||||
} from '@shell/config/table-headers';
|
} from '@shell/config/table-headers';
|
||||||
import { IF_HAVE } from '@shell/store/type-map';
|
import { IF_HAVE } from '@shell/store/type-map';
|
||||||
import {
|
import {
|
||||||
@@ -32,12 +34,41 @@ import {
|
|||||||
IMAGE_PROGRESS,
|
IMAGE_PROGRESS,
|
||||||
SNAPSHOT_TARGET_VOLUME,
|
SNAPSHOT_TARGET_VOLUME,
|
||||||
IMAGE_VIRTUAL_SIZE,
|
IMAGE_VIRTUAL_SIZE,
|
||||||
IMAGE_STORAGE_CLASS
|
IMAGE_STORAGE_CLASS,
|
||||||
|
HARVESTER_DESCRIPTION,
|
||||||
|
VM_IMPORT_SOURCE_VM,
|
||||||
|
VM_IMPORT_SOURCE_CLUSTER,
|
||||||
|
VM_IMPORT_STATUS,
|
||||||
|
VM_IMPORT_SOURCE_V_DC,
|
||||||
|
VM_IMPORT_SOURCE_V_ENDPOINT,
|
||||||
|
VM_IMPORT_SOURCE_V_STATUS,
|
||||||
|
VM_IMPORT_SOURCE_O_REGION,
|
||||||
|
VM_IMPORT_SOURCE_O_ENDPOINT,
|
||||||
|
VM_IMPORT_SOURCE_O_STATUS,
|
||||||
|
VM_IMPORT_SOURCE_OVA_URL,
|
||||||
|
VM_IMPORT_SOURCE_OVA_STATUS,
|
||||||
|
FORKLIFT_PROVIDER_TYPE,
|
||||||
|
FORKLIFT_PROVIDER_URL,
|
||||||
|
FORKLIFT_MAP_SOURCE_PROVIDER,
|
||||||
|
FORKLIFT_MAP_DEST_PROVIDER,
|
||||||
|
FORKLIFT_PLAN_TARGET_NS,
|
||||||
|
FORKLIFT_PLAN_VM_COUNT,
|
||||||
|
FORKLIFT_MIGRATION_PLAN,
|
||||||
} from './table-headers';
|
} from './table-headers';
|
||||||
|
import { ADD_ONS } from './harvester-map';
|
||||||
|
import { registerAddonSideNav } from '../utils/dynamic-nav';
|
||||||
|
|
||||||
const TEMPLATE = HCI.VM_VERSION;
|
const TEMPLATE = HCI.VM_VERSION;
|
||||||
const MONITORING_GROUP = 'Monitoring & Logging::Monitoring';
|
const MONITORING_GROUP = 'Monitoring & Logging::Monitoring';
|
||||||
const LOGGING_GROUP = 'Monitoring & Logging::Logging';
|
const LOGGING_GROUP = 'Monitoring & Logging::Logging';
|
||||||
|
const OVERLAY_NETWORKS_GROUP = 'Overlay Networks';
|
||||||
|
const UNDERLAY_NETWORKS_GROUP = 'Underlay Networks';
|
||||||
|
const NAT_INTERNET_GROUP = `${ OVERLAY_NETWORKS_GROUP }::NAT & Internet`;
|
||||||
|
const GATEWAYS_GROUP = `${ NAT_INTERNET_GROUP }::Gateways`;
|
||||||
|
const EXTERNAL_IPS_GROUP = `${ NAT_INTERNET_GROUP }::External IPs`;
|
||||||
|
const RULES_GROUP = `${ NAT_INTERNET_GROUP }::Rules`;
|
||||||
|
const SOURCE_RULES_GROUP = `${ RULES_GROUP }::Source Rules`;
|
||||||
|
const DESTINATION_RULES_GROUP = `${ RULES_GROUP }::Destination Rules`;
|
||||||
|
|
||||||
export const PRODUCT_NAME = 'harvester';
|
export const PRODUCT_NAME = 'harvester';
|
||||||
|
|
||||||
@@ -65,7 +96,6 @@ export function init($plugin, store) {
|
|||||||
configureType,
|
configureType,
|
||||||
virtualType,
|
virtualType,
|
||||||
weightGroup,
|
weightGroup,
|
||||||
weightType,
|
|
||||||
} = $plugin.DSL(store, PRODUCT_NAME);
|
} = $plugin.DSL(store, PRODUCT_NAME);
|
||||||
|
|
||||||
const isSingleVirtualCluster = process.env.rancherEnv === PRODUCT_NAME;
|
const isSingleVirtualCluster = process.env.rancherEnv === PRODUCT_NAME;
|
||||||
@@ -152,7 +182,7 @@ export function init($plugin, store) {
|
|||||||
group: 'Root',
|
group: 'Root',
|
||||||
name: HCI.HOST,
|
name: HCI.HOST,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
weight: 399,
|
weight: 499,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
params: { resource: HCI.HOST }
|
params: { resource: HCI.HOST }
|
||||||
@@ -184,7 +214,7 @@ export function init($plugin, store) {
|
|||||||
group: 'root',
|
group: 'root',
|
||||||
name: HCI.VM,
|
name: HCI.VM,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
weight: 299,
|
weight: 498,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
params: { resource: HCI.VM }
|
params: { resource: HCI.VM }
|
||||||
@@ -192,6 +222,146 @@ export function init($plugin, store) {
|
|||||||
exact: false
|
exact: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// VM Import Controller UI Flow
|
||||||
|
// ===========================================================================
|
||||||
|
// Define group (Hidden by default)
|
||||||
|
weightGroup('vmimport', 0, false);
|
||||||
|
|
||||||
|
// VirtualMachineImport
|
||||||
|
headers(HCI.VMIMPORT, [
|
||||||
|
STATE,
|
||||||
|
NAME_COL,
|
||||||
|
NAMESPACE_COL,
|
||||||
|
VM_IMPORT_SOURCE_VM,
|
||||||
|
VM_IMPORT_SOURCE_CLUSTER,
|
||||||
|
VM_IMPORT_STATUS,
|
||||||
|
AGE
|
||||||
|
]);
|
||||||
|
configureType(HCI.VMIMPORT, {
|
||||||
|
resource: HCI.VMIMPORT,
|
||||||
|
resourceDetail: HCI.VMIMPORT,
|
||||||
|
resourceEdit: HCI.VMIMPORT,
|
||||||
|
location: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.VMIMPORT }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
virtualType({ // needed to avoid 404 on refresh when combined with registerAddonSideNav()
|
||||||
|
name: HCI.VMIMPORT,
|
||||||
|
labelKey: 'harvester.addons.vmImport.labels.vmimport',
|
||||||
|
group: 'vmimport',
|
||||||
|
namespaced: true,
|
||||||
|
ifHaveType: HCI.VMIMPORT,
|
||||||
|
route: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.VMIMPORT }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Source: VMware
|
||||||
|
headers(HCI.VMIMPORT_SOURCE_V, [
|
||||||
|
STATE,
|
||||||
|
NAME_COL,
|
||||||
|
VM_IMPORT_SOURCE_V_ENDPOINT,
|
||||||
|
VM_IMPORT_SOURCE_V_DC,
|
||||||
|
VM_IMPORT_SOURCE_V_STATUS,
|
||||||
|
AGE
|
||||||
|
]);
|
||||||
|
configureType(HCI.VMIMPORT_SOURCE_V, {
|
||||||
|
resource: HCI.VMIMPORT_SOURCE_V,
|
||||||
|
resourceDetail: HCI.VMIMPORT_SOURCE_V,
|
||||||
|
resourceEdit: HCI.VMIMPORT_SOURCE_V,
|
||||||
|
location: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.VMIMPORT_SOURCE_V }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
virtualType({ // needed to avoid 404 on refresh when combined with registerAddonSideNav()
|
||||||
|
name: HCI.VMIMPORT_SOURCE_V,
|
||||||
|
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceVMWare',
|
||||||
|
group: 'vmimport',
|
||||||
|
namespaced: true,
|
||||||
|
ifHaveType: HCI.VMIMPORT_SOURCE_V,
|
||||||
|
route: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.VMIMPORT_SOURCE_V }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Source: OpenStack
|
||||||
|
headers(HCI.VMIMPORT_SOURCE_O, [
|
||||||
|
STATE,
|
||||||
|
NAME_COL,
|
||||||
|
VM_IMPORT_SOURCE_O_ENDPOINT,
|
||||||
|
VM_IMPORT_SOURCE_O_REGION,
|
||||||
|
VM_IMPORT_SOURCE_O_STATUS,
|
||||||
|
AGE
|
||||||
|
]);
|
||||||
|
configureType(HCI.VMIMPORT_SOURCE_O, {
|
||||||
|
resource: HCI.VMIMPORT_SOURCE_O,
|
||||||
|
resourceDetail: HCI.VMIMPORT_SOURCE_O,
|
||||||
|
resourceEdit: HCI.VMIMPORT_SOURCE_O,
|
||||||
|
location: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.VMIMPORT_SOURCE_O }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
virtualType({ // needed to avoid 404 on refresh when combined with registerAddonSideNav()
|
||||||
|
name: HCI.VMIMPORT_SOURCE_O,
|
||||||
|
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOpenStack',
|
||||||
|
group: 'vmimport',
|
||||||
|
namespaced: true,
|
||||||
|
ifHaveType: HCI.VMIMPORT_SOURCE_O,
|
||||||
|
route: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.VMIMPORT_SOURCE_O }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Source: OVA
|
||||||
|
headers(HCI.VMIMPORT_SOURCE_OVA, [
|
||||||
|
STATE,
|
||||||
|
NAME_COL,
|
||||||
|
VM_IMPORT_SOURCE_OVA_URL,
|
||||||
|
VM_IMPORT_SOURCE_OVA_STATUS,
|
||||||
|
AGE
|
||||||
|
]);
|
||||||
|
configureType(HCI.VMIMPORT_SOURCE_OVA, {
|
||||||
|
resource: HCI.VMIMPORT_SOURCE_OVA,
|
||||||
|
resourceDetail: HCI.VMIMPORT_SOURCE_OVA,
|
||||||
|
resourceEdit: HCI.VMIMPORT_SOURCE_OVA,
|
||||||
|
location: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.VMIMPORT_SOURCE_OVA }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
virtualType({ // needed to avoid 404 on refresh when combined with registerAddonSideNav()
|
||||||
|
name: HCI.VMIMPORT_SOURCE_OVA,
|
||||||
|
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOVA',
|
||||||
|
group: 'vmimport',
|
||||||
|
namespaced: true,
|
||||||
|
ifHaveType: HCI.VMIMPORT_SOURCE_OVA,
|
||||||
|
route: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.VMIMPORT_SOURCE_OVA }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enable SideNav based on Addon Status
|
||||||
|
registerAddonSideNav(store, PRODUCT_NAME, {
|
||||||
|
addonName: ADD_ONS.VM_IMPORT_CONTROLLER,
|
||||||
|
resourceType: HCI.ADD_ONS,
|
||||||
|
navGroup: 'vmimport',
|
||||||
|
types: [
|
||||||
|
HCI.VMIMPORT_SOURCE_V,
|
||||||
|
HCI.VMIMPORT_SOURCE_O,
|
||||||
|
HCI.VMIMPORT_SOURCE_OVA,
|
||||||
|
HCI.VMIMPORT
|
||||||
|
]
|
||||||
|
});
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
basicType([HCI.VOLUME]);
|
basicType([HCI.VOLUME]);
|
||||||
configureType(HCI.VOLUME, {
|
configureType(HCI.VOLUME, {
|
||||||
location: {
|
location: {
|
||||||
@@ -209,7 +379,7 @@ export function init($plugin, store) {
|
|||||||
ifHaveType: PVC,
|
ifHaveType: PVC,
|
||||||
name: HCI.VOLUME,
|
name: HCI.VOLUME,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
weight: 199,
|
weight: 497,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
params: { resource: HCI.VOLUME }
|
params: { resource: HCI.VOLUME }
|
||||||
@@ -222,6 +392,7 @@ export function init($plugin, store) {
|
|||||||
STATE,
|
STATE,
|
||||||
NAME_COL,
|
NAME_COL,
|
||||||
NAMESPACE_COL,
|
NAMESPACE_COL,
|
||||||
|
HARVESTER_DESCRIPTION,
|
||||||
IMAGE_STORAGE_CLASS,
|
IMAGE_STORAGE_CLASS,
|
||||||
IMAGE_PROGRESS,
|
IMAGE_PROGRESS,
|
||||||
IMAGE_DOWNLOAD_SIZE,
|
IMAGE_DOWNLOAD_SIZE,
|
||||||
@@ -234,7 +405,7 @@ export function init($plugin, store) {
|
|||||||
group: 'root',
|
group: 'root',
|
||||||
name: HCI.IMAGE,
|
name: HCI.IMAGE,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
weight: 198,
|
weight: 496,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
params: { resource: HCI.IMAGE }
|
params: { resource: HCI.IMAGE }
|
||||||
@@ -249,19 +420,19 @@ export function init($plugin, store) {
|
|||||||
group: 'root',
|
group: 'root',
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
name: 'projects-namespaces',
|
name: 'projects-namespaces',
|
||||||
weight: 98,
|
weight: 495,
|
||||||
route: { name: `${ PRODUCT_NAME }-c-cluster-projectsnamespaces` },
|
route: { name: `${ PRODUCT_NAME }-c-cluster-projectsnamespaces` },
|
||||||
exact: true,
|
exact: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isSingleVirtualCluster) {
|
if (isSingleVirtualCluster) {
|
||||||
headers(NAMESPACE, [STATE, NAME_UNLINKED, AGE]);
|
headers(NAMESPACE, [STATE, NAME_UNLINKED, DESCRIPTION, AGE]);
|
||||||
basicType([NAMESPACE]);
|
basicType([NAMESPACE]);
|
||||||
virtualType({
|
virtualType({
|
||||||
labelKey: 'harvester.namespace.label',
|
labelKey: 'harvester.namespace.label',
|
||||||
name: NAMESPACE,
|
name: NAMESPACE,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
weight: 89,
|
weight: 495,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
params: { resource: NAMESPACE }
|
params: { resource: NAMESPACE }
|
||||||
@@ -332,6 +503,7 @@ export function init($plugin, store) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
virtualType({
|
virtualType({
|
||||||
|
ifHaveType: LOGGING.CLUSTER_FLOW,
|
||||||
labelKey: 'harvester.logging.clusterFlow.label',
|
labelKey: 'harvester.logging.clusterFlow.label',
|
||||||
name: HCI.CLUSTER_FLOW,
|
name: HCI.CLUSTER_FLOW,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
@@ -355,6 +527,7 @@ export function init($plugin, store) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
virtualType({
|
virtualType({
|
||||||
|
ifHaveType: LOGGING.CLUSTER_OUTPUT,
|
||||||
labelKey: 'harvester.logging.clusterOutput.label',
|
labelKey: 'harvester.logging.clusterOutput.label',
|
||||||
name: HCI.CLUSTER_OUTPUT,
|
name: HCI.CLUSTER_OUTPUT,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
@@ -378,6 +551,7 @@ export function init($plugin, store) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
virtualType({
|
virtualType({
|
||||||
|
ifHaveType: LOGGING.FLOW,
|
||||||
labelKey: 'harvester.logging.flow.label',
|
labelKey: 'harvester.logging.flow.label',
|
||||||
name: HCI.FLOW,
|
name: HCI.FLOW,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
@@ -401,6 +575,7 @@ export function init($plugin, store) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
virtualType({
|
virtualType({
|
||||||
|
ifHaveType: LOGGING.OUTPUT,
|
||||||
labelKey: 'harvester.logging.output.label',
|
labelKey: 'harvester.logging.output.label',
|
||||||
name: HCI.OUTPUT,
|
name: HCI.OUTPUT,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
@@ -421,12 +596,53 @@ export function init($plugin, store) {
|
|||||||
[
|
[
|
||||||
HCI.CLUSTER_NETWORK,
|
HCI.CLUSTER_NETWORK,
|
||||||
HCI.NETWORK_ATTACHMENT,
|
HCI.NETWORK_ATTACHMENT,
|
||||||
|
HCI.HOST_NETWORK_CONFIG,
|
||||||
HCI.LB,
|
HCI.LB,
|
||||||
HCI.IP_POOL,
|
HCI.IP_POOL,
|
||||||
],
|
],
|
||||||
'networks'
|
'networks'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
basicType(
|
||||||
|
[HCI.VPC],
|
||||||
|
OVERLAY_NETWORKS_GROUP
|
||||||
|
);
|
||||||
|
|
||||||
|
basicType(
|
||||||
|
[NETWORK_POLICY],
|
||||||
|
OVERLAY_NETWORKS_GROUP
|
||||||
|
);
|
||||||
|
|
||||||
|
basicType(
|
||||||
|
[HCI.VPC_NAT_GATEWAY],
|
||||||
|
GATEWAYS_GROUP
|
||||||
|
);
|
||||||
|
|
||||||
|
basicType(
|
||||||
|
[HCI.IPTABLES_EIP],
|
||||||
|
EXTERNAL_IPS_GROUP
|
||||||
|
);
|
||||||
|
|
||||||
|
basicType(
|
||||||
|
[HCI.IPTABLES_SNAT_RULE],
|
||||||
|
SOURCE_RULES_GROUP
|
||||||
|
);
|
||||||
|
|
||||||
|
basicType(
|
||||||
|
[HCI.IPTABLES_DNAT_RULE],
|
||||||
|
DESTINATION_RULES_GROUP
|
||||||
|
);
|
||||||
|
|
||||||
|
basicType(
|
||||||
|
[HCI.PROVIDER_NETWORK],
|
||||||
|
UNDERLAY_NETWORKS_GROUP
|
||||||
|
);
|
||||||
|
|
||||||
|
basicType(
|
||||||
|
[HCI.VLAN],
|
||||||
|
UNDERLAY_NETWORKS_GROUP
|
||||||
|
);
|
||||||
|
|
||||||
basicType(
|
basicType(
|
||||||
[
|
[
|
||||||
HCI.SCHEDULE_VM_BACKUP,
|
HCI.SCHEDULE_VM_BACKUP,
|
||||||
@@ -437,9 +653,12 @@ export function init($plugin, store) {
|
|||||||
'backupAndSnapshot'
|
'backupAndSnapshot'
|
||||||
);
|
);
|
||||||
|
|
||||||
weightGroup('networks', 300, true);
|
weightGroup('networks', 494, true);
|
||||||
weightType(NAMESPACE, 299, true);
|
weightGroup('Overlay Networks', 493, true);
|
||||||
weightGroup('backupAndSnapshot', 289, true);
|
weightGroup('NAT & Internet', 492, true);
|
||||||
|
weightGroup('Rules', 491, true);
|
||||||
|
weightGroup('Underlay Networks', 490, true);
|
||||||
|
weightGroup('backupAndSnapshot', 489, true);
|
||||||
|
|
||||||
basicType(
|
basicType(
|
||||||
[
|
[
|
||||||
@@ -451,6 +670,7 @@ export function init($plugin, store) {
|
|||||||
HCI.PCI_DEVICE,
|
HCI.PCI_DEVICE,
|
||||||
HCI.SR_IOVGPU_DEVICE,
|
HCI.SR_IOVGPU_DEVICE,
|
||||||
HCI.VGPU_DEVICE,
|
HCI.VGPU_DEVICE,
|
||||||
|
HCI.MIG_CONFIGURATION,
|
||||||
HCI.USB_DEVICE,
|
HCI.USB_DEVICE,
|
||||||
HCI.ADD_ONS,
|
HCI.ADD_ONS,
|
||||||
HCI.SECRET,
|
HCI.SECRET,
|
||||||
@@ -517,7 +737,7 @@ export function init($plugin, store) {
|
|||||||
name: HCI.CLUSTER_NETWORK,
|
name: HCI.CLUSTER_NETWORK,
|
||||||
ifHaveType: HCI.CLUSTER_NETWORK,
|
ifHaveType: HCI.CLUSTER_NETWORK,
|
||||||
namespaced: false,
|
namespaced: false,
|
||||||
weight: 189,
|
weight: 484,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
params: { resource: HCI.CLUSTER_NETWORK }
|
params: { resource: HCI.CLUSTER_NETWORK }
|
||||||
@@ -532,14 +752,14 @@ export function init($plugin, store) {
|
|||||||
},
|
},
|
||||||
resource: NETWORK_ATTACHMENT,
|
resource: NETWORK_ATTACHMENT,
|
||||||
resourceDetail: HCI.NETWORK_ATTACHMENT,
|
resourceDetail: HCI.NETWORK_ATTACHMENT,
|
||||||
resourceEdit: HCI.NETWORK_ATTACHMENT
|
resourceEdit: HCI.NETWORK_ATTACHMENT,
|
||||||
});
|
});
|
||||||
|
|
||||||
virtualType({
|
virtualType({
|
||||||
labelKey: 'harvester.network.label',
|
labelKey: 'harvester.network.label',
|
||||||
name: HCI.NETWORK_ATTACHMENT,
|
name: HCI.NETWORK_ATTACHMENT,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
weight: 188,
|
weight: 485,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
params: { resource: HCI.NETWORK_ATTACHMENT }
|
params: { resource: HCI.NETWORK_ATTACHMENT }
|
||||||
@@ -547,6 +767,143 @@ export function init($plugin, store) {
|
|||||||
exact: false
|
exact: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
configureType(HCI.VPC, { hiddenNamespaceGroupButton: true, canYaml: false });
|
||||||
|
|
||||||
|
virtualType({
|
||||||
|
labelKey: 'harvester.vpc.label',
|
||||||
|
name: HCI.VPC,
|
||||||
|
namespaced: true,
|
||||||
|
weight: 195,
|
||||||
|
route: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.VPC }
|
||||||
|
},
|
||||||
|
exact: false,
|
||||||
|
ifHaveType: HCI.VPC,
|
||||||
|
});
|
||||||
|
|
||||||
|
configureType(HCI.VPC_NAT_GATEWAY, { hiddenNamespaceGroupButton: true, canYaml: false });
|
||||||
|
|
||||||
|
virtualType({
|
||||||
|
labelKey: 'harvester.natGateway.label',
|
||||||
|
name: HCI.VPC_NAT_GATEWAY,
|
||||||
|
namespaced: false,
|
||||||
|
weight: 193,
|
||||||
|
route: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.VPC_NAT_GATEWAY }
|
||||||
|
},
|
||||||
|
exact: false,
|
||||||
|
ifHaveType: HCI.VPC_NAT_GATEWAY,
|
||||||
|
});
|
||||||
|
|
||||||
|
configureType(HCI.IPTABLES_EIP, { hiddenNamespaceGroupButton: true, canYaml: false });
|
||||||
|
|
||||||
|
virtualType({
|
||||||
|
labelKey: 'harvester.externalIP.label',
|
||||||
|
name: HCI.IPTABLES_EIP,
|
||||||
|
namespaced: false,
|
||||||
|
weight: 192,
|
||||||
|
route: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.IPTABLES_EIP }
|
||||||
|
},
|
||||||
|
exact: false,
|
||||||
|
ifHaveType: HCI.IPTABLES_EIP,
|
||||||
|
});
|
||||||
|
|
||||||
|
configureType(HCI.IPTABLES_SNAT_RULE, { hiddenNamespaceGroupButton: true, canYaml: false });
|
||||||
|
|
||||||
|
virtualType({
|
||||||
|
labelKey: 'harvester.snat.label',
|
||||||
|
name: HCI.IPTABLES_SNAT_RULE,
|
||||||
|
namespaced: false,
|
||||||
|
weight: 191,
|
||||||
|
route: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.IPTABLES_SNAT_RULE }
|
||||||
|
},
|
||||||
|
exact: false,
|
||||||
|
ifHaveType: HCI.IPTABLES_SNAT_RULE,
|
||||||
|
});
|
||||||
|
|
||||||
|
configureType(HCI.IPTABLES_DNAT_RULE, { hiddenNamespaceGroupButton: true, canYaml: false });
|
||||||
|
|
||||||
|
virtualType({
|
||||||
|
labelKey: 'harvester.dnat.label',
|
||||||
|
name: HCI.IPTABLES_DNAT_RULE,
|
||||||
|
namespaced: false,
|
||||||
|
weight: 190,
|
||||||
|
route: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.IPTABLES_DNAT_RULE }
|
||||||
|
},
|
||||||
|
exact: false,
|
||||||
|
ifHaveType: HCI.IPTABLES_DNAT_RULE,
|
||||||
|
});
|
||||||
|
|
||||||
|
configureType(NETWORK_POLICY, { hiddenNamespaceGroupButton: true, canYaml: false });
|
||||||
|
|
||||||
|
virtualType({
|
||||||
|
labelKey: 'harvester.networkPolicy.label',
|
||||||
|
name: NETWORK_POLICY,
|
||||||
|
namespaced: true,
|
||||||
|
weight: 194,
|
||||||
|
route: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: NETWORK_POLICY }
|
||||||
|
},
|
||||||
|
exact: false,
|
||||||
|
ifHaveType: NETWORK_POLICY,
|
||||||
|
});
|
||||||
|
|
||||||
|
configureType(HCI.PROVIDER_NETWORK, { hiddenNamespaceGroupButton: true, canYaml: false });
|
||||||
|
|
||||||
|
virtualType({
|
||||||
|
labelKey: 'harvester.providerNetwork.label',
|
||||||
|
name: HCI.PROVIDER_NETWORK,
|
||||||
|
namespaced: false,
|
||||||
|
weight: 189,
|
||||||
|
route: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.PROVIDER_NETWORK }
|
||||||
|
},
|
||||||
|
exact: false,
|
||||||
|
ifHaveType: HCI.PROVIDER_NETWORK,
|
||||||
|
});
|
||||||
|
|
||||||
|
configureType(HCI.VLAN, { hiddenNamespaceGroupButton: true, canYaml: false });
|
||||||
|
|
||||||
|
headers(HCI.VLAN, [
|
||||||
|
STATE,
|
||||||
|
NAME_COL,
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
label: 'ID',
|
||||||
|
value: 'spec.id',
|
||||||
|
sort: 'spec.id'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'provider',
|
||||||
|
labelKey: 'harvester.subnet.provider.label',
|
||||||
|
value: 'spec.provider',
|
||||||
|
sort: 'spec.provider'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
virtualType({
|
||||||
|
labelKey: 'harvester.vlanNetwork.label',
|
||||||
|
name: HCI.VLAN,
|
||||||
|
namespaced: false,
|
||||||
|
weight: 188,
|
||||||
|
route: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.VLAN }
|
||||||
|
},
|
||||||
|
exact: false,
|
||||||
|
ifHaveType: HCI.VLAN,
|
||||||
|
});
|
||||||
|
|
||||||
configureType(HCI.SNAPSHOT, {
|
configureType(HCI.SNAPSHOT, {
|
||||||
isCreatable: false,
|
isCreatable: false,
|
||||||
location: {
|
location: {
|
||||||
@@ -813,6 +1170,26 @@ export function init($plugin, store) {
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
virtualType({
|
||||||
|
labelKey: 'harvester.migconfiguration.label',
|
||||||
|
group: 'advanced',
|
||||||
|
weight: 12,
|
||||||
|
name: HCI.MIG_CONFIGURATION,
|
||||||
|
namespaced: false,
|
||||||
|
route: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.MIG_CONFIGURATION }
|
||||||
|
},
|
||||||
|
exact: false,
|
||||||
|
ifHaveType: HCI.MIG_CONFIGURATION,
|
||||||
|
});
|
||||||
|
|
||||||
|
configureType(HCI.MIG_CONFIGURATION, {
|
||||||
|
isCreatable: false,
|
||||||
|
hiddenNamespaceGroupButton: true,
|
||||||
|
canYaml: false,
|
||||||
|
});
|
||||||
|
|
||||||
virtualType({
|
virtualType({
|
||||||
labelKey: 'harvester.usb.label',
|
labelKey: 'harvester.usb.label',
|
||||||
group: 'advanced',
|
group: 'advanced',
|
||||||
@@ -881,7 +1258,7 @@ export function init($plugin, store) {
|
|||||||
labelKey: 'harvester.loadBalancer.label',
|
labelKey: 'harvester.loadBalancer.label',
|
||||||
name: HCI.LB,
|
name: HCI.LB,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
weight: 185,
|
weight: 483,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
params: { resource: HCI.LB }
|
params: { resource: HCI.LB }
|
||||||
@@ -920,7 +1297,7 @@ export function init($plugin, store) {
|
|||||||
labelKey: 'harvester.ipPool.label',
|
labelKey: 'harvester.ipPool.label',
|
||||||
name: HCI.IP_POOL,
|
name: HCI.IP_POOL,
|
||||||
namespaced: false,
|
namespaced: false,
|
||||||
weight: 184,
|
weight: 482,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
params: { resource: HCI.IP_POOL }
|
params: { resource: HCI.IP_POOL }
|
||||||
@@ -929,4 +1306,207 @@ export function init($plugin, store) {
|
|||||||
ifHaveType: HCI.IP_POOL,
|
ifHaveType: HCI.IP_POOL,
|
||||||
});
|
});
|
||||||
headers(HCI.IP_POOL, IP_POOL_HEADERS);
|
headers(HCI.IP_POOL, IP_POOL_HEADERS);
|
||||||
|
|
||||||
|
configureType(HCI.HOST_NETWORK_CONFIG, {
|
||||||
|
location: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.HOST_NETWORK_CONFIG }
|
||||||
|
},
|
||||||
|
canYaml: false,
|
||||||
|
});
|
||||||
|
virtualType({
|
||||||
|
labelKey: 'harvester.hostNetworkConfig.label',
|
||||||
|
name: HCI.HOST_NETWORK_CONFIG,
|
||||||
|
namespaced: false,
|
||||||
|
weight: 481,
|
||||||
|
route: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.HOST_NETWORK_CONFIG }
|
||||||
|
},
|
||||||
|
exact: false,
|
||||||
|
ifHaveType: HCI.HOST_NETWORK_CONFIG,
|
||||||
|
});
|
||||||
|
// ===========================================================================
|
||||||
|
// Forklift Addon UI Flow
|
||||||
|
// ===========================================================================
|
||||||
|
weightGroup('vmMigration', 0, false);
|
||||||
|
|
||||||
|
// Provider
|
||||||
|
headers(HCI.FORKLIFT_PROVIDER, [
|
||||||
|
STATE,
|
||||||
|
NAME_COL,
|
||||||
|
NAMESPACE_COL,
|
||||||
|
FORKLIFT_PROVIDER_TYPE,
|
||||||
|
FORKLIFT_PROVIDER_URL,
|
||||||
|
AGE
|
||||||
|
]);
|
||||||
|
configureType(HCI.FORKLIFT_PROVIDER, {
|
||||||
|
resource: HCI.FORKLIFT_PROVIDER,
|
||||||
|
location: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.FORKLIFT_PROVIDER }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
virtualType({
|
||||||
|
name: HCI.FORKLIFT_PROVIDER,
|
||||||
|
labelKey: 'harvester.addons.vmMigration.labels.provider',
|
||||||
|
group: 'vmMigration',
|
||||||
|
namespaced: true,
|
||||||
|
weight: 100,
|
||||||
|
route: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.FORKLIFT_PROVIDER }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// NetworkMap
|
||||||
|
headers(HCI.FORKLIFT_NETWORK_MAP, [
|
||||||
|
STATE,
|
||||||
|
NAME_COL,
|
||||||
|
NAMESPACE_COL,
|
||||||
|
FORKLIFT_MAP_SOURCE_PROVIDER,
|
||||||
|
FORKLIFT_MAP_DEST_PROVIDER,
|
||||||
|
AGE
|
||||||
|
]);
|
||||||
|
configureType(HCI.FORKLIFT_NETWORK_MAP, {
|
||||||
|
resource: HCI.FORKLIFT_NETWORK_MAP,
|
||||||
|
location: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.FORKLIFT_NETWORK_MAP }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
virtualType({
|
||||||
|
name: HCI.FORKLIFT_NETWORK_MAP,
|
||||||
|
labelKey: 'harvester.addons.vmMigration.labels.networkMap',
|
||||||
|
group: 'vmMigration::Configurations',
|
||||||
|
namespaced: true,
|
||||||
|
route: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.FORKLIFT_NETWORK_MAP }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// StorageMap
|
||||||
|
headers(HCI.FORKLIFT_STORAGE_MAP, [
|
||||||
|
STATE,
|
||||||
|
NAME_COL,
|
||||||
|
NAMESPACE_COL,
|
||||||
|
FORKLIFT_MAP_SOURCE_PROVIDER,
|
||||||
|
FORKLIFT_MAP_DEST_PROVIDER,
|
||||||
|
AGE
|
||||||
|
]);
|
||||||
|
configureType(HCI.FORKLIFT_STORAGE_MAP, {
|
||||||
|
resource: HCI.FORKLIFT_STORAGE_MAP,
|
||||||
|
location: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.FORKLIFT_STORAGE_MAP }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
virtualType({
|
||||||
|
name: HCI.FORKLIFT_STORAGE_MAP,
|
||||||
|
labelKey: 'harvester.addons.vmMigration.labels.storageMap',
|
||||||
|
group: 'vmMigration::Configurations',
|
||||||
|
namespaced: true,
|
||||||
|
route: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.FORKLIFT_STORAGE_MAP }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Plan
|
||||||
|
headers(HCI.FORKLIFT_PLAN, [
|
||||||
|
STATE,
|
||||||
|
NAME_COL,
|
||||||
|
NAMESPACE_COL,
|
||||||
|
FORKLIFT_MAP_SOURCE_PROVIDER,
|
||||||
|
FORKLIFT_PLAN_TARGET_NS,
|
||||||
|
FORKLIFT_PLAN_VM_COUNT,
|
||||||
|
AGE
|
||||||
|
]);
|
||||||
|
configureType(HCI.FORKLIFT_PLAN, {
|
||||||
|
resource: HCI.FORKLIFT_PLAN,
|
||||||
|
location: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.FORKLIFT_PLAN }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
virtualType({
|
||||||
|
name: HCI.FORKLIFT_PLAN,
|
||||||
|
labelKey: 'harvester.addons.vmMigration.labels.plan',
|
||||||
|
group: 'vmMigration::Configurations',
|
||||||
|
namespaced: true,
|
||||||
|
route: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.FORKLIFT_PLAN }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Migration
|
||||||
|
headers(HCI.FORKLIFT_MIGRATION, [
|
||||||
|
STATE,
|
||||||
|
NAME_COL,
|
||||||
|
NAMESPACE_COL,
|
||||||
|
FORKLIFT_MIGRATION_PLAN,
|
||||||
|
AGE
|
||||||
|
]);
|
||||||
|
configureType(HCI.FORKLIFT_MIGRATION, {
|
||||||
|
resource: HCI.FORKLIFT_MIGRATION,
|
||||||
|
location: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.FORKLIFT_MIGRATION }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
virtualType({
|
||||||
|
name: HCI.FORKLIFT_MIGRATION,
|
||||||
|
labelKey: 'harvester.addons.vmMigration.labels.migration',
|
||||||
|
group: 'vmMigration::Configurations',
|
||||||
|
namespaced: true,
|
||||||
|
route: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.FORKLIFT_MIGRATION }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
configureType('forklift-create', { subTypes: [HCI.FORKLIFT_PLAN] });
|
||||||
|
virtualType({
|
||||||
|
name: 'forklift-create',
|
||||||
|
labelKey: 'harvester.addons.vmMigration.labels.dashboard',
|
||||||
|
group: 'vmMigration',
|
||||||
|
namespaced: true,
|
||||||
|
weight: 200,
|
||||||
|
route: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-vm-migration`,
|
||||||
|
params: {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enable SideNav based on Forklift Addon Status
|
||||||
|
// The dashboard entry ('forklift-create') is a schema-less virtual type, so it
|
||||||
|
// is registered with requireSchema:false to still be gated by the addon status.
|
||||||
|
registerAddonSideNav(store, PRODUCT_NAME, {
|
||||||
|
addonName: ADD_ONS.FORKLIFT_OPERATOR,
|
||||||
|
resourceType: HCI.ADD_ONS,
|
||||||
|
navGroup: 'vmMigration',
|
||||||
|
requireSchema: false,
|
||||||
|
types: ['forklift-create']
|
||||||
|
});
|
||||||
|
registerAddonSideNav(store, PRODUCT_NAME, {
|
||||||
|
addonName: ADD_ONS.FORKLIFT_OPERATOR,
|
||||||
|
resourceType: HCI.ADD_ONS,
|
||||||
|
navGroup: 'vmMigration',
|
||||||
|
types: [
|
||||||
|
HCI.FORKLIFT_PROVIDER,
|
||||||
|
]
|
||||||
|
});
|
||||||
|
registerAddonSideNav(store, PRODUCT_NAME, {
|
||||||
|
addonName: ADD_ONS.FORKLIFT_OPERATOR,
|
||||||
|
resourceType: HCI.ADD_ONS,
|
||||||
|
navGroup: 'vmMigration::Configurations',
|
||||||
|
types: [
|
||||||
|
HCI.FORKLIFT_NETWORK_MAP,
|
||||||
|
HCI.FORKLIFT_STORAGE_MAP,
|
||||||
|
HCI.FORKLIFT_PLAN,
|
||||||
|
HCI.FORKLIFT_MIGRATION,
|
||||||
|
]
|
||||||
|
});
|
||||||
|
// ===========================================================================
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { HCI } from '../types';
|
||||||
|
|
||||||
// TODO: delete this not used variable
|
// TODO: delete this not used variable
|
||||||
export const MemoryUnit = [{
|
export const MemoryUnit = [{
|
||||||
label: 'Mi',
|
label: 'Mi',
|
||||||
@@ -37,6 +39,12 @@ export const VOLUME_TYPE = [{
|
|||||||
value: 'cd-rom'
|
value: 'cd-rom'
|
||||||
}];
|
}];
|
||||||
|
|
||||||
|
export const VOLUME_HOTPLUG_ACTION = {
|
||||||
|
INSERT_CDROM_IMAGE: 'INSERT_CDROM_IMAGE',
|
||||||
|
EJECT_CDROM_IMAGE: 'EJECT_CDROM_IMAGE',
|
||||||
|
DETACH_DISK: 'DETACH_DISK'
|
||||||
|
};
|
||||||
|
|
||||||
export const ACCESS_CREDENTIALS = {
|
export const ACCESS_CREDENTIALS = {
|
||||||
RESET_PWD: 'userPassword',
|
RESET_PWD: 'userPassword',
|
||||||
INJECT_SSH: 'sshPublicKey'
|
INJECT_SSH: 'sshPublicKey'
|
||||||
@@ -70,6 +78,8 @@ export const ADD_ONS = {
|
|||||||
RANCHER_MONITORING: 'rancher-monitoring',
|
RANCHER_MONITORING: 'rancher-monitoring',
|
||||||
VM_IMPORT_CONTROLLER: 'vm-import-controller',
|
VM_IMPORT_CONTROLLER: 'vm-import-controller',
|
||||||
LVM_DRIVER: 'lvm.driver.harvesterhci.io',
|
LVM_DRIVER: 'lvm.driver.harvesterhci.io',
|
||||||
|
KUBEOVN_OPERATOR: 'kubeovn-operator',
|
||||||
|
FORKLIFT_OPERATOR: 'forklift-operator',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CSI_SECRETS = {
|
export const CSI_SECRETS = {
|
||||||
@@ -79,4 +89,20 @@ export const CSI_SECRETS = {
|
|||||||
CSI_NODE_PUBLISH_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-publish-secret-namespace',
|
CSI_NODE_PUBLISH_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-publish-secret-namespace',
|
||||||
CSI_NODE_STAGE_SECRET_NAME: 'csi.storage.k8s.io/node-stage-secret-name',
|
CSI_NODE_STAGE_SECRET_NAME: 'csi.storage.k8s.io/node-stage-secret-name',
|
||||||
CSI_NODE_STAGE_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-stage-secret-namespace',
|
CSI_NODE_STAGE_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-stage-secret-namespace',
|
||||||
|
CSI_NODE_EXPAND_SECRET_NAME: 'csi.storage.k8s.io/node-expand-secret-name',
|
||||||
|
CSI_NODE_EXPAND_SECRET_NAMESPACE: 'csi.storage.k8s.io/node-expand-secret-namespace'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FORKLIFT_API_VERSION = 'forklift.konveyor.io/v1beta1';
|
||||||
|
export const FORKLIFT_NAMESPACE = 'forklift';
|
||||||
|
|
||||||
|
// Some harvester CRD type is not equal to model file name, define the mapping here
|
||||||
|
export const HARVESTER_CRD_MAP = {
|
||||||
|
node: HCI.HOST,
|
||||||
|
configmap: HCI.CLOUD_TEMPLATE,
|
||||||
|
persistentvolumeclaim: HCI.VOLUME,
|
||||||
|
'snapshot.storage.k8s.io.volumesnapshot': HCI.SNAPSHOT,
|
||||||
|
// specific groupable table detail page
|
||||||
|
'network.harvesterhci.io.vlanconfig': HCI.CLUSTER_NETWORK,
|
||||||
|
'kubeovn.io.subnet': HCI.VPC
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,9 +16,11 @@ export const HCI = {
|
|||||||
CREATOR: 'harvesterhci.io/creator',
|
CREATOR: 'harvesterhci.io/creator',
|
||||||
OS: 'harvesterhci.io/os',
|
OS: 'harvesterhci.io/os',
|
||||||
GOLDEN_IMAGE: 'harvesterhci.io/goldenImage',
|
GOLDEN_IMAGE: 'harvesterhci.io/goldenImage',
|
||||||
|
CLUSTER_NETWORK: 'network.harvesterhci.io/clusternetwork',
|
||||||
NETWORK_TYPE: 'network.harvesterhci.io/type',
|
NETWORK_TYPE: 'network.harvesterhci.io/type',
|
||||||
VM_NAME: 'harvesterhci.io/vmName',
|
VM_NAME: 'harvesterhci.io/vmName',
|
||||||
VM_NAME_PREFIX: 'harvesterhci.io/vmNamePrefix',
|
VM_NAME_PREFIX: 'harvesterhci.io/vmNamePrefix',
|
||||||
|
VM_DISPLAY_NAME: 'harvesterhci.io/vmDisplayName',
|
||||||
VM_RESERVED_MEMORY: 'harvesterhci.io/reservedMemory',
|
VM_RESERVED_MEMORY: 'harvesterhci.io/reservedMemory',
|
||||||
MAINTENANCE_STATUS: 'harvesterhci.io/maintain-status',
|
MAINTENANCE_STATUS: 'harvesterhci.io/maintain-status',
|
||||||
HOST_CUSTOM_NAME: 'harvesterhci.io/host-custom-name',
|
HOST_CUSTOM_NAME: 'harvesterhci.io/host-custom-name',
|
||||||
@@ -28,6 +30,7 @@ export const HCI = {
|
|||||||
NODE_ROLE_CONTROL_PLANE: 'node-role.kubernetes.io/control-plane',
|
NODE_ROLE_CONTROL_PLANE: 'node-role.kubernetes.io/control-plane',
|
||||||
NODE_ROLE_ETCD: 'node-role.harvesterhci.io/witness',
|
NODE_ROLE_ETCD: 'node-role.harvesterhci.io/witness',
|
||||||
PROMOTE_STATUS: 'harvesterhci.io/promote-status',
|
PROMOTE_STATUS: 'harvesterhci.io/promote-status',
|
||||||
|
CLONE_BACKEND_STORAGE_STATUS: 'harvesterhci.io/clone-backend-storage-status',
|
||||||
MIGRATION_STATE: 'harvesterhci.io/migrationState',
|
MIGRATION_STATE: 'harvesterhci.io/migrationState',
|
||||||
VOLUME_CLAIM_TEMPLATE: 'harvesterhci.io/volumeClaimTemplates',
|
VOLUME_CLAIM_TEMPLATE: 'harvesterhci.io/volumeClaimTemplates',
|
||||||
IMAGE_NAME: 'harvesterhci.io/image-name',
|
IMAGE_NAME: 'harvesterhci.io/image-name',
|
||||||
@@ -35,6 +38,7 @@ export const HCI = {
|
|||||||
NODE_SCHEDULABLE: 'kubevirt.io/schedulable',
|
NODE_SCHEDULABLE: 'kubevirt.io/schedulable',
|
||||||
NETWORK_ROUTE: 'network.harvesterhci.io/route',
|
NETWORK_ROUTE: 'network.harvesterhci.io/route',
|
||||||
MATCHED_NODES: 'network.harvesterhci.io/matched-nodes',
|
MATCHED_NODES: 'network.harvesterhci.io/matched-nodes',
|
||||||
|
UPGRADE: 'harvesterhci.io/upgrade',
|
||||||
OS_UPGRADE_IMAGE: 'harvesterhci.io/os-upgrade-image',
|
OS_UPGRADE_IMAGE: 'harvesterhci.io/os-upgrade-image',
|
||||||
LATEST_UPGRADE: 'harvesterhci.io/latestUpgrade',
|
LATEST_UPGRADE: 'harvesterhci.io/latestUpgrade',
|
||||||
UPGRADE_STATE: 'harvesterhci.io/upgradeState',
|
UPGRADE_STATE: 'harvesterhci.io/upgradeState',
|
||||||
@@ -49,6 +53,7 @@ export const HCI = {
|
|||||||
STORAGE_CLASS: 'harvesterhci.io/storageClassName',
|
STORAGE_CLASS: 'harvesterhci.io/storageClassName',
|
||||||
STORAGE_NETWORK: 'storage-network.settings.harvesterhci.io',
|
STORAGE_NETWORK: 'storage-network.settings.harvesterhci.io',
|
||||||
ADDON_EXPERIMENTAL: 'addon.harvesterhci.io/experimental',
|
ADDON_EXPERIMENTAL: 'addon.harvesterhci.io/experimental',
|
||||||
|
ADDON_DISPLAYNAME: 'addon.harvesterhci.io/displayName',
|
||||||
VOLUME_ERROR: 'longhorn.io/volume-scheduling-error',
|
VOLUME_ERROR: 'longhorn.io/volume-scheduling-error',
|
||||||
VOLUME_FOR_VM: 'harvesterhci.io/volumeForVirtualMachine',
|
VOLUME_FOR_VM: 'harvesterhci.io/volumeForVirtualMachine',
|
||||||
KVM_AMD_CPU: 'cpu-feature.node.kubevirt.io/svm',
|
KVM_AMD_CPU: 'cpu-feature.node.kubevirt.io/svm',
|
||||||
@@ -66,4 +71,20 @@ export const HCI = {
|
|||||||
VM_DEVICE_ALLOCATION_DETAILS: 'harvesterhci.io/deviceAllocationDetails',
|
VM_DEVICE_ALLOCATION_DETAILS: 'harvesterhci.io/deviceAllocationDetails',
|
||||||
SVM_BACKUP_ID: 'harvesterhci.io/svmbackupId',
|
SVM_BACKUP_ID: 'harvesterhci.io/svmbackupId',
|
||||||
DISABLE_LONGHORN_V2_ENGINE: 'node.longhorn.io/disable-v2-data-engine',
|
DISABLE_LONGHORN_V2_ENGINE: 'node.longhorn.io/disable-v2-data-engine',
|
||||||
|
K8S_ARCH: 'kubernetes.io/arch',
|
||||||
|
IMAGE_DISPLAY_NAME: 'harvesterhci.io/imageDisplayName',
|
||||||
|
CUSTOM_IP: 'harvesterhci.io/custom-ip',
|
||||||
|
IMPORTED_IMAGE: 'migration.harvesterhci.io/imported',
|
||||||
|
VM_CPU_MEMORY_HOTPLUG: 'harvesterhci.io/enableCPUAndMemoryHotplug',
|
||||||
|
FILESYSTEM_OVERHEAD: 'cdi.harvesterhci.io/filesystemOverhead',
|
||||||
|
CLONE_STRATEGY: 'cdi.harvesterhci.io/storageProfileCloneStrategy',
|
||||||
|
VOLUME_MODE_ACCESS_MODES: 'cdi.harvesterhci.io/storageProfileVolumeModeAccessModes',
|
||||||
|
VOLUME_SNAPSHOT_CLASS: 'cdi.harvesterhci.io/storageProfileVolumeSnapshotClass',
|
||||||
|
MAC_ADDRESS: 'harvesterhci.io/mac-address',
|
||||||
|
STATIC_IP: 'static-ip.harvesterhci.io',
|
||||||
|
NODE_UPGRADE_PAUSE_MAP: 'harvesterhci.io/node-upgrade-pause-map',
|
||||||
|
CDI_POPULATOR_KIND: 'cdi.kubevirt.io/storage.populator.kind',
|
||||||
|
CNI_NETWORKS: 'k8s.v1.cni.cncf.io/networks',
|
||||||
|
WINDOWS_SYSPREP: 'harvesterhci.io/windows-sysprep',
|
||||||
|
SKIP_MIG_CONFIGURATION: 'harvesterhci.io/skip-mig-configuration',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export const CLUSTER_NETWORK = 'clusterNetwork';
|
export const CLUSTER_NETWORK = 'clusterNetwork';
|
||||||
|
export const VPC = 'vpc';
|
||||||
|
|||||||
@@ -1,39 +1,50 @@
|
|||||||
export const HCI_SETTING = {
|
export const HCI_SETTING = {
|
||||||
BACKUP_TARGET: 'backup-target',
|
BACKUP_TARGET: 'backup-target',
|
||||||
CONTAINERD_REGISTRY: 'containerd-registry',
|
CONTAINERD_REGISTRY: 'containerd-registry',
|
||||||
LOG_LEVEL: 'log-level',
|
LOG_LEVEL: 'log-level',
|
||||||
SERVER_VERSION: 'server-version',
|
SERVER_VERSION: 'server-version',
|
||||||
UI_INDEX: 'ui-index',
|
UI_INDEX: 'ui-index',
|
||||||
UI_PLUGIN_INDEX: 'ui-plugin-index',
|
UPGRADE_CHECKER_ENABLED: 'upgrade-checker-enabled',
|
||||||
UPGRADE_CHECKER_ENABLED: 'upgrade-checker-enabled',
|
UPGRADE_CHECKER_URL: 'upgrade-checker-url',
|
||||||
UPGRADE_CHECKER_URL: 'upgrade-checker-url',
|
VLAN: 'vlan',
|
||||||
VLAN: 'vlan',
|
UI_SOURCE: 'ui-source',
|
||||||
UI_SOURCE: 'ui-source',
|
UI_PL: 'ui-pl',
|
||||||
UI_PL: 'ui-pl',
|
HTTP_PROXY: 'http-proxy',
|
||||||
HTTP_PROXY: 'http-proxy',
|
ADDITIONAL_CA: 'additional-ca',
|
||||||
ADDITIONAL_CA: 'additional-ca',
|
OVERCOMMIT_CONFIG: 'overcommit-config',
|
||||||
OVERCOMMIT_CONFIG: 'overcommit-config',
|
CLUSTER_REGISTRATION_URL: 'cluster-registration-url',
|
||||||
CLUSTER_REGISTRATION_URL: 'cluster-registration-url',
|
DEFAULT_STORAGE_CLASS: 'default-storage-class',
|
||||||
DEFAULT_STORAGE_CLASS: 'default-storage-class',
|
SUPPORT_BUNDLE_TIMEOUT: 'support-bundle-timeout',
|
||||||
SUPPORT_BUNDLE_TIMEOUT: 'support-bundle-timeout',
|
SUPPORT_BUNDLE_EXPIRATION: 'support-bundle-expiration',
|
||||||
SUPPORT_BUNDLE_EXPIRATION: 'support-bundle-expiration',
|
SUPPORT_BUNDLE_FILE_NAME: 'support-bundle-file-name',
|
||||||
SUPPORT_BUNDLE_IMAGE: 'support-bundle-image',
|
SUPPORT_BUNDLE_IMAGE: 'support-bundle-image',
|
||||||
SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT: 'support-bundle-node-collection-timeout',
|
SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT: 'support-bundle-node-collection-timeout',
|
||||||
STORAGE_NETWORK: 'storage-network',
|
STORAGE_NETWORK: 'storage-network',
|
||||||
VM_FORCE_RESET_POLICY: 'vm-force-reset-policy',
|
RWX_NETWORK: 'rwx-network',
|
||||||
SSL_CERTIFICATES: 'ssl-certificates',
|
VM_FORCE_RESET_POLICY: 'vm-force-reset-policy',
|
||||||
SSL_PARAMETERS: 'ssl-parameters',
|
SSL_CERTIFICATES: 'ssl-certificates',
|
||||||
SUPPORT_BUNDLE_NAMESPACES: 'support-bundle-namespaces',
|
SSL_PARAMETERS: 'ssl-parameters',
|
||||||
AUTO_DISK_PROVISION_PATHS: 'auto-disk-provision-paths',
|
SUPPORT_BUNDLE_NAMESPACES: 'support-bundle-namespaces',
|
||||||
RELEASE_DOWNLOAD_URL: 'release-download-url',
|
AUTO_DISK_PROVISION_PATHS: 'auto-disk-provision-paths',
|
||||||
CCM_CSI_VERSION: 'harvester-csi-ccm-versions',
|
RELEASE_DOWNLOAD_URL: 'release-download-url',
|
||||||
CSI_DRIVER_CONFIG: 'csi-driver-config',
|
CCM_CSI_VERSION: 'harvester-csi-ccm-versions',
|
||||||
VM_TERMINATION_PERIOD: 'default-vm-termination-grace-period-seconds',
|
CSI_DRIVER_CONFIG: 'csi-driver-config',
|
||||||
NTP_SERVERS: 'ntp-servers',
|
CSI_ONLINE_EXPAND_VALIDATION: 'csi-online-expand-validation',
|
||||||
AUTO_ROTATE_RKE2_CERTS: 'auto-rotate-rke2-certs',
|
VM_TERMINATION_PERIOD: 'default-vm-termination-grace-period-seconds',
|
||||||
KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES: 'kubeconfig-default-token-ttl-minutes',
|
NTP_SERVERS: 'ntp-servers',
|
||||||
LONGHORN_V2_DATA_ENGINE_ENABLED: 'longhorn-v2-data-engine-enabled',
|
AUTO_ROTATE_RKE2_CERTS: 'auto-rotate-rke2-certs',
|
||||||
ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO: 'additional-guest-memory-overhead-ratio',
|
KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES: 'kubeconfig-default-token-ttl-minutes',
|
||||||
|
LONGHORN_V2_DATA_ENGINE_ENABLED: 'longhorn-v2-data-engine-enabled',
|
||||||
|
LONGHORN_V2_DATA_ENGINE_HUGEPAGE_ENABLED: 'longhorn-v2-data-engine-hugepage-enabled',
|
||||||
|
LONGHORN_V2_DATA_ENGINE_MEMORY_SIZE: 'longhorn-v2-data-engine-memory-size',
|
||||||
|
ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO: 'additional-guest-memory-overhead-ratio',
|
||||||
|
UPGRADE_CONFIG: 'upgrade-config',
|
||||||
|
VM_MIGRATION_NETWORK: 'vm-migration-network',
|
||||||
|
RANCHER_CLUSTER: 'rancher-cluster',
|
||||||
|
MAX_HOTPLUG_RATIO: 'max-hotplug-ratio',
|
||||||
|
KUBEVIRT_MIGRATION: 'kubevirt-migration',
|
||||||
|
INSTANCE_MANAGER_RESOURCES: 'instance-manager-resources',
|
||||||
|
CLUSTER_POD_SECURITY_STANDARD: 'cluster-pod-security-standard'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HCI_ALLOWED_SETTINGS = {
|
export const HCI_ALLOWED_SETTINGS = {
|
||||||
@@ -52,22 +63,31 @@ export const HCI_ALLOWED_SETTINGS = {
|
|||||||
from: 'import',
|
from: 'import',
|
||||||
featureFlag: 'autoRotateRke2CertsSetting'
|
featureFlag: 'autoRotateRke2CertsSetting'
|
||||||
},
|
},
|
||||||
[HCI_SETTING.CSI_DRIVER_CONFIG]: { kind: 'json', from: 'import' },
|
[HCI_SETTING.CSI_DRIVER_CONFIG]: { kind: 'json', from: 'import' },
|
||||||
[HCI_SETTING.SERVER_VERSION]: { readOnly: true },
|
[HCI_SETTING.CSI_ONLINE_EXPAND_VALIDATION]: {
|
||||||
[HCI_SETTING.UPGRADE_CHECKER_ENABLED]: { kind: 'boolean' },
|
kind: 'json', from: 'import', featureFlag: 'csiOnlineExpandValidation'
|
||||||
[HCI_SETTING.UPGRADE_CHECKER_URL]: { kind: 'url' },
|
},
|
||||||
[HCI_SETTING.HTTP_PROXY]: { kind: 'json', from: 'import' },
|
[HCI_SETTING.SERVER_VERSION]: { readOnly: true },
|
||||||
[HCI_SETTING.ADDITIONAL_CA]: {
|
[HCI_SETTING.UPGRADE_CHECKER_ENABLED]: { kind: 'boolean' },
|
||||||
|
[HCI_SETTING.UPGRADE_CHECKER_URL]: { kind: 'url' },
|
||||||
|
[HCI_SETTING.HTTP_PROXY]: { kind: 'json', from: 'import' },
|
||||||
|
[HCI_SETTING.ADDITIONAL_CA]: {
|
||||||
kind: 'multiline', canReset: true, from: 'import'
|
kind: 'multiline', canReset: true, from: 'import'
|
||||||
},
|
},
|
||||||
[HCI_SETTING.OVERCOMMIT_CONFIG]: { kind: 'json', from: 'import' },
|
[HCI_SETTING.OVERCOMMIT_CONFIG]: { kind: 'json', from: 'import' },
|
||||||
[HCI_SETTING.SUPPORT_BUNDLE_TIMEOUT]: {},
|
[HCI_SETTING.SUPPORT_BUNDLE_TIMEOUT]: { kind: 'number' },
|
||||||
[HCI_SETTING.SUPPORT_BUNDLE_EXPIRATION]: {},
|
[HCI_SETTING.SUPPORT_BUNDLE_EXPIRATION]: { kind: 'number' },
|
||||||
[HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT]: { featureFlag: 'supportBundleNodeCollectionTimeoutSetting' },
|
[HCI_SETTING.SUPPORT_BUNDLE_FILE_NAME]: {
|
||||||
|
kind: 'string', canReset: true, featureFlag: 'supportBundleFileNameSetting'
|
||||||
|
},
|
||||||
|
[HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT]: { kind: 'number', featureFlag: 'supportBundleNodeCollectionTimeoutSetting' },
|
||||||
[HCI_SETTING.SUPPORT_BUNDLE_IMAGE]: { kind: 'json', from: 'import' },
|
[HCI_SETTING.SUPPORT_BUNDLE_IMAGE]: { kind: 'json', from: 'import' },
|
||||||
[HCI_SETTING.STORAGE_NETWORK]: {
|
[HCI_SETTING.STORAGE_NETWORK]: {
|
||||||
kind: 'custom', from: 'import', canReset: true
|
kind: 'custom', from: 'import', canReset: true
|
||||||
},
|
},
|
||||||
|
[HCI_SETTING.RWX_NETWORK]: {
|
||||||
|
kind: 'json', from: 'import', canReset: true, featureFlag: 'rwxNetworkSetting'
|
||||||
|
},
|
||||||
[HCI_SETTING.VM_FORCE_RESET_POLICY]: { kind: 'json', from: 'import' },
|
[HCI_SETTING.VM_FORCE_RESET_POLICY]: { kind: 'json', from: 'import' },
|
||||||
[HCI_SETTING.SSL_CERTIFICATES]: { kind: 'json', from: 'import' },
|
[HCI_SETTING.SSL_CERTIFICATES]: { kind: 'json', from: 'import' },
|
||||||
[HCI_SETTING.SSL_PARAMETERS]: {
|
[HCI_SETTING.SSL_PARAMETERS]: {
|
||||||
@@ -76,7 +96,6 @@ export const HCI_ALLOWED_SETTINGS = {
|
|||||||
[HCI_SETTING.SUPPORT_BUNDLE_NAMESPACES]: { from: 'import', canReset: true },
|
[HCI_SETTING.SUPPORT_BUNDLE_NAMESPACES]: { from: 'import', canReset: true },
|
||||||
[HCI_SETTING.AUTO_DISK_PROVISION_PATHS]: { canReset: true },
|
[HCI_SETTING.AUTO_DISK_PROVISION_PATHS]: { canReset: true },
|
||||||
[HCI_SETTING.RELEASE_DOWNLOAD_URL]: { kind: 'url' },
|
[HCI_SETTING.RELEASE_DOWNLOAD_URL]: { kind: 'url' },
|
||||||
[HCI_SETTING.UI_PLUGIN_INDEX]: { kind: 'url' },
|
|
||||||
[HCI_SETTING.CONTAINERD_REGISTRY]: {
|
[HCI_SETTING.CONTAINERD_REGISTRY]: {
|
||||||
kind: 'json', from: 'import', canReset: true
|
kind: 'json', from: 'import', canReset: true
|
||||||
},
|
},
|
||||||
@@ -89,18 +108,49 @@ export const HCI_ALLOWED_SETTINGS = {
|
|||||||
[HCI_SETTING.NTP_SERVERS]: {
|
[HCI_SETTING.NTP_SERVERS]: {
|
||||||
kind: 'json', from: 'import', canReset: true
|
kind: 'json', from: 'import', canReset: true
|
||||||
},
|
},
|
||||||
[HCI_SETTING.KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES]: { featureFlag: 'kubeconfigDefaultTokenTTLMinutesSetting' },
|
[HCI_SETTING.KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES]: { kind: 'number', featureFlag: 'kubeconfigDefaultTokenTTLMinutesSetting' },
|
||||||
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_ENABLED]: {
|
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_ENABLED]: {
|
||||||
kind: 'boolean',
|
kind: 'boolean',
|
||||||
experimental: true,
|
experimental: true,
|
||||||
featureFlag: 'longhornV2LVMSupport'
|
featureFlag: 'longhornV2LVMSupport'
|
||||||
},
|
},
|
||||||
|
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_HUGEPAGE_ENABLED]: {
|
||||||
|
kind: 'boolean',
|
||||||
|
featureFlag: 'longhornV2HugepageSettings'
|
||||||
|
},
|
||||||
|
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_MEMORY_SIZE]: {
|
||||||
|
kind: 'number',
|
||||||
|
featureFlag: 'longhornV2HugepageSettings'
|
||||||
|
},
|
||||||
[HCI_SETTING.ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO]: { kind: 'string', from: 'import' },
|
[HCI_SETTING.ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO]: { kind: 'string', from: 'import' },
|
||||||
|
[HCI_SETTING.UPGRADE_CONFIG]: {
|
||||||
|
kind: 'json',
|
||||||
|
from: 'import',
|
||||||
|
featureFlag: 'upgradeConfigSetting',
|
||||||
|
docPath: 'UPGRADE_CONFIG_URL'
|
||||||
|
},
|
||||||
|
[HCI_SETTING.RANCHER_CLUSTER]: {
|
||||||
|
kind: 'custom', from: 'import', canReset: true, featureFlag: 'rancherClusterSetting'
|
||||||
|
},
|
||||||
|
[HCI_SETTING.MAX_HOTPLUG_RATIO]: { kind: 'number', featureFlag: 'cpuMemoryHotplug' },
|
||||||
|
[HCI_SETTING.VM_MIGRATION_NETWORK]: {
|
||||||
|
kind: 'json', from: 'import', canReset: true, featureFlag: 'vmNetworkMigration',
|
||||||
|
},
|
||||||
|
[HCI_SETTING.KUBEVIRT_MIGRATION]: {
|
||||||
|
kind: 'json', from: 'import', canReset: true, featureFlag: 'kubevirtMigration',
|
||||||
|
},
|
||||||
|
[HCI_SETTING.INSTANCE_MANAGER_RESOURCES]: {
|
||||||
|
kind: 'json', from: 'import', featureFlag: 'instanceManagerResourcesSetting'
|
||||||
|
},
|
||||||
|
[HCI_SETTING.CLUSTER_POD_SECURITY_STANDARD]: {
|
||||||
|
kind: 'json', from: 'import', canReset: true, featureFlag: 'clusterPodSecurityStandardSetting'
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = {
|
export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = {
|
||||||
[HCI_SETTING.CLUSTER_REGISTRATION_URL]: {
|
[HCI_SETTING.CLUSTER_REGISTRATION_URL]: {
|
||||||
kind: 'url',
|
kind: 'custom',
|
||||||
|
from: 'import',
|
||||||
canReset: true,
|
canReset: true,
|
||||||
},
|
},
|
||||||
[HCI_SETTING.UI_PL]: {
|
[HCI_SETTING.UI_PL]: {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Harvester
|
* Harvester
|
||||||
*/
|
*/
|
||||||
|
import { DESCRIPTION } from '@shell/config/table-headers';
|
||||||
|
|
||||||
// image
|
// image
|
||||||
export const IMAGE_DOWNLOAD_SIZE = {
|
export const IMAGE_DOWNLOAD_SIZE = {
|
||||||
@@ -96,5 +97,203 @@ export const IMAGE_STORAGE_CLASS = {
|
|||||||
sort: 'imageStorageClass',
|
sort: 'imageStorageClass',
|
||||||
value: 'imageStorageClass',
|
value: 'imageStorageClass',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
width: 200,
|
width: 150,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const HARVESTER_DESCRIPTION = {
|
||||||
|
...DESCRIPTION,
|
||||||
|
width: 150,
|
||||||
|
};
|
||||||
|
|
||||||
|
// The CIDR_BLOCK column in VPC list page
|
||||||
|
export const CIDR_BLOCK = {
|
||||||
|
name: 'cidrBlock',
|
||||||
|
labelKey: 'harvester.subnet.cidrBlock.label',
|
||||||
|
sort: 'cidrBlock',
|
||||||
|
value: 'spec.cidrBlock',
|
||||||
|
align: 'left',
|
||||||
|
};
|
||||||
|
|
||||||
|
// The Protocol column in VPC list page
|
||||||
|
export const PROTOCOL = {
|
||||||
|
name: 'protocol',
|
||||||
|
labelKey: 'harvester.subnet.protocol.label',
|
||||||
|
sort: 'protocol',
|
||||||
|
value: 'spec.protocol',
|
||||||
|
align: 'left',
|
||||||
|
};
|
||||||
|
|
||||||
|
// The Provider column in VPC list page
|
||||||
|
export const PROVIDER = {
|
||||||
|
name: 'provider',
|
||||||
|
labelKey: 'harvester.subnet.provider.label',
|
||||||
|
sort: 'provider',
|
||||||
|
value: 'spec.provider',
|
||||||
|
align: 'left',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Source VM column in migration.harvesterhci.io.virtualmachineimport list page
|
||||||
|
export const VM_IMPORT_SOURCE_VM = {
|
||||||
|
name: 'sourceVm',
|
||||||
|
labelKey: 'harvester.tableHeaders.vmImportSourceVm',
|
||||||
|
value: 'spec.virtualMachineName',
|
||||||
|
sort: 'spec.virtualMachineName',
|
||||||
|
align: 'left',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Source Cluster column in migration.harvesterhci.io.virtualmachineimport list page
|
||||||
|
export const VM_IMPORT_SOURCE_CLUSTER = {
|
||||||
|
name: 'sourceCluster',
|
||||||
|
labelKey: 'harvester.tableHeaders.vmImportSourceCluster',
|
||||||
|
value: 'spec.sourceCluster.name',
|
||||||
|
sort: 'spec.sourceCluster.name',
|
||||||
|
align: 'left',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Import Status column in migration.harvesterhci.io.virtualmachineimport list page
|
||||||
|
export const VM_IMPORT_STATUS = {
|
||||||
|
name: 'importStatus',
|
||||||
|
labelKey: 'harvester.tableHeaders.vmImportStatus',
|
||||||
|
value: 'status.importStatus',
|
||||||
|
sort: 'status.importStatus',
|
||||||
|
align: 'left',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Datacenter column in migration.harvesterhci.io.vmwaresource list page
|
||||||
|
export const VM_IMPORT_SOURCE_V_DC = {
|
||||||
|
name: 'datacenter',
|
||||||
|
labelKey: 'harvester.tableHeaders.vmImportSourceVDatacenter',
|
||||||
|
value: 'spec.dc',
|
||||||
|
sort: 'spec.dc',
|
||||||
|
align: 'left',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Endpoint column in migration.harvesterhci.io.vmwaresource list page
|
||||||
|
export const VM_IMPORT_SOURCE_V_ENDPOINT = {
|
||||||
|
name: 'endpoint',
|
||||||
|
labelKey: 'harvester.tableHeaders.vmImportSourceVEndpoint',
|
||||||
|
value: 'spec.endpoint',
|
||||||
|
sort: 'spec.endpoint',
|
||||||
|
align: 'left',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cluster Status column in migration.harvesterhci.io.vmwaresource list page
|
||||||
|
export const VM_IMPORT_SOURCE_V_STATUS = {
|
||||||
|
name: 'clusterStatus',
|
||||||
|
labelKey: 'harvester.tableHeaders.vmImportSourceVClusterStatus',
|
||||||
|
value: 'status.status',
|
||||||
|
sort: 'status.status',
|
||||||
|
align: 'left',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Region column in migration.harvesterhci.io.openstacksource list page
|
||||||
|
export const VM_IMPORT_SOURCE_O_REGION = {
|
||||||
|
name: 'region',
|
||||||
|
labelKey: 'harvester.tableHeaders.vmImportSourceORegion',
|
||||||
|
value: 'spec.region',
|
||||||
|
sort: 'spec.region',
|
||||||
|
align: 'left',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Endpoint column in migration.harvesterhci.io.openstacksource list page
|
||||||
|
export const VM_IMPORT_SOURCE_O_ENDPOINT = {
|
||||||
|
name: 'endpoint',
|
||||||
|
labelKey: 'harvester.tableHeaders.vmImportSourceOEndpoint',
|
||||||
|
value: 'spec.endpoint',
|
||||||
|
sort: 'spec.endpoint',
|
||||||
|
align: 'left',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cluster Status column in migration.harvesterhci.io.openstacksource list page
|
||||||
|
export const VM_IMPORT_SOURCE_O_STATUS = {
|
||||||
|
name: 'clusterStatus',
|
||||||
|
labelKey: 'harvester.tableHeaders.vmImportSourceOClusterStatus',
|
||||||
|
value: 'status.status',
|
||||||
|
sort: 'status.status',
|
||||||
|
align: 'left',
|
||||||
|
};
|
||||||
|
|
||||||
|
// URL column in migration.harvesterhci.io.ovasource list page
|
||||||
|
export const VM_IMPORT_SOURCE_OVA_URL = {
|
||||||
|
name: 'url',
|
||||||
|
labelKey: 'harvester.tableHeaders.vmImportSourceOVAUrl',
|
||||||
|
value: 'spec.url',
|
||||||
|
sort: 'spec.url',
|
||||||
|
align: 'left',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Status column in migration.harvesterhci.io.ovasource list page
|
||||||
|
export const VM_IMPORT_SOURCE_OVA_STATUS = {
|
||||||
|
name: 'status',
|
||||||
|
labelKey: 'harvester.tableHeaders.vmImportSourceOVAStatus',
|
||||||
|
value: 'status.status',
|
||||||
|
sort: 'status.status',
|
||||||
|
align: 'left',
|
||||||
|
};
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Forklift table headers
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
// Provider type column in forklift.konveyor.io.provider list page
|
||||||
|
export const FORKLIFT_PROVIDER_TYPE = {
|
||||||
|
name: 'providerType',
|
||||||
|
labelKey: 'harvester.tableHeaders.vmMigrationProviderType',
|
||||||
|
value: 'spec.type',
|
||||||
|
sort: 'spec.type',
|
||||||
|
align: 'left',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Provider URL column in forklift.konveyor.io.provider list page
|
||||||
|
export const FORKLIFT_PROVIDER_URL = {
|
||||||
|
name: 'providerUrl',
|
||||||
|
labelKey: 'harvester.tableHeaders.vmMigrationProviderUrl',
|
||||||
|
value: 'spec.url',
|
||||||
|
sort: 'spec.url',
|
||||||
|
align: 'left',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Source provider column in forklift network/storage map list page
|
||||||
|
export const FORKLIFT_MAP_SOURCE_PROVIDER = {
|
||||||
|
name: 'sourceProvider',
|
||||||
|
labelKey: 'harvester.tableHeaders.vmMigrationMapSourceProvider',
|
||||||
|
value: 'spec.provider.source.name',
|
||||||
|
sort: 'spec.provider.source.name',
|
||||||
|
align: 'left',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Destination provider column in forklift network/storage map list page
|
||||||
|
export const FORKLIFT_MAP_DEST_PROVIDER = {
|
||||||
|
name: 'destProvider',
|
||||||
|
labelKey: 'harvester.tableHeaders.vmMigrationMapDestProvider',
|
||||||
|
value: 'spec.provider.destination.name',
|
||||||
|
sort: 'spec.provider.destination.name',
|
||||||
|
align: 'left',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Target namespace column in forklift.konveyor.io.plan list page
|
||||||
|
export const FORKLIFT_PLAN_TARGET_NS = {
|
||||||
|
name: 'targetNamespace',
|
||||||
|
labelKey: 'harvester.tableHeaders.vmMigrationPlanTargetNs',
|
||||||
|
value: 'spec.targetNamespace',
|
||||||
|
sort: 'spec.targetNamespace',
|
||||||
|
align: 'left',
|
||||||
|
};
|
||||||
|
|
||||||
|
// VM count column in forklift.konveyor.io.plan list page
|
||||||
|
export const FORKLIFT_PLAN_VM_COUNT = {
|
||||||
|
name: 'vmCount',
|
||||||
|
labelKey: 'harvester.tableHeaders.vmMigrationPlanVmCount',
|
||||||
|
value: 'spec.vms.length',
|
||||||
|
sort: 'spec.vms.length',
|
||||||
|
align: 'center',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Plan reference column in forklift.konveyor.io.migration list page
|
||||||
|
export const FORKLIFT_MIGRATION_PLAN = {
|
||||||
|
name: 'plan',
|
||||||
|
labelKey: 'harvester.tableHeaders.vmMigrationMigrationPlan',
|
||||||
|
value: 'spec.plan.name',
|
||||||
|
sort: 'spec.plan.name',
|
||||||
|
align: 'left',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,11 +4,58 @@ export const BACKUP_TYPE = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const NETWORK_TYPE = {
|
export const NETWORK_TYPE = {
|
||||||
L2VLAN: 'L2VlanNetwork',
|
L2VLAN: 'L2VlanNetwork',
|
||||||
UNTAGGED: 'UntaggedNetwork'
|
UNTAGGED: 'UntaggedNetwork',
|
||||||
|
OVERLAY: 'OverlayNetwork',
|
||||||
|
L2TRUNK_VLAN: 'L2VlanTrunkNetwork',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VOLUME_MODE = {
|
export const VOLUME_MODE = {
|
||||||
BLOCK: 'Block',
|
BLOCK: 'Block',
|
||||||
FILE_SYSTEM: 'Filesystem'
|
FILE_SYSTEM: 'Filesystem'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ACCESS_MODE = {
|
||||||
|
READ_WRITE_MANY: 'ReadWriteMany',
|
||||||
|
READ_WRITE_ONCE: 'ReadWriteOnce',
|
||||||
|
READ_ONLY_MANY: 'ReadOnlyMany',
|
||||||
|
READ_WRITE_ONCE_POD: 'ReadWriteOncePod',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NETWORK_PROTOCOL = {
|
||||||
|
IPv4: 'IPv4',
|
||||||
|
IPv6: 'IPv6',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const INTERNAL_STORAGE_CLASS = {
|
||||||
|
VMSTATE_PERSISTENCE: 'vmstate-persistence',
|
||||||
|
LONGHORN_STATIC: 'longhorn-static',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const L2VLAN_MODE = {
|
||||||
|
ACCESS: 'access',
|
||||||
|
TRUNK: 'trunk',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const VMIMPORT_SOURCE_PROVIDER = {
|
||||||
|
VMWARE: 'vmware',
|
||||||
|
OPENSTACK: 'openstack',
|
||||||
|
OVA: 'ova',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const VMIMPORT_SOURCE_KINDS = {
|
||||||
|
VMWARE: 'VmwareSource',
|
||||||
|
OPENSTACK: 'OpenstackSource',
|
||||||
|
OVA: 'OvaSource',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CDI_POPULATOR_KIND = {
|
||||||
|
VOLUME_IMPORT_SOURCE: 'VolumeImportSource',
|
||||||
|
VOLUME_CLONE_SOURCE: 'VolumeCloneSource',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FILESYSTEM_SOURCE_TYPE = {
|
||||||
|
CONFIGMAP: 'configmap',
|
||||||
|
SECRET: 'secret',
|
||||||
|
SERVICEACCOUNT: 'serviceaccount',
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
<script>
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import ResourceTabs from '@shell/components/form/ResourceTabs';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import SortableTable from '@shell/components/SortableTable';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
|
||||||
|
components: {
|
||||||
|
ResourceTabs,
|
||||||
|
Tab,
|
||||||
|
SortableTable,
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
headers() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'profileName',
|
||||||
|
labelKey: 'harvester.migconfiguration.tableHeaders.profileName',
|
||||||
|
value: 'name',
|
||||||
|
width: 75,
|
||||||
|
sort: 'name',
|
||||||
|
dashIfEmpty: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'vGPUID',
|
||||||
|
labelKey: 'harvester.migconfiguration.tableHeaders.vGPUID',
|
||||||
|
value: 'vGPUID',
|
||||||
|
width: 75,
|
||||||
|
sort: 'vGPUID',
|
||||||
|
dashIfEmpty: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'available',
|
||||||
|
labelKey: 'harvester.migconfiguration.tableHeaders.available',
|
||||||
|
value: 'available',
|
||||||
|
width: 75,
|
||||||
|
sort: 'available',
|
||||||
|
align: 'center',
|
||||||
|
dashIfEmpty: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'requested',
|
||||||
|
labelKey: 'harvester.migconfiguration.tableHeaders.requested',
|
||||||
|
value: 'requested',
|
||||||
|
width: 75,
|
||||||
|
sort: 'requested',
|
||||||
|
align: 'center',
|
||||||
|
dashIfEmpty: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'total',
|
||||||
|
labelKey: 'harvester.migconfiguration.tableHeaders.total',
|
||||||
|
value: 'total',
|
||||||
|
width: 75,
|
||||||
|
sort: 'total',
|
||||||
|
align: 'center',
|
||||||
|
dashIfEmpty: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
rows() {
|
||||||
|
let out = (this.value?.status?.profileStatus || []).map((profile) => {
|
||||||
|
const {
|
||||||
|
id, name, total, available
|
||||||
|
} = profile;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
total,
|
||||||
|
available,
|
||||||
|
vGPUID: profile.vGPUID?.join(', ') || '',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
out = out.map((row) => {
|
||||||
|
const requested = this.value?.spec?.profileSpec.find((p) => p.id === row.id)?.requested || 0;
|
||||||
|
|
||||||
|
return { ...row, requested };
|
||||||
|
});
|
||||||
|
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ResourceTabs
|
||||||
|
:value="value"
|
||||||
|
:need-events="false"
|
||||||
|
:need-related="false"
|
||||||
|
:mode="mode"
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
name="Profile Status"
|
||||||
|
:label="t('harvester.migconfiguration.profileStatus')"
|
||||||
|
>
|
||||||
|
<SortableTable
|
||||||
|
:headers="headers"
|
||||||
|
:rows="rows"
|
||||||
|
key-field="condition"
|
||||||
|
default-sort-by="condition"
|
||||||
|
:table-actions="false"
|
||||||
|
:row-actions="false"
|
||||||
|
:search="false"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
</ResourceTabs>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
<script>
|
||||||
|
import LabelValue from '@shell/components/LabelValue';
|
||||||
|
import { HCI } from '../../types';
|
||||||
|
import { DOC } from '../../config/doc-links';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterHugepages',
|
||||||
|
components: { LabelValue },
|
||||||
|
|
||||||
|
props: {
|
||||||
|
node: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
docsTransparentHugepagesLink() {
|
||||||
|
return DOC.TRANSPARENT_HUGEPAGES;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
const hash = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.HUGEPAGES });
|
||||||
|
|
||||||
|
this.hugepages = hash.find((node) => {
|
||||||
|
return node.id === this.node.id;
|
||||||
|
}) || {};
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return { hugepages: {} };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<template v-if="hugepages.status">
|
||||||
|
<h2>{{ t('harvester.host.hugepages.meminfo') }}</h2>
|
||||||
|
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.host.hugepages.status.anon')"
|
||||||
|
:value="hugepages.status.meminfo.anonHugePages"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.host.hugepages.status.size')"
|
||||||
|
:value="hugepages.status.meminfo.hugepageSize"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-3">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.host.hugepages.status.total')"
|
||||||
|
:value="hugepages.status.meminfo.hugePagesTotal"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-3">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.host.hugepages.status.free')"
|
||||||
|
:value="hugepages.status.meminfo.hugePagesFree"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-3">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.host.hugepages.status.rsvd')"
|
||||||
|
:value="hugepages.status.meminfo.hugePagesRsvd"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-3">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.host.hugepages.status.surp')"
|
||||||
|
:value="hugepages.status.meminfo.hugePagesSurp"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<hr class="divider" />
|
||||||
|
<h3>
|
||||||
|
<t
|
||||||
|
k="harvester.host.hugepages.transparent.title"
|
||||||
|
:raw="true"
|
||||||
|
:url="docsTransparentHugepagesLink"
|
||||||
|
/>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.host.hugepages.transparent.enabled')"
|
||||||
|
:value="hugepages.spec.transparent.enabled"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.host.hugepages.transparent.shmemEnabled')"
|
||||||
|
:value="hugepages.spec.transparent.shmemEnabled"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.host.hugepages.transparent.defrag')"
|
||||||
|
:value="hugepages.spec.transparent.defrag"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -27,15 +27,11 @@ export default {
|
|||||||
await allHash({
|
await allHash({
|
||||||
vms: this.$store.dispatch('harvester/findAll', { type: HCI.VM }),
|
vms: this.$store.dispatch('harvester/findAll', { type: HCI.VM }),
|
||||||
vmis: this.$store.dispatch('harvester/findAll', { type: HCI.VMI }),
|
vmis: this.$store.dispatch('harvester/findAll', { type: HCI.VMI }),
|
||||||
allClusterNetwork: this.$store.dispatch('harvester/findAll', { type: HCI.CLUSTER_NETWORK }),
|
vmims: this.$store.dispatch('harvester/findAll', { type: HCI.VMIM }),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
allClusterNetwork() {
|
|
||||||
return this.$store.getters['harvester/all'](HCI.CLUSTER_NETWORK);
|
|
||||||
},
|
|
||||||
|
|
||||||
rows() {
|
rows() {
|
||||||
const vms = this.$store.getters['harvester/all'](HCI.VM);
|
const vms = this.$store.getters['harvester/all'](HCI.VM);
|
||||||
|
|
||||||
@@ -47,20 +43,26 @@ export default {
|
|||||||
STATE,
|
STATE,
|
||||||
NAME,
|
NAME,
|
||||||
{
|
{
|
||||||
name: 'vmCPU',
|
name: 'CPU',
|
||||||
labelKey: 'tableHeaders.cpu',
|
label: 'CPU',
|
||||||
search: false,
|
sort: ['displayCPU'],
|
||||||
sort: ['spec.template.spec.domain.cpu.cores'],
|
value: 'displayCPU',
|
||||||
value: 'spec.template.spec.domain.cpu.cores',
|
align: 'center',
|
||||||
width: 120
|
dashIfEmpty: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'vmRAM',
|
name: 'Memory',
|
||||||
labelKey: 'glance.memory',
|
value: 'displayMemory',
|
||||||
search: false,
|
sort: ['memorySort'],
|
||||||
sort: ['memorySort'],
|
align: 'center',
|
||||||
value: 'spec.template.spec.domain.resources.limits.memory',
|
labelKey: 'tableHeaders.memory',
|
||||||
width: 120
|
formatter: 'Si',
|
||||||
|
formatterOpts: {
|
||||||
|
opts: {
|
||||||
|
increment: 1024, addSuffix: true, maxExponent: 3, minExponent: 3, suffix: 'i',
|
||||||
|
},
|
||||||
|
needParseSi: true
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'ip',
|
name: 'ip',
|
||||||
@@ -102,7 +104,6 @@ export default {
|
|||||||
<HarvesterVmState
|
<HarvesterVmState
|
||||||
class="vmstate"
|
class="vmstate"
|
||||||
:row="scope.row"
|
:row="scope.row"
|
||||||
:all-cluster-network="allClusterNetwork"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import Instance from './VirtualMachineInstance';
|
|||||||
import Disk from './HarvesterHostDisk';
|
import Disk from './HarvesterHostDisk';
|
||||||
import VlanStatus from './VlanStatus';
|
import VlanStatus from './VlanStatus';
|
||||||
import HarvesterKsmtuned from './HarvesterKsmtuned.vue';
|
import HarvesterKsmtuned from './HarvesterKsmtuned.vue';
|
||||||
|
import HarvesterHugepages from './HarvesterHugepages.vue';
|
||||||
import HarvesterSeeder from './HarvesterSeeder';
|
import HarvesterSeeder from './HarvesterSeeder';
|
||||||
|
|
||||||
const LONGHORN_SYSTEM = 'longhorn-system';
|
const LONGHORN_SYSTEM = 'longhorn-system';
|
||||||
@@ -46,6 +47,7 @@ export default {
|
|||||||
VlanStatus,
|
VlanStatus,
|
||||||
LabelValue,
|
LabelValue,
|
||||||
HarvesterKsmtuned,
|
HarvesterKsmtuned,
|
||||||
|
HarvesterHugepages,
|
||||||
Loading,
|
Loading,
|
||||||
SortableTable,
|
SortableTable,
|
||||||
HarvesterSeeder,
|
HarvesterSeeder,
|
||||||
@@ -209,6 +211,12 @@ export default {
|
|||||||
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.KSTUNED);
|
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.KSTUNED);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
hasHugepagesSchema() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.HUGEPAGES);
|
||||||
|
},
|
||||||
|
|
||||||
hasBlockDevicesSchema() {
|
hasBlockDevicesSchema() {
|
||||||
return !!this.$store.getters['harvester/schemaFor'](HCI.BLOCK_DEVICE);
|
return !!this.$store.getters['harvester/schemaFor'](HCI.BLOCK_DEVICE);
|
||||||
},
|
},
|
||||||
@@ -468,6 +476,16 @@ export default {
|
|||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
|
<Tab
|
||||||
|
v-if="hasHugepagesSchema"
|
||||||
|
name="hugepages"
|
||||||
|
:weight="0"
|
||||||
|
:show-header="false"
|
||||||
|
:label="t('harvester.host.tabs.hugepages')"
|
||||||
|
>
|
||||||
|
<HarvesterHugepages :node="value" />
|
||||||
|
</Tab>
|
||||||
|
|
||||||
<Tab
|
<Tab
|
||||||
v-if="seederEnabled"
|
v-if="seederEnabled"
|
||||||
name="seeder"
|
name="seeder"
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export default {
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col span-6 mb-20">
|
<div class="col span-6 mb-20">
|
||||||
<LabelValue
|
<LabelValue
|
||||||
:name="t('harvester.schedule.cron')"
|
:name="t('harvester.schedule.cron.label')"
|
||||||
:value="cronExpression"
|
:value="cronExpression"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ export default {
|
|||||||
:resource="value"
|
:resource="value"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:apply-hooks="applyHooks"
|
:apply-hooks="applyHooks"
|
||||||
|
@error="e=>errors=e"
|
||||||
>
|
>
|
||||||
<Tabbed
|
<Tabbed
|
||||||
v-if="spec"
|
v-if="spec"
|
||||||
@@ -171,6 +172,9 @@ export default {
|
|||||||
:cpu="cpu"
|
:cpu="cpu"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:memory="memory"
|
:memory="memory"
|
||||||
|
:max-cpu="maxCpu"
|
||||||
|
:max-memory="maxMemory"
|
||||||
|
:enable-hot-plug="cpuMemoryHotplugEnabled"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|||||||
@@ -118,6 +118,10 @@ export default {
|
|||||||
imageName() {
|
imageName() {
|
||||||
return this.value?.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_NAME] || '-';
|
return this.value?.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_NAME] || '-';
|
||||||
},
|
},
|
||||||
|
|
||||||
|
sourceType() {
|
||||||
|
return this.value?.spec?.sourceType;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -252,6 +256,16 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.image.source')"
|
||||||
|
:value="sourceType"
|
||||||
|
class="mb-20"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="errorMessage !== '-'"
|
v-if="errorMessage !== '-'"
|
||||||
class="row"
|
class="row"
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ export default {
|
|||||||
:resource="value"
|
:resource="value"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:apply-hooks="applyHooks"
|
:apply-hooks="applyHooks"
|
||||||
|
@error="e=>errors=e"
|
||||||
>
|
>
|
||||||
<Tabbed
|
<Tabbed
|
||||||
v-if="spec"
|
v-if="spec"
|
||||||
@@ -171,6 +172,9 @@ export default {
|
|||||||
:cpu="cpu"
|
:cpu="cpu"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:memory="memory"
|
:memory="memory"
|
||||||
|
:max-cpu="maxCpu"
|
||||||
|
:max-memory="maxMemory"
|
||||||
|
:enable-hot-plug="cpuMemoryHotplugEnabled"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="row mb-10">
|
<div class="row mb-10">
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+3
-2
@@ -5,6 +5,7 @@ import CreateEditView from '@shell/mixins/create-edit-view';
|
|||||||
import HarvesterIpAddress from '../../../formatters/HarvesterIpAddress';
|
import HarvesterIpAddress from '../../../formatters/HarvesterIpAddress';
|
||||||
import VMConsoleBar from '../../../components/VMConsoleBar';
|
import VMConsoleBar from '../../../components/VMConsoleBar';
|
||||||
import { HCI } from '../../../types';
|
import { HCI } from '../../../types';
|
||||||
|
import { getVmCPUMemoryValues } from '../../../utils/cpuMemory';
|
||||||
|
|
||||||
const UNDEFINED = 'n/a';
|
const UNDEFINED = 'n/a';
|
||||||
|
|
||||||
@@ -91,9 +92,9 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
flavor() {
|
flavor() {
|
||||||
const domain = this.value?.spec?.template?.spec?.domain;
|
const { cpu, memory } = getVmCPUMemoryValues(this.value);
|
||||||
|
|
||||||
return `${ domain.cpu?.cores } vCPU , ${ domain.resources?.limits?.memory } ${ this.t('harvester.virtualMachine.input.memory') }`;
|
return `${ cpu } vCPU , ${ memory } ${ this.t('harvester.virtualMachine.input.memory') }`;
|
||||||
},
|
},
|
||||||
|
|
||||||
kernelRelease() {
|
kernelRelease() {
|
||||||
|
|||||||
+25
@@ -17,6 +17,13 @@ export default {
|
|||||||
default: () => {
|
default: () => {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
vmimResource: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
default: () => {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -25,6 +32,12 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
liveMigrationProgressEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('liveMigrationProgress');
|
||||||
|
},
|
||||||
|
migrationPhase() {
|
||||||
|
return this.vmimResource?.status?.phase || 'N/A';
|
||||||
|
},
|
||||||
migrationState() {
|
migrationState() {
|
||||||
return this.localResource?.status?.migrationState;
|
return this.localResource?.status?.migrationState;
|
||||||
},
|
},
|
||||||
@@ -58,6 +71,18 @@ export default {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
<div
|
||||||
|
v-if="liveMigrationProgressEnabled"
|
||||||
|
class="row mb-20"
|
||||||
|
>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabelValue
|
||||||
|
:name="t('harvester.virtualMachine.detail.details.phase')"
|
||||||
|
:value="migrationPhase"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row mb-20">
|
<div class="row mb-20">
|
||||||
<div class="col span-6">
|
<div class="col span-6">
|
||||||
<LabelValue
|
<LabelValue
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ import { formatSi } from '@shell/utils/units';
|
|||||||
|
|
||||||
const VM_METRICS_DETAIL_URL = '/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/d/harvester-vm-detail-1/vm-info-detail?orgId=1';
|
const VM_METRICS_DETAIL_URL = '/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/d/harvester-vm-detail-1/vm-info-detail?orgId=1';
|
||||||
|
|
||||||
|
const VM_MIGRATION_DETAIL_URL = '/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/d/harvester-vm-migration-details-1/harvester-vm-migration-details?orgId=1';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'VMIDetailsPage',
|
name: 'VMIDetailsPage',
|
||||||
|
|
||||||
@@ -63,6 +65,7 @@ export default {
|
|||||||
hasResourceQuotaSchema: false,
|
hasResourceQuotaSchema: false,
|
||||||
switchToCloud: false,
|
switchToCloud: false,
|
||||||
VM_METRICS_DETAIL_URL,
|
VM_METRICS_DETAIL_URL,
|
||||||
|
VM_MIGRATION_DETAIL_URL,
|
||||||
showVmMetrics: false,
|
showVmMetrics: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -78,6 +81,7 @@ export default {
|
|||||||
events: this.$store.dispatch(`${ inStore }/findAll`, { type: EVENT }),
|
events: this.$store.dispatch(`${ inStore }/findAll`, { type: EVENT }),
|
||||||
allSSHs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SSH }),
|
allSSHs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SSH }),
|
||||||
vmis: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMI }),
|
vmis: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMI }),
|
||||||
|
vmims: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMIM }),
|
||||||
restore: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.RESTORE }),
|
restore: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.RESTORE }),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -131,6 +135,39 @@ export default {
|
|||||||
return this.$store.getters[`${ inStore }/all`](EVENT);
|
return this.$store.getters[`${ inStore }/all`](EVENT);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
vmim() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const vmimList = this.$store.getters[`${ inStore }/all`](HCI.VMIM) || [];
|
||||||
|
|
||||||
|
const vmiName = this.vmi?.name || '';
|
||||||
|
|
||||||
|
// filter the corresponding vmims by vmim.spec.vmiName and find the latest one by creationTimestamp
|
||||||
|
const vmim = vmimList.filter((VMIM) => VMIM?.spec?.vmiName === vmiName).sort((a, b) => {
|
||||||
|
if (a?.metadata?.creationTimestamp > b?.metadata?.creationTimestamp) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
return vmim.length > 0 && vmim[0] ? vmim[0] : null;
|
||||||
|
},
|
||||||
|
|
||||||
|
migrationEvents() {
|
||||||
|
const migrationVMName = this.vmim?.metadata.name || '';
|
||||||
|
|
||||||
|
if (migrationVMName === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.allEvents.filter((e) => {
|
||||||
|
const { creationTimestamp } = this.value?.metadata || {};
|
||||||
|
const involvedName = e?.involvedObject?.name;
|
||||||
|
|
||||||
|
return involvedName === migrationVMName && e.firstTimestamp >= creationTimestamp;
|
||||||
|
}).sort((a, b) => a.lastTimestamp > b.lastTimestamp);
|
||||||
|
},
|
||||||
|
|
||||||
events() {
|
events() {
|
||||||
return this.allEvents.filter((e) => {
|
return this.allEvents.filter((e) => {
|
||||||
const { name, creationTimestamp } = this.value?.metadata || {};
|
const { name, creationTimestamp } = this.value?.metadata || {};
|
||||||
@@ -138,7 +175,6 @@ export default {
|
|||||||
const pvcName = this.value.persistentVolumeClaimName || [];
|
const pvcName = this.value.persistentVolumeClaimName || [];
|
||||||
|
|
||||||
const involvedName = e?.involvedObject?.name;
|
const involvedName = e?.involvedObject?.name;
|
||||||
|
|
||||||
const matchPVC = pvcName.find((name) => name === involvedName);
|
const matchPVC = pvcName.find((name) => name === involvedName);
|
||||||
|
|
||||||
return (involvedName === name || involvedName === podName || matchPVC) && e.firstTimestamp >= creationTimestamp;
|
return (involvedName === name || involvedName === podName || matchPVC) && e.firstTimestamp >= creationTimestamp;
|
||||||
@@ -157,6 +193,10 @@ export default {
|
|||||||
vm: this.value.name
|
vm: this.value.name
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
liveMigrationProgressEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('liveMigrationProgress');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@@ -173,6 +213,7 @@ export default {
|
|||||||
const diskRows = this.getDiskRows(neu);
|
const diskRows = this.getDiskRows(neu);
|
||||||
|
|
||||||
this['diskRows'] = diskRows;
|
this['diskRows'] = diskRows;
|
||||||
|
this['networkRows'] = this.getNetworkRows(neu, { fromTemplate: false, init: false });
|
||||||
},
|
},
|
||||||
deep: true
|
deep: true
|
||||||
}
|
}
|
||||||
@@ -225,6 +266,7 @@ export default {
|
|||||||
<Network
|
<Network
|
||||||
v-model:value="networkRows"
|
v-model:value="networkRows"
|
||||||
mode="view"
|
mode="view"
|
||||||
|
:vm="value"
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
@@ -346,6 +388,20 @@ export default {
|
|||||||
<Migration
|
<Migration
|
||||||
:value="value"
|
:value="value"
|
||||||
:vmi-resource="vmi"
|
:vmi-resource="vmi"
|
||||||
|
:vmim-resource="vmim"
|
||||||
|
/>
|
||||||
|
<DashboardMetrics
|
||||||
|
v-if="showVmMetrics && liveMigrationProgressEnabled"
|
||||||
|
:detail-url="VM_MIGRATION_DETAIL_URL"
|
||||||
|
graph-height="640px"
|
||||||
|
:has-summary-and-detail="false"
|
||||||
|
:vars="graphVars"
|
||||||
|
class="mb-30"
|
||||||
|
/>
|
||||||
|
<Events
|
||||||
|
v-if="liveMigrationProgressEnabled"
|
||||||
|
:resource="vmi"
|
||||||
|
:events="migrationEvents"
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
<script>
|
<script>
|
||||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import { runStrategies as runStrategyOptions } from '../config/harvester-map';
|
||||||
import { Card } from '@components/Card';
|
import { Card } from '@components/Card';
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import { Checkbox } from '@components/Form/Checkbox';
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
import AsyncButton from '@shell/components/AsyncButton';
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CloneVMModal',
|
name: 'CloneVMModal',
|
||||||
@@ -14,7 +15,7 @@ export default {
|
|||||||
emits: ['close'],
|
emits: ['close'],
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
AsyncButton, Banner, Checkbox, Card, LabeledInput
|
AsyncButton, Banner, Checkbox, Card, LabeledInput, LabeledSelect
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
@@ -26,9 +27,11 @@ export default {
|
|||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
name: '',
|
name: '',
|
||||||
cloneData: true,
|
cloneData: true,
|
||||||
errors: []
|
errors: [],
|
||||||
|
runStrategy: runStrategyOptions[1],
|
||||||
|
runStrategyOptions
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -38,6 +41,9 @@ export default {
|
|||||||
actionResource() {
|
actionResource() {
|
||||||
return this.resources[0];
|
return this.resources[0];
|
||||||
},
|
},
|
||||||
|
vmCloneRunStrategyEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('vmCloneRunStrategy');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@@ -58,7 +64,8 @@ export default {
|
|||||||
|
|
||||||
// deep clone
|
// deep clone
|
||||||
try {
|
try {
|
||||||
const res = await this.actionResource.doAction('clone', { targetVm: this.name }, {}, false);
|
const payload = this.vmCloneRunStrategyEnabled ? { targetVm: this.name, runStrategy: this.runStrategy } : { targetVm: this.name };
|
||||||
|
const res = await this.actionResource.doAction('clone', payload, {}, false);
|
||||||
|
|
||||||
if (res._status === 200 || res._status === 204) {
|
if (res._status === 200 || res._status === 204) {
|
||||||
this.$store.dispatch('growl/success', {
|
this.$store.dispatch('growl/success', {
|
||||||
@@ -106,6 +113,19 @@ export default {
|
|||||||
:label="t('harvester.modal.cloneVM.name')"
|
:label="t('harvester.modal.cloneVM.name')"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
<LabeledSelect
|
||||||
|
v-if="vmCloneRunStrategyEnabled"
|
||||||
|
v-model:value="runStrategy"
|
||||||
|
label-key="harvester.virtualMachine.runStrategy"
|
||||||
|
:options="runStrategyOptions"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
<Banner
|
||||||
|
v-for="(err, i) in errors"
|
||||||
|
:key="i"
|
||||||
|
color="error"
|
||||||
|
:label="err"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template
|
<template
|
||||||
@@ -127,11 +147,6 @@ export default {
|
|||||||
@click="create"
|
@click="create"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Banner
|
|
||||||
v-for="(err, i) in errors"
|
|
||||||
:key="i"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</Card>
|
</Card>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export default {
|
|||||||
plusMore() {
|
plusMore() {
|
||||||
const remaining = this.resources.length - this.names.length;
|
const remaining = this.resources.length - this.names.length;
|
||||||
|
|
||||||
return this.t('dialog.confirmExecution.andOthers', { count: remaining });
|
return this.t('dialog.confirmExecution.andOthers', { count: remaining }, true);
|
||||||
},
|
},
|
||||||
|
|
||||||
type() {
|
type() {
|
||||||
@@ -98,9 +98,9 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
escapeHtml,
|
escapeHtml,
|
||||||
|
|
||||||
close() {
|
close(data) {
|
||||||
this.errors = [];
|
this.errors = [];
|
||||||
this.$emit('close');
|
this.$emit('close', data);
|
||||||
},
|
},
|
||||||
|
|
||||||
async apply(buttonDone) {
|
async apply(buttonDone) {
|
||||||
@@ -109,7 +109,7 @@ export default {
|
|||||||
await resource.doActionGrowl(this.modalData.action, {});
|
await resource.doActionGrowl(this.modalData.action, {});
|
||||||
}
|
}
|
||||||
buttonDone(true);
|
buttonDone(true);
|
||||||
this.close();
|
this.close({ performCallback: true, clearTableSelection: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.errors = exceptionToErrorsArray(e);
|
this.errors = exceptionToErrorsArray(e);
|
||||||
buttonDone(false);
|
buttonDone(false);
|
||||||
@@ -138,6 +138,8 @@ export default {
|
|||||||
<Banner
|
<Banner
|
||||||
v-for="(error, i) in errors"
|
v-for="(error, i) in errors"
|
||||||
:key="i"
|
:key="i"
|
||||||
|
:label="error"
|
||||||
|
color="error"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export default {
|
|||||||
resources: {
|
resources: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
@@ -42,8 +42,25 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
...mapState('action-menu', ['modalData']),
|
...mapState('action-menu', ['modalData']),
|
||||||
|
|
||||||
warningMessageKey() {
|
title() {
|
||||||
return this.modalData.warningMessageKey;
|
return this.modalData?.title || 'dialog.promptRemove.title';
|
||||||
|
},
|
||||||
|
|
||||||
|
formattedType() {
|
||||||
|
return this.type.toLowerCase();
|
||||||
|
},
|
||||||
|
|
||||||
|
warningMessage() {
|
||||||
|
if (this.modalData?.warningMessage) return this.modalData.warningMessage;
|
||||||
|
|
||||||
|
const isPlural = this.type.endsWith('s');
|
||||||
|
const thisOrThese = isPlural ? 'these' : 'this';
|
||||||
|
const defaultMessage = this.t('dialog.promptRemove.warningMessage', {
|
||||||
|
type: this.formattedType,
|
||||||
|
thisOrThese,
|
||||||
|
});
|
||||||
|
|
||||||
|
return defaultMessage;
|
||||||
},
|
},
|
||||||
|
|
||||||
names() {
|
names() {
|
||||||
@@ -70,6 +87,12 @@ export default {
|
|||||||
return this.resources[0].nameDisplay;
|
return this.resources[0].nameDisplay;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
needConfirmation() {
|
||||||
|
const { needConfirmation = true } = this.modalData ;
|
||||||
|
|
||||||
|
return needConfirmation === true;
|
||||||
|
},
|
||||||
|
|
||||||
plusMore() {
|
plusMore() {
|
||||||
const remaining = this.resources.length - this.names.length;
|
const remaining = this.resources.length - this.names.length;
|
||||||
|
|
||||||
@@ -97,11 +120,15 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
deleteDisabled() {
|
deleteDisabled() {
|
||||||
|
if (!this.needConfirmation) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return this.confirmName !== this.nameToMatch;
|
return this.confirmName !== this.nameToMatch;
|
||||||
},
|
},
|
||||||
|
|
||||||
protip() {
|
protip() {
|
||||||
return this.t('promptRemove.protip', { alternateLabel });
|
return this.t('dialog.promptRemove.protip', { alternateLabel });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -118,6 +145,7 @@ export default {
|
|||||||
try {
|
try {
|
||||||
for (const resource of this.resources) {
|
for (const resource of this.resources) {
|
||||||
await resource.remove();
|
await resource.remove();
|
||||||
|
if (this.modalData?.extraActionAfterRemove) await this.modalData.extraActionAfterRemove();
|
||||||
}
|
}
|
||||||
buttonDone(true);
|
buttonDone(true);
|
||||||
this.close();
|
this.close();
|
||||||
@@ -137,35 +165,45 @@ export default {
|
|||||||
>
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
<h4 class="text-default-text">
|
<h4 class="text-default-text">
|
||||||
{{ t('promptRemove.title') }}
|
{{ t(title, { type }, true) }}
|
||||||
</h4>
|
</h4>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #body>
|
<template #body>
|
||||||
<div class="pl-10 pr-10">
|
<div class="pl-10 pr-10">
|
||||||
<span
|
<span
|
||||||
v-clean-html="t(warningMessageKey, { type, names: resourceNames }, true)"
|
v-clean-html="warningMessage"
|
||||||
></span>
|
></span>
|
||||||
|
|
||||||
<div class="mt-10 mb-10">
|
<div
|
||||||
<span
|
v-if="needConfirmation"
|
||||||
v-clean-html="t('promptRemove.confirmName', { nameToMatch: escapeHtml(nameToMatch) }, true)"
|
class="mt-20"
|
||||||
></span>
|
>
|
||||||
</div>
|
<div class="mt-10 mb-10">
|
||||||
<div class="mb-10">
|
<span
|
||||||
<CopyToClipboardText :text="nameToMatch" />
|
v-clean-html="t('dialog.promptRemove.confirmName', {
|
||||||
</div>
|
type: formattedType,
|
||||||
<input
|
nameToMatch: escapeHtml(nameToMatch)
|
||||||
id="confirm"
|
}, true)"
|
||||||
v-model="confirmName"
|
></span>
|
||||||
type="text"
|
</div>
|
||||||
/>
|
<div class="mb-10">
|
||||||
<div class="text-info mt-20">
|
<CopyToClipboardText :text="nameToMatch" />
|
||||||
{{ protip }}
|
</div>
|
||||||
|
<input
|
||||||
|
id="confirm"
|
||||||
|
v-model="confirmName"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
<div class="text-info mt-20">
|
||||||
|
{{ protip }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Banner
|
<Banner
|
||||||
v-for="(error, i) in errors"
|
v-for="(error, i) in errors"
|
||||||
:key="i"
|
:key="i"
|
||||||
|
:label="error"
|
||||||
|
color="error"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,150 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
|
|
||||||
import { Card } from '@components/Card';
|
|
||||||
import { Banner } from '@components/Banner';
|
|
||||||
import { Checkbox } from '@components/Form/Checkbox';
|
|
||||||
import AsyncButton from '@shell/components/AsyncButton';
|
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'CloneVMModal',
|
|
||||||
|
|
||||||
emits: ['close'],
|
|
||||||
|
|
||||||
components: {
|
|
||||||
AsyncButton, Banner, Checkbox, Card, LabeledInput
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
|
||||||
resources: {
|
|
||||||
type: Array,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
name: '',
|
|
||||||
cloneData: true,
|
|
||||||
errors: []
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
...mapGetters({ t: 'i18n/t' }),
|
|
||||||
|
|
||||||
actionResource() {
|
|
||||||
return this.resources[0];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
close() {
|
|
||||||
this.name = '';
|
|
||||||
this.$emit('close');
|
|
||||||
},
|
|
||||||
|
|
||||||
async create(buttonCb) {
|
|
||||||
// shadow clone
|
|
||||||
if (!this.cloneData) {
|
|
||||||
this.resources[0].goToClone();
|
|
||||||
buttonCb(false);
|
|
||||||
this.close();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// deep clone
|
|
||||||
try {
|
|
||||||
const res = await this.actionResource.doAction('clone', { targetVm: this.name }, {}, false);
|
|
||||||
|
|
||||||
if (res._status === 200 || res._status === 204) {
|
|
||||||
this.$store.dispatch('growl/success', {
|
|
||||||
title: this.t('harvester.notification.title.succeed'),
|
|
||||||
message: this.t('harvester.modal.cloneVM.message.success', { name: this.name })
|
|
||||||
}, { root: true });
|
|
||||||
|
|
||||||
this.close();
|
|
||||||
buttonCb(true);
|
|
||||||
} else {
|
|
||||||
const error = [res?.data] || exceptionToErrorsArray(res);
|
|
||||||
|
|
||||||
this['errors'] = error;
|
|
||||||
buttonCb(false);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
const error = err?.data || err;
|
|
||||||
const message = exceptionToErrorsArray(error);
|
|
||||||
|
|
||||||
this['errors'] = message;
|
|
||||||
buttonCb(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Card :show-highlight-border="false">
|
|
||||||
<template #title>
|
|
||||||
{{ t('harvester.modal.cloneVM.title') }}
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #body>
|
|
||||||
<Checkbox
|
|
||||||
v-model:value="cloneData"
|
|
||||||
class="mb-10"
|
|
||||||
label-key="harvester.modal.cloneVM.type"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LabeledInput
|
|
||||||
v-show="cloneData"
|
|
||||||
v-model:value="name"
|
|
||||||
class="mb-20"
|
|
||||||
:label="t('harvester.modal.cloneVM.name')"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #actions>
|
|
||||||
<div class="actions">
|
|
||||||
<div class="buttons">
|
|
||||||
<button
|
|
||||||
class="btn role-secondary mr-10"
|
|
||||||
@click="close"
|
|
||||||
>
|
|
||||||
{{ t('generic.cancel') }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<AsyncButton
|
|
||||||
mode="create"
|
|
||||||
:action-label="cloneData ? t('harvester.modal.cloneVM.action.create') : t('harvester.modal.cloneVM.action.clone')"
|
|
||||||
:disabled="cloneData && !name"
|
|
||||||
@click="create"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Banner
|
|
||||||
v-for="(err, i) in errors"
|
|
||||||
:key="i"
|
|
||||||
color="error"
|
|
||||||
:label="err"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.actions {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttons {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
import { Card } from '@components/Card';
|
import { Card } from '@components/Card';
|
||||||
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
import AsyncButton from '@shell/components/AsyncButton';
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
import { escapeHtml } from '@shell/utils/string';
|
import { escapeHtml } from '@shell/utils/string';
|
||||||
import { HCI } from '../types';
|
import { HCI } from '../types';
|
||||||
|
import { getHarvesterUserName } from '../utils/auth';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterEnablePciPassthrough',
|
name: 'HarvesterEnablePciPassthrough',
|
||||||
@@ -12,7 +15,9 @@ export default {
|
|||||||
|
|
||||||
components: {
|
components: {
|
||||||
AsyncButton,
|
AsyncButton,
|
||||||
|
Banner,
|
||||||
Card,
|
Card,
|
||||||
|
Checkbox,
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
@@ -23,10 +28,16 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {};
|
return { disableResourcePooling: false };
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: { ...mapGetters({ t: 'i18n/t' }) },
|
computed: {
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
|
||||||
|
disableResourcePoolingEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('disableResourcePooling');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
close() {
|
close() {
|
||||||
@@ -34,16 +45,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async save(buttonCb) {
|
async save(buttonCb) {
|
||||||
// isSingleProduct == this is a standalone Harvester cluster
|
const userName = getHarvesterUserName(this.$store.getters);
|
||||||
const isSingleProduct = this.$store.getters['isSingleProduct'];
|
|
||||||
let userName = 'admin';
|
|
||||||
|
|
||||||
// if this is imported Harvester, there may be users other than 'admin
|
|
||||||
if (!isSingleProduct) {
|
|
||||||
const user = this.$store.getters['auth/v3User'];
|
|
||||||
|
|
||||||
userName = user?.username || user?.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < this.resources.length; i++) {
|
for (let i = 0; i < this.resources.length; i++) {
|
||||||
const actionResource = this.resources[i];
|
const actionResource = this.resources[i];
|
||||||
@@ -60,9 +62,10 @@ export default {
|
|||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
spec: {
|
spec: {
|
||||||
address: actionResource.status.address,
|
address: actionResource.status.address,
|
||||||
nodeName: actionResource.status.nodeName,
|
nodeName: actionResource.status.nodeName,
|
||||||
userName
|
userName,
|
||||||
|
disableResourcePooling: this.disableResourcePooling,
|
||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
|
|
||||||
@@ -93,7 +96,19 @@ export default {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #body>
|
<template #body>
|
||||||
{{ t('harvester.pci.enablePassthroughWarning') }}
|
<p class="mb-20">
|
||||||
|
{{ t('harvester.pci.enablePassthroughWarning') }}
|
||||||
|
</p>
|
||||||
|
<template v-if="disableResourcePoolingEnabled">
|
||||||
|
<Checkbox
|
||||||
|
v-model:value="disableResourcePooling"
|
||||||
|
label-key="harvester.pci.disableResourcePooling"
|
||||||
|
/>
|
||||||
|
<Banner
|
||||||
|
color="info"
|
||||||
|
:label="t('harvester.pci.disableResourcePoolingDescription')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #actions>
|
<template #actions>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user