mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-03 20:56:06 +08:00
Compare commits
315
Commits
v1.5.0-rc4
..
v1.6.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40d16d0bf3 | ||
|
|
3e013bc4fc | ||
|
|
d31979a91f | ||
|
|
3ef5a14567 | ||
|
|
1e0e2ea867 | ||
|
|
16e5fd3177 | ||
|
|
20696f6801 | ||
|
|
e5b487ebcb | ||
|
|
f058698fb5 | ||
|
|
208496d6f7 | ||
|
|
967966e852 | ||
|
|
f19809da04 | ||
|
|
bc3d9389f2 | ||
|
|
6fbdf5244e | ||
|
|
7b21f6bb2e | ||
|
|
0f6faed2dc | ||
|
|
021de41e68 | ||
|
|
65f4405d29 | ||
|
|
4bae62d046 | ||
|
|
4cc60e22a8 | ||
|
|
19b6842b21 | ||
|
|
fa78229d3a | ||
|
|
c3bfaf00ea | ||
|
|
1a176960bc | ||
|
|
e82ba9d9f5 | ||
|
|
f7fc001abd | ||
|
|
d1c676ee2c | ||
|
|
c562dd73ba | ||
|
|
1cd5e5ca7d | ||
|
|
1aaa8f71c4 | ||
|
|
003a09d0a9 | ||
|
|
f08e632aad | ||
|
|
df1d7001bc | ||
|
|
41f78c83a7 | ||
|
|
f1a53311ce | ||
|
|
e0c6e905a9 | ||
|
|
1852e434f1 | ||
|
|
77c085d3a7 | ||
|
|
9dbe7d9015 | ||
|
|
3f19494d98 | ||
|
|
296601d38c | ||
|
|
5d9ba584b8 | ||
|
|
eab4ba3d0f | ||
|
|
4e5320dc72 | ||
|
|
ba7092f400 | ||
|
|
eec7c3573d | ||
|
|
f0014cc627 | ||
|
|
9aa311a7e3 | ||
|
|
013d9f1d7e | ||
|
|
8c044ba44c | ||
|
|
7b8592e07d | ||
|
|
53d2e67b4f | ||
|
|
1d146a40bd | ||
|
|
11d6943596 | ||
|
|
7b754457ec | ||
|
|
2cb838eded | ||
|
|
bdcb34ab14 | ||
|
|
8daf44b3f6 | ||
|
|
8044e74102 | ||
|
|
264d65cc6d | ||
|
|
342a82c8e0 | ||
|
|
e7cd5cc4b0 | ||
|
|
cf405be71e | ||
|
|
911d30d7ba | ||
|
|
1b4bfa0962 | ||
|
|
d0f6f18a8a | ||
|
|
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 |
@@ -0,0 +1,6 @@
|
|||||||
|
addAssignees: author
|
||||||
|
addReviewers: true
|
||||||
|
numberOfReviewers: 0
|
||||||
|
reviewers:
|
||||||
|
- a110605
|
||||||
|
- houhoucoop
|
||||||
@@ -2,6 +2,7 @@ pull_request_rules:
|
|||||||
- name: Automatically open backport PR to release-harvester-v1.5
|
- name: Automatically open backport PR to release-harvester-v1.5
|
||||||
conditions:
|
conditions:
|
||||||
- base=main
|
- base=main
|
||||||
|
- label="require backport 1.5"
|
||||||
actions:
|
actions:
|
||||||
backport:
|
backport:
|
||||||
branches:
|
branches:
|
||||||
@@ -11,6 +12,19 @@ pull_request_rules:
|
|||||||
labels:
|
labels:
|
||||||
- "v1.5 backport PR"
|
- "v1.5 backport PR"
|
||||||
|
|
||||||
|
- name: Automatically open backport PR to release-harvester-v1.0
|
||||||
|
conditions:
|
||||||
|
- base=main
|
||||||
|
- label="require backport 1.0"
|
||||||
|
actions:
|
||||||
|
backport:
|
||||||
|
branches:
|
||||||
|
- "release-harvester-v1.0"
|
||||||
|
assignees:
|
||||||
|
- "{{ author }}"
|
||||||
|
labels:
|
||||||
|
- "v1.0 backport PR"
|
||||||
|
|
||||||
- name: Ask to resolve conflict
|
- name: Ask to resolve conflict
|
||||||
conditions:
|
conditions:
|
||||||
- conflict
|
- conflict
|
||||||
|
|||||||
@@ -2,18 +2,13 @@
|
|||||||
### Summary
|
### Summary
|
||||||
|
|
||||||
### PR Checklists
|
### PR Checklists
|
||||||
- Do we need to backport this PR change to the [Harvester Dashboard](https://github.com/harvester/dashboard)?
|
- Are backend engineers aware of UI changes ?
|
||||||
- [ ] Yes, the relevant PR is at:
|
|
||||||
- Are backend engineers aware of UI changes?
|
|
||||||
- [ ] Yes, the backend owner is:
|
- [ ] Yes, the backend owner is:
|
||||||
|
|
||||||
### Related Issue #
|
### Related Issue #
|
||||||
<!-- Define findings related to the feature or bug issue. -->
|
<!-- Define findings related to the feature or bug issue. -->
|
||||||
|
|
||||||
### Test screenshot/video
|
### Test screenshot or video
|
||||||
<!-- Attach screenshot or video of the changes and eventual comparison if you find it necessary -->
|
<!-- Attach screenshot or video of the changes and eventual comparison if you find it necessary -->
|
||||||
|
|
||||||
### Extra technical notes summary
|
|
||||||
<!-- Outline technical changes which may pass unobserved or may help to understand the process of solving the issue -->
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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,46 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": [
|
||||||
|
"config:recommended",
|
||||||
|
":semanticCommitTypeAll(deps)",
|
||||||
|
":semanticCommitScopeDisabled"
|
||||||
|
],
|
||||||
|
"baseBranches": [
|
||||||
|
"main",
|
||||||
|
"/^release-harvester-v\\d+\\.\\d+$/"
|
||||||
|
],
|
||||||
|
"automergeMajor": false,
|
||||||
|
"semanticCommits": "enabled",
|
||||||
|
"semanticCommitType": "deps",
|
||||||
|
"prHourlyLimit": 12,
|
||||||
|
"timezone": "Asia/Taipei",
|
||||||
|
"schedule": ["after 10am on sunday"],
|
||||||
|
"postUpdateOptions": ["yarnDedupeFewer"],
|
||||||
|
"packageRules": [
|
||||||
|
{
|
||||||
|
"matchUpdateTypes": ["major"],
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchPackagePatterns": ["vue", "vue-router", "vuex"],
|
||||||
|
"matchUpdateTypes": ["major"],
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchPackageNames": ["@rancher/shell"],
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchUpdateTypes": ["minor"],
|
||||||
|
"groupName": "minor dependencies",
|
||||||
|
"labels": ["minor-update"],
|
||||||
|
"reviewers": ["a110605", "houhoucoop"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchUpdateTypes": ["patch", "digest"],
|
||||||
|
"automerge": true,
|
||||||
|
"groupName": "patch digest dependencies",
|
||||||
|
"labels": ["patch-update", "automerge"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
name: "[PR Management] Auto Assign Reviewer & Assignee"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened, ready_for_review]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
auto-assign:
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: rancher/gh-issue-mgr/auto-assign-action@main
|
||||||
|
with:
|
||||||
|
configuration-path: .github/auto-assign-config.yaml
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
name: "[PR Management] Add Labels"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened, reopened]
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- 'release-harvester-v*'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
add-require-backport-label:
|
||||||
|
if: github.event.pull_request.draft == false &&
|
||||||
|
github.event.pull_request.base.ref == 'main'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.base_ref }}
|
||||||
|
|
||||||
|
- name: Fetch release branches and PR labels
|
||||||
|
id: fetch_info
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo="${{ github.repository }}"
|
||||||
|
pr_number=${{ github.event.pull_request.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 "should_label=false" >> "$GITHUB_OUTPUT"
|
||||||
|
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 "should_label=false" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
label="require backport/v${version}"
|
||||||
|
echo "should_label=true" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "backport_label=$label" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
pr_labels=$(gh pr view "$pr_number" --repo "$repo" --json labels --jq '.labels[].name' || echo "")
|
||||||
|
pr_labels_csv=$(echo "$pr_labels" | tr '\n' ',' | sed 's/,$//')
|
||||||
|
echo "pr_labels=$pr_labels_csv" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Add label if needed
|
||||||
|
if: steps.fetch_info.outputs.should_label == 'true' && !contains(steps.fetch_info.outputs.pr_labels, steps.fetch_info.outputs.backport_label)
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
echo "Adding label: ${{ steps.fetch_info.outputs.backport_label }}"
|
||||||
|
gh pr edit ${{ github.event.pull_request.number }} \
|
||||||
|
--repo ${{ github.repository }} \
|
||||||
|
--add-label "${{ steps.fetch_info.outputs.backport_label }}"
|
||||||
|
|
||||||
|
add-backport-label:
|
||||||
|
if: github.event.pull_request.draft == false &&
|
||||||
|
startsWith(github.event.pull_request.base.ref, 'release-harvester-v')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check conditions for backport label
|
||||||
|
id: check
|
||||||
|
run: |
|
||||||
|
IS_MERGIFY=$(echo '${{ github.event.pull_request.user.login }}' | grep -iq 'mergify' && echo true || echo false)
|
||||||
|
TARGET_BRANCH=${{ github.event.pull_request.base.ref }}
|
||||||
|
|
||||||
|
echo "IS_MERGIFY=$IS_MERGIFY" >> $GITHUB_OUTPUT
|
||||||
|
echo "TARGET_BRANCH=$TARGET_BRANCH" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Add label if needed
|
||||||
|
if: steps.check.outputs.IS_MERGIFY == 'true' && startsWith(steps.check.outputs.TARGET_BRANCH, 'release-harvester-v')
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
TARGET_BRANCH="${{ steps.check.outputs.TARGET_BRANCH }}"
|
||||||
|
version="${TARGET_BRANCH#release-harvester-v}"
|
||||||
|
label="backport/v${version}"
|
||||||
|
echo "Adding label $label"
|
||||||
|
gh pr edit ${{ github.event.pull_request.number }} \
|
||||||
|
--repo ${{ github.repository }} \
|
||||||
|
--add-label "$label"
|
||||||
@@ -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@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
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- 'release-harvester-v*'
|
- 'release-harvester-v*'
|
||||||
|
- 'main'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
setup-target-branch:
|
setup-target-branch:
|
||||||
@@ -17,14 +18,17 @@ jobs:
|
|||||||
- 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
|
||||||
@@ -128,7 +132,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@v1.7.0
|
||||||
with:
|
with:
|
||||||
charts_dir: ./charts
|
charts_dir: ./charts
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -22,26 +22,27 @@ jobs:
|
|||||||
- 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@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@master
|
||||||
permissions:
|
permissions:
|
||||||
actions: write
|
actions: write
|
||||||
|
|||||||
@@ -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@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,30 @@
|
|||||||
|
name: "[PR Management] Add PR Label"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened, reopened, edited]
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- 'release-harvester-v*'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
auto-assign-pr-label:
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.base_ref }}
|
||||||
|
- name: Setup Nodejs and yarn install
|
||||||
|
uses: ./.github/actions/setup
|
||||||
|
- name: Set PR label
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
PR_LABEL=$(node ./scripts/extract-release-label.mjs "${{ github.event.pull_request.title }}")
|
||||||
|
echo "PR_LABEL = $PR_LABEL"
|
||||||
|
gh pr edit ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --add-label "$PR_LABEL"
|
||||||
@@ -7,6 +7,7 @@ on:
|
|||||||
- main
|
- main
|
||||||
- 'release-*'
|
- 'release-*'
|
||||||
pull_request:
|
pull_request:
|
||||||
|
types: [opened, reopened, edited, synchronize]
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- 'release-*'
|
- 'release-*'
|
||||||
@@ -17,7 +18,42 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@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
|
||||||
|
FROM="$GITHUB_BEFORE"
|
||||||
|
TO="$GITHUB_AFTER"
|
||||||
|
else
|
||||||
|
echo "No valid commit range found, skipping commitlint."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "FROM=$FROM"
|
||||||
|
echo "TO=$TO"
|
||||||
|
npx commitlint --from "$FROM" --to "$TO" --verbose
|
||||||
|
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
|
||||||
@@ -1,62 +1,169 @@
|
|||||||
# 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:**
|
||||||
From Rancher 2.10.0, Harvester integration is provided by this new Harvester UI Extension.
|
> 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
|
## 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 detailed installation instructions, please refer to the [official Harvester documentation](https://docs.harvesterhci.io/v1.5/rancher/harvester-ui-extension#installation-on-rancher-210).
|
||||||
|
|
||||||
## Running for Development
|
|
||||||
|
|
||||||
This is what you probably want to get started.
|
## Development Setup
|
||||||
|
|
||||||
### Prerequisite
|
|
||||||
|
|
||||||
Nodejs version >= 20
|
|
||||||
|
|
||||||
|
Ensure **Node.js v20 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`
|
||||||
|
- Test `1.6.x` series → `v1.6-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
|
## 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-2025 [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");
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
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
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'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'],
|
||||||
|
'body-leading-blank': [2, 'always'],
|
||||||
|
'body-max-line-length': [2, 'always', 100],
|
||||||
|
'footer-leading-blank': [2, 'always'],
|
||||||
|
'footer-max-line-length': [2, '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'),
|
||||||
|
],
|
||||||
|
};
|
||||||
+21
-15
@@ -1,36 +1,36 @@
|
|||||||
{
|
{
|
||||||
"name": "harvester-ui-extension",
|
"name": "harvester-ui-extension",
|
||||||
"version": "1.5.0-rc4",
|
"version": "1.6.1",
|
||||||
"private": false,
|
"private": false,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rancher/shell": "3.0.2-rc.6",
|
"@babel/plugin-transform-class-static-block": "7.28.3",
|
||||||
|
"@rancher/shell": "3.0.5-rc.8",
|
||||||
"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",
|
||||||
"node-polyfill-webpack-plugin": "^3.0.0",
|
"node-polyfill-webpack-plugin": "^3.0.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": "~20.19.0",
|
||||||
"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.15.11",
|
||||||
"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.20",
|
||||||
"merge": "2.1.1",
|
"merge": "2.1.1",
|
||||||
"node-forge": "1.3.1",
|
"node-forge": "1.3.1",
|
||||||
"nth-check": "2.1.1",
|
"nth-check": "2.1.1",
|
||||||
"qs": "6.11.1",
|
"qs": "6.14.0",
|
||||||
"roarr": "7.0.4",
|
"roarr": "7.21.1",
|
||||||
"semver": "7.5.4",
|
"semver": "7.7.3",
|
||||||
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
|
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -41,10 +41,16 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
<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: [
|
||||||
@@ -19,6 +22,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
AsyncButton,
|
AsyncButton,
|
||||||
Banner,
|
Banner,
|
||||||
|
ActionMenu
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
@@ -30,15 +34,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 +60,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 +159,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 +199,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 +220,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 +242,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 +250,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 +312,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 +368,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"
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
softReboot() {
|
softReboot() {
|
||||||
this.vmResource.softrebootVM();
|
this.vmResource.doSoftReboot();
|
||||||
},
|
},
|
||||||
|
|
||||||
showKeysModal() {
|
showKeysModal() {
|
||||||
@@ -372,3 +372,12 @@ 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;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -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,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>
|
||||||
@@ -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,74 @@ 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 });
|
||||||
|
|
||||||
|
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 = [];
|
||||||
|
} finally {
|
||||||
|
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: {
|
||||||
|
allNamespaces() {
|
||||||
|
return this.$store.getters['harvester/all'](NAMESPACE).map((ns) => ns.id);
|
||||||
|
},
|
||||||
|
|
||||||
|
filteredNamespaces() {
|
||||||
|
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 allSelected =
|
||||||
return {
|
this.namespaces.length === this.filteredNamespaces.length &&
|
||||||
label: N.id,
|
this.filteredNamespaces.every((ns) => this.namespaces.includes(ns));
|
||||||
value: N.id
|
|
||||||
};
|
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, ...this.filteredNamespaces];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
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 +92,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,163 @@
|
|||||||
|
<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';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterUpgradeConfig',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
LabeledInput,
|
||||||
|
LabeledSelect,
|
||||||
|
RadioGroup
|
||||||
|
},
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
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' }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
normalizeValue(obj) {
|
||||||
|
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="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
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
// 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`,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,69 +1,69 @@
|
|||||||
// 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,
|
'vmMachineTypes',
|
||||||
'refreshIntervalInSecond',
|
'customSupportBundle',
|
||||||
'allowEmptySnapshotClassName'
|
'csiOnlineExpandValidation',
|
||||||
];
|
'vmNetworkMigration',
|
||||||
|
'kubeovnVpcSubnet',
|
||||||
// TODO: add v1.5.0 official release note
|
'rancherClusterSetting',
|
||||||
const featuresV150 = [
|
'cpuMemoryHotplug',
|
||||||
...featuresV142,
|
'cdiSettings',
|
||||||
'tpmPersistentState',
|
'vmCloneRunStrategy',
|
||||||
'efiPersistentState',
|
],
|
||||||
'untaggedNetworkSetting',
|
'v1.6.1': [],
|
||||||
'skipSingleReplicaDetachedVol',
|
|
||||||
'thirdPartyStorage'
|
|
||||||
];
|
|
||||||
|
|
||||||
export const RELEASE_FEATURES = {
|
|
||||||
'v1.3.0': featuresV130,
|
|
||||||
'v1.3.1': featuresV131,
|
|
||||||
'v1.3.2': featuresV132,
|
|
||||||
'v1.3.3': featuresV133,
|
|
||||||
'v1.4.0': featuresV140,
|
|
||||||
'v1.4.1': featuresV141,
|
|
||||||
'v1.4.2': featuresV142,
|
|
||||||
'v1.5.0': featuresV150
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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();
|
||||||
|
|||||||
@@ -24,6 +24,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,7 +33,8 @@ 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
|
||||||
} from './table-headers';
|
} from './table-headers';
|
||||||
|
|
||||||
const TEMPLATE = HCI.VM_VERSION;
|
const TEMPLATE = HCI.VM_VERSION;
|
||||||
@@ -222,6 +224,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,
|
||||||
@@ -255,7 +258,7 @@ export function init($plugin, store) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
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',
|
||||||
@@ -421,6 +424,7 @@ export function init($plugin, store) {
|
|||||||
[
|
[
|
||||||
HCI.CLUSTER_NETWORK,
|
HCI.CLUSTER_NETWORK,
|
||||||
HCI.NETWORK_ATTACHMENT,
|
HCI.NETWORK_ATTACHMENT,
|
||||||
|
HCI.VPC,
|
||||||
HCI.LB,
|
HCI.LB,
|
||||||
HCI.IP_POOL,
|
HCI.IP_POOL,
|
||||||
],
|
],
|
||||||
@@ -547,6 +551,21 @@ 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: 187,
|
||||||
|
route: {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
|
params: { resource: HCI.VPC }
|
||||||
|
},
|
||||||
|
exact: false,
|
||||||
|
ifHaveType: HCI.VPC,
|
||||||
|
});
|
||||||
|
|
||||||
configureType(HCI.SNAPSHOT, {
|
configureType(HCI.SNAPSHOT, {
|
||||||
isCreatable: false,
|
isCreatable: false,
|
||||||
location: {
|
location: {
|
||||||
|
|||||||
@@ -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',
|
||||||
@@ -70,6 +72,7 @@ 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',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CSI_SECRETS = {
|
export const CSI_SECRETS = {
|
||||||
@@ -80,3 +83,14 @@ export const CSI_SECRETS = {
|
|||||||
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',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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
|
||||||
|
};
|
||||||
|
|||||||
@@ -35,6 +35,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',
|
||||||
@@ -66,4 +67,13 @@ 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',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export const CLUSTER_NETWORK = 'clusterNetwork';
|
export const CLUSTER_NETWORK = 'clusterNetwork';
|
||||||
|
export const VPC = 'vpc';
|
||||||
|
|||||||
@@ -28,12 +28,17 @@ export const HCI_SETTING = {
|
|||||||
RELEASE_DOWNLOAD_URL: 'release-download-url',
|
RELEASE_DOWNLOAD_URL: 'release-download-url',
|
||||||
CCM_CSI_VERSION: 'harvester-csi-ccm-versions',
|
CCM_CSI_VERSION: 'harvester-csi-ccm-versions',
|
||||||
CSI_DRIVER_CONFIG: 'csi-driver-config',
|
CSI_DRIVER_CONFIG: 'csi-driver-config',
|
||||||
|
CSI_ONLINE_EXPAND_VALIDATION: 'csi-online-expand-validation',
|
||||||
VM_TERMINATION_PERIOD: 'default-vm-termination-grace-period-seconds',
|
VM_TERMINATION_PERIOD: 'default-vm-termination-grace-period-seconds',
|
||||||
NTP_SERVERS: 'ntp-servers',
|
NTP_SERVERS: 'ntp-servers',
|
||||||
AUTO_ROTATE_RKE2_CERTS: 'auto-rotate-rke2-certs',
|
AUTO_ROTATE_RKE2_CERTS: 'auto-rotate-rke2-certs',
|
||||||
KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES: 'kubeconfig-default-token-ttl-minutes',
|
KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES: 'kubeconfig-default-token-ttl-minutes',
|
||||||
LONGHORN_V2_DATA_ENGINE_ENABLED: 'longhorn-v2-data-engine-enabled',
|
LONGHORN_V2_DATA_ENGINE_ENABLED: 'longhorn-v2-data-engine-enabled',
|
||||||
ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO: 'additional-guest-memory-overhead-ratio',
|
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'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HCI_ALLOWED_SETTINGS = {
|
export const HCI_ALLOWED_SETTINGS = {
|
||||||
@@ -52,18 +57,21 @@ 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_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
|
||||||
@@ -89,13 +97,26 @@ 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.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',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = {
|
export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = {
|
||||||
|
|||||||
@@ -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,37 @@ 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',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,10 +5,21 @@ export const BACKUP_TYPE = {
|
|||||||
|
|
||||||
export const NETWORK_TYPE = {
|
export const NETWORK_TYPE = {
|
||||||
L2VLAN: 'L2VlanNetwork',
|
L2VLAN: 'L2VlanNetwork',
|
||||||
UNTAGGED: 'UntaggedNetwork'
|
UNTAGGED: 'UntaggedNetwork',
|
||||||
|
OVERLAY: 'OverlayNetwork',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VOLUME_MODE = {
|
export const VOLUME_MODE = {
|
||||||
BLOCK: 'Block',
|
BLOCK: 'Block',
|
||||||
FILE_SYSTEM: 'Filesystem'
|
FILE_SYSTEM: 'Filesystem'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const NETWORK_PROTOCOL = {
|
||||||
|
IPv4: 'IPv4',
|
||||||
|
IPv6: 'IPv6',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const INTERNAL_STORAGE_CLASS = {
|
||||||
|
VMSTATE_PERSISTENCE: 'vmstate-persistence',
|
||||||
|
LONGHORN_STATIC: 'longhorn-static',
|
||||||
|
};
|
||||||
|
|||||||
@@ -47,20 +47,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',
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
+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: {
|
||||||
@@ -346,6 +386,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() {
|
||||||
|
|||||||
@@ -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 });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -137,31 +164,39 @@ 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"
|
||||||
|
|||||||
@@ -1,149 +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
|
|
||||||
/>
|
|
||||||
<Banner
|
|
||||||
v-for="(err, i) in errors"
|
|
||||||
:key="i"
|
|
||||||
:label="err"
|
|
||||||
color="error"
|
|
||||||
/>
|
|
||||||
</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>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.actions {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttons {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -57,6 +57,10 @@ export default {
|
|||||||
if (!!pvc.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_ID]) {
|
if (!!pvc.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_ID]) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// we won't show golden image volume in the hot plug volume modal
|
||||||
|
if (pvc.isGoldenImageVolume) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return !pvc.attachVM;
|
return !pvc.attachVM;
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
<script>
|
||||||
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import { getVmCPUMemoryValues } from '../utils/cpuMemory';
|
||||||
|
import UnitInput from '@shell/components/form/UnitInput';
|
||||||
|
import { Card } from '@components/Card';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
|
import { GIBIBYTE } from '../utils/unit';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CPUMemoryHotplugModal',
|
||||||
|
|
||||||
|
emits: ['close'],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
AsyncButton, Card, Banner, UnitInput
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
resources: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const { cpu, memory } = getVmCPUMemoryValues(this.resources[0] || {});
|
||||||
|
|
||||||
|
return {
|
||||||
|
cpu,
|
||||||
|
memory,
|
||||||
|
errors: [],
|
||||||
|
GIBIBYTE
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
|
||||||
|
actionResource() {
|
||||||
|
return this.resources[0] || {};
|
||||||
|
},
|
||||||
|
maxResourcesMessage() {
|
||||||
|
const { maxCpu, maxMemory } = getVmCPUMemoryValues(this.actionResource);
|
||||||
|
|
||||||
|
if (maxCpu && maxMemory) {
|
||||||
|
return this.t('harvester.modal.cpuMemoryHotplug.maxResourcesMessage', { maxCpu, maxMemory });
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
close() {
|
||||||
|
this.cpu = '';
|
||||||
|
this.memory = '';
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
|
||||||
|
change() {
|
||||||
|
if (parseInt(this.memory, 10) < 1 ) {
|
||||||
|
this.memory = '1Gi';
|
||||||
|
}
|
||||||
|
if (this.cpu < 1) {
|
||||||
|
this.cpu = 1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async save(buttonCb) {
|
||||||
|
if (this.actionResource) {
|
||||||
|
try {
|
||||||
|
const res = await this.actionResource.doAction('cpuAndMemoryHotplug', { sockets: this.cpu, memory: this.memory });
|
||||||
|
|
||||||
|
if (res._status === 200 || res._status === 204) {
|
||||||
|
this.$store.dispatch('growl/success', {
|
||||||
|
title: this.t('generic.notification.title.succeed'),
|
||||||
|
message: this.t('harvester.modal.cpuMemoryHotplug.success', { vm: this.actionResource.nameDisplay })
|
||||||
|
}, { 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
|
||||||
|
ref="modal"
|
||||||
|
name="modal"
|
||||||
|
:show-highlight-border="false"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
<h4
|
||||||
|
v-clean-html="t('harvester.modal.cpuMemoryHotplug.title')"
|
||||||
|
class="text-default-text"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<Banner
|
||||||
|
v-if="maxResourcesMessage"
|
||||||
|
:label="maxResourcesMessage"
|
||||||
|
color="info"
|
||||||
|
/>
|
||||||
|
<UnitInput
|
||||||
|
v-model:value="cpu"
|
||||||
|
:label="t('harvester.virtualMachine.input.cpu')"
|
||||||
|
:delay="0"
|
||||||
|
required
|
||||||
|
suffix="C"
|
||||||
|
class="mb-20"
|
||||||
|
:mode="mode"
|
||||||
|
:min="1"
|
||||||
|
@update:value="change"
|
||||||
|
/>
|
||||||
|
<UnitInput
|
||||||
|
v-model:value="memory"
|
||||||
|
:label="t('harvester.virtualMachine.input.memory')"
|
||||||
|
:mode="mode"
|
||||||
|
:input-exponent="3"
|
||||||
|
:delay="0"
|
||||||
|
:min="1"
|
||||||
|
:increment="1024"
|
||||||
|
:output-modifier="true"
|
||||||
|
:disabled="disabled"
|
||||||
|
:suffix="GIBIBYTE"
|
||||||
|
class="mb-20"
|
||||||
|
@update:value="change"
|
||||||
|
/>
|
||||||
|
<Banner
|
||||||
|
v-for="(err, i) in errors"
|
||||||
|
:key="i"
|
||||||
|
:label="err"
|
||||||
|
color="error"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #actions>
|
||||||
|
<div class="actions">
|
||||||
|
<div class="buttons">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn role-secondary mr-10"
|
||||||
|
@click="close"
|
||||||
|
>
|
||||||
|
{{ t('generic.cancel') }}
|
||||||
|
</button>
|
||||||
|
<AsyncButton
|
||||||
|
mode="apply"
|
||||||
|
@click="save"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -10,6 +10,7 @@ import { LabeledInput } from '@components/Form/LabeledInput';
|
|||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
import { NAMESPACE, STORAGE_CLASS } from '@shell/config/types';
|
import { NAMESPACE, STORAGE_CLASS } from '@shell/config/types';
|
||||||
import { allHash } from '@shell/utils/promise';
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { isInternalStorageClass } from '../utils/storage-class';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterExportImageDialog',
|
name: 'HarvesterExportImageDialog',
|
||||||
@@ -105,11 +106,17 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const options = storages.filter((s) => !s.parameters?.backingImage).map((s) => {
|
const options = storages.filter((s) => !s.parameters?.backingImage).map((s) => {
|
||||||
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
let label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||||
|
const isInternal = isInternalStorageClass(s.metadata?.name);
|
||||||
|
|
||||||
|
if (isInternal) {
|
||||||
|
label += ` (${ this.t('harvester.storage.internal.label') })`;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label,
|
label,
|
||||||
value: s.name,
|
value: s.name,
|
||||||
|
disabled: isInternal,
|
||||||
};
|
};
|
||||||
}) || [];
|
}) || [];
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import { NAMESPACE } from '@shell/config/types';
|
||||||
import { randomStr } from '@shell/utils/string';
|
import { randomStr } from '@shell/utils/string';
|
||||||
import { exceptionToErrorsArray, stringify } from '@shell/utils/error';
|
import { exceptionToErrorsArray, stringify } from '@shell/utils/error';
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
@@ -6,7 +7,15 @@ import AsyncButton from '@shell/components/AsyncButton';
|
|||||||
import GraphCircle from '@shell/components/graph/Circle';
|
import GraphCircle from '@shell/components/graph/Circle';
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import AppModal from '@shell/components/AppModal';
|
import AppModal from '@shell/components/AppModal';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import UnitInput from '@shell/components/form/UnitInput';
|
||||||
import { HCI } from '../types';
|
import { HCI } from '../types';
|
||||||
|
import { HCI_SETTING } from '../config/settings';
|
||||||
|
import { DOC } from '../config/doc-links';
|
||||||
|
import { docLink } from '../utils/feature-flags';
|
||||||
|
|
||||||
|
const SELECT_ALL = 'select_all';
|
||||||
|
const UNSELECT_ALL = 'unselect_all';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SupportBundle',
|
name: 'SupportBundle',
|
||||||
@@ -17,14 +26,36 @@ export default {
|
|||||||
AsyncButton,
|
AsyncButton,
|
||||||
Banner,
|
Banner,
|
||||||
AppModal,
|
AppModal,
|
||||||
|
LabeledSelect,
|
||||||
|
UnitInput
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
await this.$store.dispatch('harvester/findAll', { type: NAMESPACE });
|
||||||
|
|
||||||
|
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 = [];
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
url: '',
|
isOpen: false,
|
||||||
description: '',
|
errors: [],
|
||||||
errors: [],
|
version: '',
|
||||||
isOpen: false,
|
clusterName: '',
|
||||||
|
url: '',
|
||||||
|
description: '',
|
||||||
|
namespaces: [],
|
||||||
|
defaultNamespaces: [],
|
||||||
|
timeout: '',
|
||||||
|
expiration: '',
|
||||||
|
nodeTimeout: '',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -39,23 +70,51 @@ export default {
|
|||||||
|
|
||||||
percentage() {
|
percentage() {
|
||||||
return this.$store.getters['harvester-common/getBundlePercentage'];
|
return this.$store.getters['harvester-common/getBundlePercentage'];
|
||||||
}
|
},
|
||||||
|
|
||||||
|
availableNamespaces() {
|
||||||
|
const allNamespaces = this.$store.getters['harvester/all'](NAMESPACE).map((ns) => ns.id);
|
||||||
|
const defaultNamespacesIds = this.defaultNamespaces.map((ns) => ns.id);
|
||||||
|
|
||||||
|
return allNamespaces.filter((ns) => !defaultNamespacesIds.includes(ns) || this.namespaces.includes(ns));
|
||||||
|
},
|
||||||
|
|
||||||
|
namespaceOptions() {
|
||||||
|
if (this.availableNamespaces.length === 0) return [];
|
||||||
|
|
||||||
|
const allSelected = this.namespaces.length === this.availableNamespaces.length &&
|
||||||
|
this.availableNamespaces.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, ...this.availableNamespaces];
|
||||||
|
},
|
||||||
|
|
||||||
|
docLink() {
|
||||||
|
const version = this.$store.getters['harvester-common/getServerVersion']();
|
||||||
|
|
||||||
|
return docLink(DOC.SUPPORT_BUNDLE_NAMESPACES, version);
|
||||||
|
},
|
||||||
|
|
||||||
|
customSupportBundleFeatureEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('customSupportBundle');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
isShowBundleModal: {
|
isShowBundleModal: {
|
||||||
|
immediate: true,
|
||||||
handler(show) {
|
handler(show) {
|
||||||
if (show) {
|
this.isOpen = show;
|
||||||
this.$nextTick(() => {
|
}
|
||||||
this.isOpen = true;
|
},
|
||||||
});
|
|
||||||
} else {
|
isOpen(newVal) {
|
||||||
this.isOpen = false;
|
if (newVal) {
|
||||||
this.url = '';
|
this.loadDefaultSettings();
|
||||||
this.description = '';
|
} else {
|
||||||
}
|
this.resetForm();
|
||||||
},
|
}
|
||||||
immediate: true
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -65,33 +124,93 @@ export default {
|
|||||||
close() {
|
close() {
|
||||||
this.isOpen = false;
|
this.isOpen = false;
|
||||||
this.$store.commit('harvester-common/toggleBundleModal', false);
|
this.$store.commit('harvester-common/toggleBundleModal', false);
|
||||||
this.backUpName = '';
|
},
|
||||||
|
|
||||||
|
loadDefaultSettings() {
|
||||||
|
const cluster = this.$store.getters['currentCluster'];
|
||||||
|
const versionSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SERVER_VERSION);
|
||||||
|
const namespacesSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_NAMESPACES);
|
||||||
|
const timeoutSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_TIMEOUT);
|
||||||
|
const expirationSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_EXPIRATION);
|
||||||
|
const nodeTimeoutSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT);
|
||||||
|
|
||||||
|
this.version = versionSetting?.currentVersion || '';
|
||||||
|
this.clusterName = cluster?.id || '';
|
||||||
|
this.namespaces = (namespacesSetting?.value ?? namespacesSetting?.default ?? '').split(',').map((ns) => ns.trim()).filter((ns) => ns);
|
||||||
|
this.timeout = timeoutSetting?.value ?? timeoutSetting?.default ?? '';
|
||||||
|
this.expiration = expirationSetting?.value ?? expirationSetting?.default ?? '';
|
||||||
|
this.nodeTimeout = nodeTimeoutSetting?.value ?? nodeTimeoutSetting?.default ?? '';
|
||||||
|
this.url = '';
|
||||||
|
this.description = '';
|
||||||
|
this.errors = [];
|
||||||
|
},
|
||||||
|
|
||||||
|
resetForm() {
|
||||||
|
this.url = '';
|
||||||
|
this.description = '';
|
||||||
|
this.namespaces = [];
|
||||||
|
this.timeout = '';
|
||||||
|
this.expiration = '';
|
||||||
|
this.nodeTimeout = '';
|
||||||
|
this.errors = [];
|
||||||
|
},
|
||||||
|
|
||||||
|
updateNamespaces(selected) {
|
||||||
|
if (selected.includes(SELECT_ALL)) {
|
||||||
|
this.namespaces = [...this.availableNamespaces];
|
||||||
|
} else if (selected.includes(UNSELECT_ALL)) {
|
||||||
|
this.namespaces = [];
|
||||||
|
} else {
|
||||||
|
this.namespaces = selected.filter((val) => val !== SELECT_ALL && val !== UNSELECT_ALL);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateNumberValue(field, value) {
|
||||||
|
if (value === '' || value === null || isNaN(value)) {
|
||||||
|
this[field] = '';
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const num = Number(value);
|
||||||
|
const isValid = Number.isInteger(num) && num >= 0;
|
||||||
|
|
||||||
|
this[field] = isValid ? String(num) : '';
|
||||||
|
},
|
||||||
|
|
||||||
|
onKeyDown(e) {
|
||||||
|
if (['e', 'E', '+', '-', '.'].includes(e.key)) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async save(buttonCb) {
|
async save(buttonCb) {
|
||||||
this.errors = [];
|
this.errors = [];
|
||||||
|
|
||||||
const name = `bundle-${ randomStr(5).toLowerCase() }`;
|
const name = `bundle-${ this.clusterName }-${ this.version }-${ randomStr(5).toLowerCase() }`;
|
||||||
const namespace = 'harvester-system';
|
const namespace = 'harvester-system';
|
||||||
|
|
||||||
|
const spec = {
|
||||||
|
description: this.description.trim(),
|
||||||
|
...(this.url.trim() && { issueURL: this.url.trim() }),
|
||||||
|
...(this.namespaces.length > 0 && { extraCollectionNamespaces: this.namespaces }),
|
||||||
|
...(this.timeout !== '' && { timeout: Number(this.timeout) }),
|
||||||
|
...(this.expiration !== '' && { expiration: Number(this.expiration) }),
|
||||||
|
...(this.nodeTimeout !== '' && { nodeTimeout: Number(this.nodeTimeout) }),
|
||||||
|
};
|
||||||
|
|
||||||
const bundleCrd = {
|
const bundleCrd = {
|
||||||
apiVersion: 'harvesterhci.io/v1beta1',
|
apiVersion: 'harvesterhci.io/v1beta1',
|
||||||
type: HCI.SUPPORT_BUNDLE,
|
type: HCI.SUPPORT_BUNDLE,
|
||||||
kind: 'SupportBundle',
|
kind: 'SupportBundle',
|
||||||
metadata: {
|
metadata: { name, namespace },
|
||||||
name,
|
spec,
|
||||||
namespace
|
|
||||||
},
|
|
||||||
spec: {
|
|
||||||
issueURL: this.url,
|
|
||||||
description: this.description
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
const bundleValue = await this.$store.dispatch(`${ inStore }/create`, bundleCrd);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const bundleValue = await this.$store.dispatch(`${ inStore }/create`, bundleCrd);
|
||||||
|
|
||||||
await bundleValue.save();
|
await bundleValue.save();
|
||||||
|
|
||||||
this.$store.commit('harvester-common/setLatestBundleId', `${ namespace }/${ name }`, { root: true });
|
this.$store.commit('harvester-common/setLatestBundleId', `${ namespace }/${ name }`, { root: true });
|
||||||
@@ -118,69 +237,110 @@ export default {
|
|||||||
@close="close"
|
@close="close"
|
||||||
>
|
>
|
||||||
<div class="p-20">
|
<div class="p-20">
|
||||||
<h2>
|
<h2>{{ t('harvester.modal.bundle.title') }}</h2>
|
||||||
{{ t('harvester.modal.bundle.title') }}
|
<div class="content">
|
||||||
</h2>
|
<div
|
||||||
|
v-if="bundlePending"
|
||||||
<div
|
class="circle mb-20"
|
||||||
v-if="!bundlePending"
|
>
|
||||||
class="content"
|
|
||||||
>
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="url"
|
|
||||||
:label="t('harvester.modal.bundle.url')"
|
|
||||||
class="mb-20"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="description"
|
|
||||||
:label="t('harvester.modal.bundle.description')"
|
|
||||||
type="multiline"
|
|
||||||
:min-height="120"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="content"
|
|
||||||
>
|
|
||||||
<div class="circle">
|
|
||||||
<GraphCircle
|
<GraphCircle
|
||||||
primary-stroke-color="green"
|
primary-stroke-color="green"
|
||||||
secondary-stroke-color="white"
|
secondary-stroke-color="lightgrey"
|
||||||
:stroke-width="6"
|
:stroke-width="6"
|
||||||
:percentage="percentage"
|
:percentage="percentage"
|
||||||
:show-text="true"
|
:show-text="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<template v-else>
|
||||||
|
<p
|
||||||
|
v-if="customSupportBundleFeatureEnabled"
|
||||||
|
v-clean-html="t('harvester.modal.bundle.tip', { doc: docLink }, true)"
|
||||||
|
class="mb-20"
|
||||||
|
/>
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="url"
|
||||||
|
:label="t('harvester.modal.bundle.url')"
|
||||||
|
class="mb-10"
|
||||||
|
/>
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="description"
|
||||||
|
required
|
||||||
|
:label="t('harvester.modal.bundle.description')"
|
||||||
|
type="multiline"
|
||||||
|
:min-height="80"
|
||||||
|
class="mb-10"
|
||||||
|
/>
|
||||||
|
|
||||||
<div
|
<template v-if="customSupportBundleFeatureEnabled">
|
||||||
v-for="(err, idx) in errors"
|
<LabeledSelect
|
||||||
:key="idx"
|
v-model:value="namespaces"
|
||||||
>
|
:label="t('harvester.modal.bundle.namespaces.label')"
|
||||||
<Banner
|
:clearable="true"
|
||||||
color="error"
|
:multiple="true"
|
||||||
:label="stringify(err)"
|
:options="namespaceOptions"
|
||||||
/>
|
class="mb-10 label-select"
|
||||||
</div>
|
:tooltip="t('harvester.modal.bundle.namespaces.tooltip', _, true)"
|
||||||
|
@update:value="updateNamespaces"
|
||||||
<div class="footer mt-20">
|
/>
|
||||||
<button
|
<UnitInput
|
||||||
class="btn btn-sm role-secondary mr-10"
|
v-model:value="timeout"
|
||||||
@click="close"
|
:label="t('harvester.modal.bundle.timeout.label')"
|
||||||
|
class="mb-10"
|
||||||
|
type="number"
|
||||||
|
:min="0"
|
||||||
|
:tooltip="t('harvester.modal.bundle.timeout.tooltip', _, true)"
|
||||||
|
:suffix="timeout > 1 ? 'Minutes' : 'Minute'"
|
||||||
|
@keydown="onKeyDown"
|
||||||
|
@update:value="val => updateNumberValue('timeout', val)"
|
||||||
|
/>
|
||||||
|
<UnitInput
|
||||||
|
v-model:value="expiration"
|
||||||
|
:label="t('harvester.modal.bundle.expiration.label')"
|
||||||
|
class="mb-10"
|
||||||
|
type="number"
|
||||||
|
:min="0"
|
||||||
|
:tooltip="t('harvester.modal.bundle.expiration.tooltip', _, true)"
|
||||||
|
:suffix="expiration > 1 ? 'Minutes' : 'Minute'"
|
||||||
|
@keydown="onKeyDown"
|
||||||
|
@update:value="val => updateNumberValue('expiration', val)"
|
||||||
|
/>
|
||||||
|
<UnitInput
|
||||||
|
v-model:value="nodeTimeout"
|
||||||
|
:label="t('harvester.modal.bundle.nodeTimeout.label')"
|
||||||
|
class="mb-10"
|
||||||
|
type="number"
|
||||||
|
:min="0"
|
||||||
|
:tooltip="t('harvester.modal.bundle.nodeTimeout.tooltip', _, true)"
|
||||||
|
:suffix="nodeTimeout > 1 ? 'Minutes' : 'Minute'"
|
||||||
|
@keydown="onKeyDown"
|
||||||
|
@update:value="val => updateNumberValue('nodeTimeout', val)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<div
|
||||||
|
v-for="(err, idx) in errors"
|
||||||
|
:key="idx"
|
||||||
>
|
>
|
||||||
{{ t('generic.close') }}
|
<Banner
|
||||||
</button>
|
color="error"
|
||||||
|
:label="stringify(err)"
|
||||||
<AsyncButton
|
/>
|
||||||
type="submit"
|
</div>
|
||||||
mode="generate"
|
<div class="footer mt-20">
|
||||||
class="btn btn-sm bg-primary"
|
<button
|
||||||
:disabled="bundlePending"
|
class="btn btn-sm role-secondary mr-10"
|
||||||
@click="save"
|
@click="close"
|
||||||
/>
|
>
|
||||||
|
{{ t('generic.close') }}
|
||||||
|
</button>
|
||||||
|
<AsyncButton
|
||||||
|
type="submit"
|
||||||
|
mode="generate"
|
||||||
|
class="btn btn-sm bg-primary"
|
||||||
|
:disabled="bundlePending"
|
||||||
|
@click="save"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</app-modal>
|
</app-modal>
|
||||||
@@ -194,6 +354,10 @@ export default {
|
|||||||
max-height: 100vh;
|
max-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.labeled-select.taggable ::v-deep(.vs__selected-options .vs__selected.vs__selected > button) {
|
||||||
|
margin: 0 7px;
|
||||||
|
}
|
||||||
|
|
||||||
.bundle {
|
.bundle {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
@@ -204,10 +368,8 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
height: 218px;
|
|
||||||
|
|
||||||
.circle {
|
.circle {
|
||||||
padding-top: 20px;
|
padding: 10px 0;
|
||||||
height: 160px;
|
height: 160px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,8 +73,10 @@ export default {
|
|||||||
|
|
||||||
data.spec.clusterNetwork = this.clusterNetwork;
|
data.spec.clusterNetwork = this.clusterNetwork;
|
||||||
|
|
||||||
|
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl'](`v1/harvester/${ HCI.VLAN_CONFIG }s/${ data.id }`);
|
||||||
|
|
||||||
await this.$store.dispatch('harvester/request', {
|
await this.$store.dispatch('harvester/request', {
|
||||||
url: `/v1/harvester/${ HCI.VLAN_CONFIG }s/${ data.id }`,
|
url,
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export default {
|
|||||||
:mode="mode"
|
:mode="mode"
|
||||||
:errors="errors"
|
:errors="errors"
|
||||||
@finish="save"
|
@finish="save"
|
||||||
|
@error="e=>errors=e"
|
||||||
>
|
>
|
||||||
<component
|
<component
|
||||||
:is="currentComponent"
|
:is="currentComponent"
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ export default {
|
|||||||
:apply-hooks="applyHooks"
|
:apply-hooks="applyHooks"
|
||||||
@finish="save"
|
@finish="save"
|
||||||
@cancel="done"
|
@cancel="done"
|
||||||
|
@error="e=>errors=e"
|
||||||
>
|
>
|
||||||
<NameNsDescription
|
<NameNsDescription
|
||||||
:value="value"
|
:value="value"
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ export default {
|
|||||||
:errors="errors"
|
:errors="errors"
|
||||||
:apply-hooks="applyHooks"
|
:apply-hooks="applyHooks"
|
||||||
@finish="save"
|
@finish="save"
|
||||||
|
@error="e=>errors=e"
|
||||||
>
|
>
|
||||||
<div class="header mb-20">
|
<div class="header mb-20">
|
||||||
<FileSelector
|
<FileSelector
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ export default {
|
|||||||
:validation-passed="true"
|
:validation-passed="true"
|
||||||
:errors="errors"
|
:errors="errors"
|
||||||
:can-yaml="true"
|
:can-yaml="true"
|
||||||
@error="e=>errors = e"
|
@error="e=>errors=e"
|
||||||
@finish="save"
|
@finish="save"
|
||||||
@cancel="done"
|
@cancel="done"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ export default {
|
|||||||
:resource="value"
|
:resource="value"
|
||||||
:subtypes="[]"
|
:subtypes="[]"
|
||||||
:cancel-event="true"
|
:cancel-event="true"
|
||||||
@error="e=>errors = e"
|
@error="e=>errors=e"
|
||||||
@finish="save"
|
@finish="save"
|
||||||
@cancel="done"
|
@cancel="done"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -6,14 +6,13 @@ import { LabeledInput } from '@components/Form/LabeledInput';
|
|||||||
import { RadioGroup } from '@components/Form/Radio';
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
|
||||||
import { HCI as HCI_LABELS_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
import { HCI as HCI_LABELS_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
import { allHash } from '@shell/utils/promise';
|
import { allHash } from '@shell/utils/promise';
|
||||||
import { HCI } from '../types';
|
import { HCI } from '../types';
|
||||||
import { NETWORK_TYPE } from '../config/types';
|
import { NETWORK_TYPE } from '../config/types';
|
||||||
|
|
||||||
const { L2VLAN, UNTAGGED } = NETWORK_TYPE;
|
const { L2VLAN, UNTAGGED, OVERLAY } = NETWORK_TYPE;
|
||||||
|
|
||||||
const AUTO = 'auto';
|
const AUTO = 'auto';
|
||||||
const MANUAL = 'manual';
|
const MANUAL = 'manual';
|
||||||
@@ -44,13 +43,10 @@ export default {
|
|||||||
|
|
||||||
data() {
|
data() {
|
||||||
const config = JSON.parse(this.value.spec.config);
|
const config = JSON.parse(this.value.spec.config);
|
||||||
|
|
||||||
const annotations = this.value?.metadata?.annotations || {};
|
const annotations = this.value?.metadata?.annotations || {};
|
||||||
const layer3Network = JSON.parse(annotations[HCI_LABELS_ANNOTATIONS.NETWORK_ROUTE] || '{}');
|
const layer3Network = JSON.parse(annotations[HCI_LABELS_ANNOTATIONS.NETWORK_ROUTE] || '{}');
|
||||||
|
|
||||||
if ((config.bridge || '').endsWith('-br')) {
|
|
||||||
config.bridge = config.bridge.slice(0, -3);
|
|
||||||
}
|
|
||||||
|
|
||||||
const type = this.value.vlanType || L2VLAN ;
|
const type = this.value.vlanType || L2VLAN ;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -78,6 +74,30 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
clusterBridge: {
|
||||||
|
get() {
|
||||||
|
if (!this.config.bridge) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove -br suffix if exists
|
||||||
|
return this.config?.bridge?.endsWith('-br') ? this.config.bridge.slice(0, -3) : '';
|
||||||
|
},
|
||||||
|
|
||||||
|
set(neu) {
|
||||||
|
if (neu === '') {
|
||||||
|
this.config.bridge = '';
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!neu.endsWith('-br')) {
|
||||||
|
this.config.bridge = `${ neu }-br`;
|
||||||
|
} else {
|
||||||
|
this.config.bridge = neu;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
modeOptions() {
|
modeOptions() {
|
||||||
return [{
|
return [{
|
||||||
label: this.t('harvester.network.layer3Network.mode.auto'),
|
label: this.t('harvester.network.layer3Network.mode.auto'),
|
||||||
@@ -88,6 +108,14 @@ export default {
|
|||||||
}];
|
}];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
kubeovnVpcSubnetSupport() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('kubeovnVpcSubnet');
|
||||||
|
},
|
||||||
|
|
||||||
|
longhornV2LVMSupport() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('longhornV2LVMSupport');
|
||||||
|
},
|
||||||
|
|
||||||
clusterNetworkOptions() {
|
clusterNetworkOptions() {
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
|
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
|
||||||
@@ -103,8 +131,30 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
networkType() {
|
networkTypes() {
|
||||||
return [L2VLAN, UNTAGGED];
|
const types = [L2VLAN, UNTAGGED];
|
||||||
|
|
||||||
|
if (this.kubeovnVpcSubnetSupport) {
|
||||||
|
types.push(OVERLAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
return types;
|
||||||
|
},
|
||||||
|
|
||||||
|
isL2VlanNetwork() {
|
||||||
|
if (this.isView) {
|
||||||
|
return this.value.vlanType === L2VLAN;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.type === L2VLAN;
|
||||||
|
},
|
||||||
|
|
||||||
|
isOverlayNetwork() {
|
||||||
|
if (this.isView) {
|
||||||
|
return this.value.vlanType === OVERLAY;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.type === OVERLAY;
|
||||||
},
|
},
|
||||||
|
|
||||||
isUntaggedNetwork() {
|
isUntaggedNetwork() {
|
||||||
@@ -116,36 +166,54 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
type(newType) {
|
||||||
|
if (newType === OVERLAY) {
|
||||||
|
this.config.type = 'kube-ovn';
|
||||||
|
this.config.provider = `${ this.value.metadata.name }.${ this.value.metadata.namespace }.ovn`;
|
||||||
|
this.config.server_socket = '/run/openvswitch/kube-ovn-daemon.sock';
|
||||||
|
} else {
|
||||||
|
this.config.type = 'bridge';
|
||||||
|
this.config.promiscMode = true;
|
||||||
|
this.config.ipam = {};
|
||||||
|
this.config.bridge = '';
|
||||||
|
delete this.config.provider;
|
||||||
|
delete this.config.server_socket;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async saveNetwork(buttonCb) {
|
async saveNetwork(buttonCb) {
|
||||||
const errors = [];
|
const errors = [];
|
||||||
|
|
||||||
if (!this.config.vlan && !this.isUntaggedNetwork) {
|
if (this.isL2VlanNetwork || this.isUntaggedNetwork) {
|
||||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('tableHeaders.networkVlan') }));
|
if (!this.config.vlan && !this.isUntaggedNetwork) {
|
||||||
}
|
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('tableHeaders.networkVlan') }));
|
||||||
|
|
||||||
if (!this.config.bridge) {
|
|
||||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.clusterNetwork.label') }));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.layer3Network.mode === MANUAL) {
|
|
||||||
if (!this.layer3Network.gateway) {
|
|
||||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.gateway.label') }));
|
|
||||||
}
|
}
|
||||||
if (!this.layer3Network.cidr) {
|
|
||||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.cidr.label') }));
|
if (!this.config.bridge) {
|
||||||
|
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.clusterNetwork.label') }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.layer3Network.mode === MANUAL) {
|
||||||
|
if (!this.layer3Network.gateway) {
|
||||||
|
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.gateway.label') }));
|
||||||
|
}
|
||||||
|
if (!this.layer3Network.cidr) {
|
||||||
|
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.cidr.label') }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
buttonCb(false);
|
||||||
|
this.errors = errors;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.value.setAnnotation(HCI_LABELS_ANNOTATIONS.NETWORK_ROUTE, JSON.stringify(this.layer3Network));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errors.length > 0) {
|
|
||||||
buttonCb(false);
|
|
||||||
this.errors = errors;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.value.setAnnotation(HCI_LABELS_ANNOTATIONS.NETWORK_ROUTE, JSON.stringify(this.layer3Network));
|
|
||||||
|
|
||||||
await this.save(buttonCb);
|
await this.save(buttonCb);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -169,14 +237,19 @@ export default {
|
|||||||
updateBeforeSave() {
|
updateBeforeSave() {
|
||||||
this.config.name = this.value.metadata.name;
|
this.config.name = this.value.metadata.name;
|
||||||
|
|
||||||
|
if (this.isOverlayNetwork) {
|
||||||
|
this.config.provider = `${ this.value.metadata.name }.${ this.value.metadata.namespace }.ovn`;
|
||||||
|
delete this.config.bridge;
|
||||||
|
delete this.config.promiscMode;
|
||||||
|
delete this.config.vlan;
|
||||||
|
delete this.config.ipam;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isUntaggedNetwork) {
|
if (this.isUntaggedNetwork) {
|
||||||
delete this.config.vlan;
|
delete this.config.vlan;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.value.spec.config = JSON.stringify({
|
this.value.spec.config = JSON.stringify({ ...this.config });
|
||||||
...this.config,
|
|
||||||
bridge: `${ this.config.bridge }-br`,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -190,6 +263,7 @@ export default {
|
|||||||
:errors="errors"
|
:errors="errors"
|
||||||
:apply-hooks="applyHooks"
|
:apply-hooks="applyHooks"
|
||||||
@finish="saveNetwork"
|
@finish="saveNetwork"
|
||||||
|
@error="e=>errors=e"
|
||||||
>
|
>
|
||||||
<NameNsDescription
|
<NameNsDescription
|
||||||
ref="nd"
|
ref="nd"
|
||||||
@@ -211,14 +285,15 @@ export default {
|
|||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
v-model:value="type"
|
v-model:value="type"
|
||||||
class="mb-20"
|
class="mb-20"
|
||||||
:options="networkType"
|
:options="networkTypes"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
|
:disabled="isEdit"
|
||||||
:label="t('harvester.fields.type')"
|
:label="t('harvester.fields.type')"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LabeledInput
|
<LabeledInput
|
||||||
v-if="!isUntaggedNetwork"
|
v-if="isL2VlanNetwork"
|
||||||
v-model:value.number="config.vlan"
|
v-model:value.number="config.vlan"
|
||||||
class="mb-20"
|
class="mb-20"
|
||||||
required
|
required
|
||||||
@@ -228,25 +303,20 @@ export default {
|
|||||||
:mode="mode"
|
:mode="mode"
|
||||||
@update:value="input"
|
@update:value="input"
|
||||||
/>
|
/>
|
||||||
|
<LabeledSelect
|
||||||
<div class="row">
|
v-if="!isOverlayNetwork"
|
||||||
<div
|
v-model:value="clusterBridge"
|
||||||
class="col span-12"
|
class="mb-20"
|
||||||
>
|
:label="t('harvester.network.clusterNetwork.label')"
|
||||||
<LabeledSelect
|
required
|
||||||
v-model:value="config.bridge"
|
:disabled="isEdit"
|
||||||
class="mb-20"
|
:options="clusterNetworkOptions"
|
||||||
:label="t('harvester.network.clusterNetwork.label')"
|
:mode="mode"
|
||||||
required
|
:placeholder="t('harvester.network.clusterNetwork.selectPlaceholder')"
|
||||||
:options="clusterNetworkOptions"
|
/>
|
||||||
:mode="mode"
|
|
||||||
:placeholder="t('harvester.network.clusterNetwork.selectPlaceholder')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
v-if="!isUntaggedNetwork"
|
v-if="isL2VlanNetwork"
|
||||||
name="layer3Network"
|
name="layer3Network"
|
||||||
:label="t('harvester.network.tabs.layer3Network')"
|
:label="t('harvester.network.tabs.layer3Network')"
|
||||||
:weight="98"
|
:weight="98"
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ export default {
|
|||||||
:apply-hooks="applyHooks"
|
:apply-hooks="applyHooks"
|
||||||
:validation-passed="canSave"
|
:validation-passed="canSave"
|
||||||
@finish="save"
|
@finish="save"
|
||||||
@error="e=>errors = e"
|
@error="e=>errors=e"
|
||||||
>
|
>
|
||||||
<div class="banner">
|
<div class="banner">
|
||||||
<Banner
|
<Banner
|
||||||
@@ -233,7 +233,7 @@ export default {
|
|||||||
:disabled="isEdit || isView"
|
:disabled="isEdit || isView"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
row
|
row
|
||||||
@input="onTypeChange"
|
@update:value="onTypeChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-30">
|
<div class="row mb-30">
|
||||||
|
|||||||
@@ -84,6 +84,10 @@ export default {
|
|||||||
this.value['_type'] = TYPES.OPAQUE;
|
this.value['_type'] = TYPES.OPAQUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( this.realMode === _CLONE) {
|
||||||
|
this.value['_type'] = this.liveValue._type;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isCloud,
|
isCloud,
|
||||||
nodeDrivers: null,
|
nodeDrivers: null,
|
||||||
@@ -234,7 +238,7 @@ export default {
|
|||||||
:resource="value"
|
:resource="value"
|
||||||
:errors="errors"
|
:errors="errors"
|
||||||
@finish="saveSecret"
|
@finish="saveSecret"
|
||||||
@error="e=>errors = e"
|
@error="e=>errors=e"
|
||||||
>
|
>
|
||||||
<NameNsDescription
|
<NameNsDescription
|
||||||
:value="value"
|
:value="value"
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import { RadioGroup } from '@components/Form/Radio';
|
|||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
import { TextAreaAutoGrow } from '@components/Form/TextArea';
|
import { TextAreaAutoGrow } from '@components/Form/TextArea';
|
||||||
|
import UnitInput from '@shell/components/form/UnitInput';
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import { DOC } from '../config/doc-links';
|
||||||
|
import { docLink } from '../utils/feature-flags';
|
||||||
|
|
||||||
import { HCI_ALLOWED_SETTINGS, HCI_SINGLE_CLUSTER_ALLOWED_SETTING, HCI_SETTING } from '../config/settings';
|
import { HCI_ALLOWED_SETTINGS, HCI_SINGLE_CLUSTER_ALLOWED_SETTING, HCI_SETTING } from '../config/settings';
|
||||||
|
|
||||||
@@ -15,7 +17,8 @@ export default {
|
|||||||
LabeledInput,
|
LabeledInput,
|
||||||
LabeledSelect,
|
LabeledSelect,
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
TextAreaAutoGrow
|
TextAreaAutoGrow,
|
||||||
|
UnitInput
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [CreateEditView],
|
mixins: [CreateEditView],
|
||||||
@@ -57,7 +60,13 @@ export default {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
setting,
|
setting,
|
||||||
description: isHarvester ? t(`advancedSettings.descriptions.harv-${ this.value.id }`) : t(`advancedSettings.descriptions.${ this.value.id }`),
|
description: isHarvester ? t(
|
||||||
|
`advancedSettings.descriptions.harv-${ this.value.id }`,
|
||||||
|
this.getDocLinkParams()
|
||||||
|
) : t(
|
||||||
|
`advancedSettings.descriptions.${ this.value.id }`,
|
||||||
|
this.getDocLinkParams()
|
||||||
|
),
|
||||||
editHelp: t(`advancedSettings.editHelp.${ this.value.id }`),
|
editHelp: t(`advancedSettings.editHelp.${ this.value.id }`),
|
||||||
enumOptions,
|
enumOptions,
|
||||||
canReset,
|
canReset,
|
||||||
@@ -128,6 +137,13 @@ export default {
|
|||||||
await this.clusterRegistrationUrlTip();
|
await this.clusterRegistrationUrlTip();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// remove any leading zeros (e.g. '0123' becomes 123)
|
||||||
|
if (this.setting.kind === 'number' && this.value.value) {
|
||||||
|
const num = Number(this.value.value);
|
||||||
|
|
||||||
|
this.value.value = isNaN(num) ? 0 : `${ num }`;
|
||||||
|
}
|
||||||
|
|
||||||
this.save(done);
|
this.save(done);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -163,6 +179,18 @@ export default {
|
|||||||
if (typeof this.$refs.settingComp?.useDefault === 'function') {
|
if (typeof this.$refs.settingComp?.useDefault === 'function') {
|
||||||
this.$refs.settingComp.useDefault();
|
this.$refs.settingComp.useDefault();
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
getDocLinkParams() {
|
||||||
|
const setting = HCI_ALLOWED_SETTINGS[this.value.id] || HCI_SINGLE_CLUSTER_ALLOWED_SETTING[this.value.id];
|
||||||
|
|
||||||
|
if (setting?.docPath) {
|
||||||
|
const version = this.$store.getters['harvester-common/getServerVersion']();
|
||||||
|
const url = docLink(DOC[setting.docPath], version);
|
||||||
|
|
||||||
|
return { url };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -177,7 +205,7 @@ export default {
|
|||||||
:subtypes="[]"
|
:subtypes="[]"
|
||||||
:can-yaml="false"
|
:can-yaml="false"
|
||||||
:cancel-event="true"
|
:cancel-event="true"
|
||||||
@error="e => (errors = e)"
|
@error="e=>errors=e"
|
||||||
@finish="saveSettings"
|
@finish="saveSettings"
|
||||||
@cancel="done"
|
@cancel="done"
|
||||||
>
|
>
|
||||||
@@ -238,6 +266,13 @@ export default {
|
|||||||
:min-height="254"
|
:min-height="254"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else-if="setting.kind === 'number'">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="value.value"
|
||||||
|
:label="t('advancedSettings.edit.value')"
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<LabeledInput
|
<LabeledInput
|
||||||
v-model:value="value.value"
|
v-model:value="value.value"
|
||||||
|
|||||||
@@ -0,0 +1,319 @@
|
|||||||
|
<script>
|
||||||
|
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
||||||
|
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
|
import ArrayList from '@shell/components/form/ArrayList';
|
||||||
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
|
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import { VOLUME_SNAPSHOT_CLASS, HCI } from '../../types';
|
||||||
|
import { HCI_SETTING } from '../../config/settings';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { _EDIT, _CREATE } from '@shell/config/query-params';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CDISettings',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
provisioner: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
components: {
|
||||||
|
ArrayList,
|
||||||
|
Checkbox,
|
||||||
|
LabeledInput,
|
||||||
|
LabeledSelect,
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ['update:cdiSettings'],
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const hash = {
|
||||||
|
volumeSnapshotClasses: this.$store.dispatch(`${ this.inStore }/findAll`, { type: VOLUME_SNAPSHOT_CLASS }),
|
||||||
|
csiDriverConfigSetting: this.$store.dispatch(`${ this.inStore }/find`, { type: HCI.SETTING, id: HCI_SETTING.CSI_DRIVER_CONFIG }),
|
||||||
|
};
|
||||||
|
|
||||||
|
await allHash(hash);
|
||||||
|
|
||||||
|
if (this.mode === _CREATE ) {
|
||||||
|
this.setDefaultVolumeSnapshotClass();
|
||||||
|
} else {
|
||||||
|
this.initCDISettingsFromAnnotations();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
cdiSettings: {
|
||||||
|
volumeModeAccessModes: [],
|
||||||
|
volumeSnapshotClass: null,
|
||||||
|
cloneStrategy: null,
|
||||||
|
filesystemOverhead: null,
|
||||||
|
},
|
||||||
|
defaultAddValue: { volumeMode: null, accessModes: [] },
|
||||||
|
noneOption: { label: 'None', value: '' },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
inStore() {
|
||||||
|
return this.$store.getters['currentProduct'].inStore;
|
||||||
|
},
|
||||||
|
|
||||||
|
allVolumeModeOptions() {
|
||||||
|
return Object.values(VOLUME_MODE).map((mode) => ({ label: mode, value: mode }));
|
||||||
|
},
|
||||||
|
|
||||||
|
selectedVolumeModes() {
|
||||||
|
return this.cdiSettings.volumeModeAccessModes
|
||||||
|
.map((item) => item.volumeMode)
|
||||||
|
.filter(Boolean);
|
||||||
|
},
|
||||||
|
|
||||||
|
volumeModeOptions() {
|
||||||
|
return [this.noneOption, ...this.allVolumeModeOptions].filter( (option) => !this.selectedVolumeModes.includes(option.value) );
|
||||||
|
},
|
||||||
|
|
||||||
|
allVolumeModesSelected() {
|
||||||
|
return this.selectedVolumeModes.length === this.allVolumeModeOptions.length;
|
||||||
|
},
|
||||||
|
|
||||||
|
accessModeOptions() {
|
||||||
|
return [
|
||||||
|
{ label: 'ReadWriteOnce', value: 'ReadWriteOnce' },
|
||||||
|
{ label: 'ReadOnlyMany', value: 'ReadOnlyMany' },
|
||||||
|
{ label: 'ReadWriteMany', value: 'ReadWriteMany' },
|
||||||
|
{ label: 'ReadWriteOncePod', value: 'ReadWriteOncePod' },
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
cloneStrategyOptions() {
|
||||||
|
return [
|
||||||
|
this.noneOption,
|
||||||
|
{ label: this.t('harvester.storage.cdiSettings.cloneStrategy.copy'), value: 'copy' },
|
||||||
|
{ label: this.t('harvester.storage.cdiSettings.cloneStrategy.snapshot'), value: 'snapshot' },
|
||||||
|
{ label: this.t('harvester.storage.cdiSettings.cloneStrategy.csiClone'), value: 'csi-clone' },
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
volumeSnapshotClassOptions() {
|
||||||
|
const allClasses = this.$store.getters[`${ this.inStore }/all`](VOLUME_SNAPSHOT_CLASS) || [];
|
||||||
|
const filtered = allClasses.filter((c) => c.driver === this.provisioner);
|
||||||
|
|
||||||
|
return [this.noneOption, ...filtered.map((c) => ({ label: c.name, value: c.name }))];
|
||||||
|
},
|
||||||
|
|
||||||
|
isFilesystemOverheadValid() {
|
||||||
|
const val = this.cdiSettings.filesystemOverhead;
|
||||||
|
|
||||||
|
if (val === null || val === '') return true;
|
||||||
|
const regex = /^(0(\.\d{1,3})?|1(\.0{1,3})?)$/;
|
||||||
|
|
||||||
|
return regex.test(val);
|
||||||
|
},
|
||||||
|
|
||||||
|
isCustomClass() {
|
||||||
|
return this.mode === _CREATE || this.mode === _EDIT;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
provisioner() {
|
||||||
|
this.resetCdiSettings();
|
||||||
|
this.$nextTick(this.setDefaultVolumeSnapshotClass);
|
||||||
|
},
|
||||||
|
|
||||||
|
cdiSettings: {
|
||||||
|
handler(val) {
|
||||||
|
this.$emit('update:cdiSettings', val);
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
|
immediate: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
setDefaultVolumeSnapshotClass() {
|
||||||
|
try {
|
||||||
|
const setting = this.$store.getters[`${ this.inStore }/byId`](HCI.SETTING, HCI_SETTING.CSI_DRIVER_CONFIG);
|
||||||
|
const config = JSON.parse(setting?.value || setting?.default || '{}');
|
||||||
|
const defaultClass = config?.[this.provisioner]?.volumeSnapshotClassName || null;
|
||||||
|
|
||||||
|
const allClasses = this.$store.getters[`${ this.inStore }/all`](VOLUME_SNAPSHOT_CLASS) || [];
|
||||||
|
const matched = allClasses.find((cls) => cls.name === defaultClass && cls.driver === this.provisioner);
|
||||||
|
|
||||||
|
this.cdiSettings.volumeSnapshotClass = matched?.name || null;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse CSI config:', e); // eslint-disable-line no-console
|
||||||
|
this.cdiSettings.volumeSnapshotClass = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
initCDISettingsFromAnnotations() {
|
||||||
|
const annotations = this.value?.metadata?.annotations || {};
|
||||||
|
|
||||||
|
let volumeModeAccessModes = [];
|
||||||
|
const rawVolumeMode = annotations[HCI_ANNOTATIONS.VOLUME_MODE_ACCESS_MODES];
|
||||||
|
|
||||||
|
if (rawVolumeMode) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(rawVolumeMode);
|
||||||
|
|
||||||
|
volumeModeAccessModes = Object.entries(parsed).map(([volumeMode, accessModes]) => ({
|
||||||
|
volumeMode,
|
||||||
|
accessModes: Array.isArray(accessModes) ? accessModes : [],
|
||||||
|
}));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse annotation:', e); // eslint-disable-line no-console
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (volumeModeAccessModes.length) {
|
||||||
|
this.cdiSettings.volumeModeAccessModes = volumeModeAccessModes;
|
||||||
|
}
|
||||||
|
if (annotations[HCI_ANNOTATIONS.VOLUME_SNAPSHOT_CLASS]) {
|
||||||
|
this.cdiSettings.volumeSnapshotClass = annotations[HCI_ANNOTATIONS.VOLUME_SNAPSHOT_CLASS];
|
||||||
|
}
|
||||||
|
if (annotations[HCI_ANNOTATIONS.CLONE_STRATEGY]) {
|
||||||
|
this.cdiSettings.cloneStrategy = annotations[HCI_ANNOTATIONS.CLONE_STRATEGY];
|
||||||
|
}
|
||||||
|
if (annotations[HCI_ANNOTATIONS.FILESYSTEM_OVERHEAD]) {
|
||||||
|
this.cdiSettings.filesystemOverhead = annotations[HCI_ANNOTATIONS.FILESYSTEM_OVERHEAD];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
resetCdiSettings() {
|
||||||
|
this.cdiSettings.volumeModeAccessModes = [this.defaultAddValue];
|
||||||
|
this.cdiSettings.volumeSnapshotClass = null;
|
||||||
|
this.cdiSettings.cloneStrategy = null;
|
||||||
|
this.cdiSettings.filesystemOverhead = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
validateFilesystemOverhead(e) {
|
||||||
|
const val = e.target.value;
|
||||||
|
|
||||||
|
this.cdiSettings.filesystemOverhead = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ArrayList
|
||||||
|
v-model:value="cdiSettings.volumeModeAccessModes"
|
||||||
|
:initial-empty-row="true"
|
||||||
|
:show-header="true"
|
||||||
|
:mode="mode"
|
||||||
|
:title="t('harvester.storage.cdiSettings.volumeModeAccessModes.label')"
|
||||||
|
:add-label="t('harvester.storage.cdiSettings.volumeModeAccessModes.add')"
|
||||||
|
:default-add-value="defaultAddValue"
|
||||||
|
:protip="t('harvester.storage.cdiSettings.volumeModeAccessModes.tooltip')"
|
||||||
|
:add-disabled="allVolumeModesSelected"
|
||||||
|
>
|
||||||
|
<template #column-headers>
|
||||||
|
<div class="column-headers">
|
||||||
|
<div
|
||||||
|
class="row"
|
||||||
|
:class="{ custom: isCustomClass }"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="col span-3 value text-label mb-10"
|
||||||
|
for="volumeMode"
|
||||||
|
>
|
||||||
|
{{ t('harvester.storage.cdiSettings.volumeModeAccessModes.volumeMode') }}
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
class="col span-9 value text-label mb-10"
|
||||||
|
for="accessModes"
|
||||||
|
>
|
||||||
|
{{ t('harvester.storage.cdiSettings.volumeModeAccessModes.accessModes.label') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #columns="{ row }">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-3">
|
||||||
|
<LabeledSelect
|
||||||
|
id="volumeMode"
|
||||||
|
v-model:value="row.value.volumeMode"
|
||||||
|
:mode="mode"
|
||||||
|
:options="volumeModeOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
id="accessModes"
|
||||||
|
class="col span-9"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
v-for="opt in accessModeOptions"
|
||||||
|
:key="opt.value"
|
||||||
|
v-model:value="row.value.accessModes"
|
||||||
|
:value-when-true="opt.value"
|
||||||
|
:label="opt.label"
|
||||||
|
type="checkbox"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ArrayList>
|
||||||
|
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="cdiSettings.volumeSnapshotClass"
|
||||||
|
class="select mt-20 mb-20"
|
||||||
|
:label="t('harvester.storage.cdiSettings.volumeSnapshotClass.label')"
|
||||||
|
:tooltip="t('harvester.storage.cdiSettings.volumeSnapshotClass.tooltip')"
|
||||||
|
:mode="mode"
|
||||||
|
:options="volumeSnapshotClassOptions"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="cdiSettings.cloneStrategy"
|
||||||
|
class="select mb-20"
|
||||||
|
:label="t('harvester.storage.cdiSettings.cloneStrategy.label')"
|
||||||
|
:tooltip="t('harvester.storage.cdiSettings.cloneStrategy.tooltip')"
|
||||||
|
:mode="mode"
|
||||||
|
:options="cloneStrategyOptions"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="cdiSettings.filesystemOverhead"
|
||||||
|
class="select mb-20"
|
||||||
|
:label="t('harvester.storage.cdiSettings.fileSystemOverhead.label')"
|
||||||
|
:tooltip="t('harvester.storage.cdiSettings.fileSystemOverhead.tooltip')"
|
||||||
|
:mode="mode"
|
||||||
|
type="number"
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:step="0.001"
|
||||||
|
:placeholder="t('harvester.storage.cdiSettings.fileSystemOverhead.placeholder')"
|
||||||
|
:status="isFilesystemOverheadValid ? null : 'error'"
|
||||||
|
@input="validateFilesystemOverhead"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.column-headers .row.custom {
|
||||||
|
max-width: calc(100% - 75px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select {
|
||||||
|
max-width: 480px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
import CruResource from '@shell/components/CruResource';
|
import CruResource from '@shell/components/CruResource';
|
||||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||||
@@ -6,11 +7,9 @@ import ArrayList from '@shell/components/form/ArrayList';
|
|||||||
import Tab from '@shell/components/Tabbed/Tab';
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
import Tabbed from '@shell/components/Tabbed';
|
import Tabbed from '@shell/components/Tabbed';
|
||||||
import { RadioGroup } from '@components/Form/Radio';
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
|
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
import Loading from '@shell/components/Loading';
|
import Loading from '@shell/components/Loading';
|
||||||
|
|
||||||
import { _CREATE, _VIEW } from '@shell/config/query-params';
|
import { _CREATE, _VIEW } from '@shell/config/query-params';
|
||||||
import { mapFeature, UNSUPPORTED_STORAGE_DRIVERS } from '@shell/store/features';
|
import { mapFeature, UNSUPPORTED_STORAGE_DRIVERS } from '@shell/store/features';
|
||||||
import {
|
import {
|
||||||
@@ -22,6 +21,7 @@ import { CSI_DRIVER } from '../../types';
|
|||||||
import Tags from '../../components/DiskTags';
|
import Tags from '../../components/DiskTags';
|
||||||
import { DATA_ENGINE_V1, DATA_ENGINE_V2 } from '../../models/harvester/persistentvolumeclaim';
|
import { DATA_ENGINE_V1, DATA_ENGINE_V2 } from '../../models/harvester/persistentvolumeclaim';
|
||||||
import { LVM_DRIVER } from '../../models/harvester/storage.k8s.io.storageclass';
|
import { LVM_DRIVER } from '../../models/harvester/storage.k8s.io.storageclass';
|
||||||
|
import CDISettings from './CDISettings';
|
||||||
|
|
||||||
export const LVM_TOPOLOGY_LABEL = 'topology.lvm.csi/node';
|
export const LVM_TOPOLOGY_LABEL = 'topology.lvm.csi/node';
|
||||||
|
|
||||||
@@ -45,6 +45,7 @@ export default {
|
|||||||
Tabbed,
|
Tabbed,
|
||||||
Loading,
|
Loading,
|
||||||
Tags,
|
Tags,
|
||||||
|
CDISettings,
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [CreateEditView],
|
mixins: [CreateEditView],
|
||||||
@@ -117,6 +118,7 @@ export default {
|
|||||||
key: '',
|
key: '',
|
||||||
values: [],
|
values: [],
|
||||||
},
|
},
|
||||||
|
cdiSettings: {},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -124,10 +126,10 @@ export default {
|
|||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
const hash = {
|
const hash = {
|
||||||
namespaces: this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE }),
|
namespaces: this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE }),
|
||||||
storages: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }),
|
storages: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }),
|
||||||
longhornNodes: this.$store.dispatch(`${ inStore }/findAll`, { type: LONGHORN.NODES }),
|
longhornNodes: this.$store.dispatch(`${ inStore }/findAll`, { type: LONGHORN.NODES }),
|
||||||
csiDrivers: this.$store.dispatch(`${ inStore }/findAll`, { type: CSI_DRIVER })
|
csiDrivers: this.$store.dispatch(`${ inStore }/findAll`, { type: CSI_DRIVER }),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.value.longhornV2LVMSupport) {
|
if (this.value.longhornV2LVMSupport) {
|
||||||
@@ -197,6 +199,14 @@ export default {
|
|||||||
|
|
||||||
return v2DataEngine.value === 'true' ? DATA_ENGINE_V2 : DATA_ENGINE_V1;
|
return v2DataEngine.value === 'true' ? DATA_ENGINE_V2 : DATA_ENGINE_V1;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isCDISettingsFeatureEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('cdiSettings');
|
||||||
|
},
|
||||||
|
|
||||||
|
shouldShowCDISettingsTab() {
|
||||||
|
return this.isCDISettingsFeatureEnabled && this.provisioner !== `${ LONGHORN_DRIVER }_${ DATA_ENGINE_V1 }`;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
@@ -249,6 +259,10 @@ export default {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.formatAllowedTopoloties();
|
this.formatAllowedTopoloties();
|
||||||
|
|
||||||
|
if (this.shouldShowCDISettingsTab) {
|
||||||
|
this.formatCDISettings();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
formatAllowedTopoloties() {
|
formatAllowedTopoloties() {
|
||||||
@@ -270,6 +284,42 @@ export default {
|
|||||||
if (matchLabelExpressions.length > 0) {
|
if (matchLabelExpressions.length > 0) {
|
||||||
this.value.allowedTopologies = [{ matchLabelExpressions: [...matchLabelExpressions, ...lvmMatchExpression] }];
|
this.value.allowedTopologies = [{ matchLabelExpressions: [...matchLabelExpressions, ...lvmMatchExpression] }];
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
formatCDISettings() {
|
||||||
|
const annotations = this.value.metadata.annotations || {};
|
||||||
|
const volumeModeAccessModes = {};
|
||||||
|
|
||||||
|
this.cdiSettings.volumeModeAccessModes.forEach((setting) => {
|
||||||
|
if (setting.volumeMode && Array.isArray(setting.accessModes) && setting.accessModes.length > 0) {
|
||||||
|
volumeModeAccessModes[setting.volumeMode] = setting.accessModes;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(volumeModeAccessModes).length > 0) {
|
||||||
|
annotations[HCI_ANNOTATIONS.VOLUME_MODE_ACCESS_MODES] = JSON.stringify(volumeModeAccessModes);
|
||||||
|
} else {
|
||||||
|
delete annotations[HCI_ANNOTATIONS.VOLUME_MODE_ACCESS_MODES];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.cdiSettings.volumeSnapshotClass) {
|
||||||
|
annotations[HCI_ANNOTATIONS.VOLUME_SNAPSHOT_CLASS] = this.cdiSettings.volumeSnapshotClass;
|
||||||
|
} else {
|
||||||
|
delete annotations[HCI_ANNOTATIONS.VOLUME_SNAPSHOT_CLASS];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.cdiSettings.cloneStrategy) {
|
||||||
|
annotations[HCI_ANNOTATIONS.CLONE_STRATEGY] = this.cdiSettings.cloneStrategy;
|
||||||
|
} else {
|
||||||
|
delete annotations[HCI_ANNOTATIONS.CLONE_STRATEGY];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.cdiSettings.filesystemOverhead) {
|
||||||
|
annotations[HCI_ANNOTATIONS.FILESYSTEM_OVERHEAD] = this.cdiSettings.filesystemOverhead;
|
||||||
|
} else {
|
||||||
|
delete annotations[HCI_ANNOTATIONS.FILESYSTEM_OVERHEAD];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.value.metadata.annotations = annotations;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -286,7 +336,7 @@ export default {
|
|||||||
:validation-passed="true"
|
:validation-passed="true"
|
||||||
:apply-hooks="applyHooks"
|
:apply-hooks="applyHooks"
|
||||||
:errors="errors"
|
:errors="errors"
|
||||||
@error="e=>errors = e"
|
@error="e=>errors=e"
|
||||||
@finish="save"
|
@finish="save"
|
||||||
@cancel="done"
|
@cancel="done"
|
||||||
>
|
>
|
||||||
@@ -404,6 +454,19 @@ export default {
|
|||||||
</template>
|
</template>
|
||||||
</ArrayList>
|
</ArrayList>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
v-if="shouldShowCDISettingsTab"
|
||||||
|
name="cdiSettings"
|
||||||
|
:label="t('harvester.storage.cdiSettings.label')"
|
||||||
|
:weight="-2"
|
||||||
|
>
|
||||||
|
<CDISettings
|
||||||
|
v-model:cdi-settings="cdiSettings"
|
||||||
|
:value="value"
|
||||||
|
:mode="mode"
|
||||||
|
:provisioner="value.provisioner"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
</Tabbed>
|
</Tabbed>
|
||||||
</CruResource>
|
</CruResource>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { VM_IMAGE_FILE_FORMAT } from '../validators/vm-image';
|
|||||||
import { OS } from '../mixins/harvester-vm';
|
import { OS } from '../mixins/harvester-vm';
|
||||||
import { HCI } from '../types';
|
import { HCI } from '../types';
|
||||||
import { LVM_DRIVER } from '../models/harvester/storage.k8s.io.storageclass';
|
import { LVM_DRIVER } from '../models/harvester/storage.k8s.io.storageclass';
|
||||||
|
import { isInternalStorageClass } from '../utils/storage-class';
|
||||||
|
|
||||||
const ENCRYPT = 'encrypt';
|
const ENCRYPT = 'encrypt';
|
||||||
const DECRYPT = 'decrypt';
|
const DECRYPT = 'decrypt';
|
||||||
@@ -110,6 +111,7 @@ export default {
|
|||||||
headers: {},
|
headers: {},
|
||||||
fileUrl: '',
|
fileUrl: '',
|
||||||
file: '',
|
file: '',
|
||||||
|
HCI_ANNOTATIONS,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -167,11 +169,18 @@ export default {
|
|||||||
|
|
||||||
return filteredStorages
|
return filteredStorages
|
||||||
.map((s) => {
|
.map((s) => {
|
||||||
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
let label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||||
|
let disabled = false;
|
||||||
|
|
||||||
|
if (isInternalStorageClass(s.name)) {
|
||||||
|
label += ` (${ this.t('harvester.storage.internal.label') })`;
|
||||||
|
disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label,
|
label,
|
||||||
value: s.name,
|
value: s.name,
|
||||||
|
disabled
|
||||||
};
|
};
|
||||||
}) || [];
|
}) || [];
|
||||||
},
|
},
|
||||||
@@ -595,6 +604,8 @@ export default {
|
|||||||
:pad-left="false"
|
:pad-left="false"
|
||||||
:read-allowed="false"
|
:read-allowed="false"
|
||||||
:value-can-be-empty="true"
|
:value-can-be-empty="true"
|
||||||
|
:toggle-filter="true"
|
||||||
|
:protected-keys="[HCI_ANNOTATIONS.IMAGE_DISPLAY_NAME]"
|
||||||
@focusKey="focusKey"
|
@focusKey="focusKey"
|
||||||
@update:value="value.setLabels($event)"
|
@update:value="value.setLabels($event)"
|
||||||
>
|
>
|
||||||
@@ -611,7 +622,7 @@ export default {
|
|||||||
<input
|
<input
|
||||||
v-else
|
v-else
|
||||||
v-model="row[valueName]"
|
v-model="row[valueName]"
|
||||||
:disabled="isView"
|
:disabled="isView || row[keyName] === HCI_ANNOTATIONS.IMAGE_DISPLAY_NAME"
|
||||||
:type="'text'"
|
:type="'text'"
|
||||||
:placeholder="t('keyValue.valuePlaceholder')"
|
:placeholder="t('keyValue.valuePlaceholder')"
|
||||||
autocorrect="off"
|
autocorrect="off"
|
||||||
|
|||||||
@@ -189,6 +189,16 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.value.spec['templateId'] = `${ namespace }/${ name }`;
|
this.value.spec['templateId'] = `${ namespace }/${ name }`;
|
||||||
|
|
||||||
|
// inherit labels and annotations so the VM gets them when created from the template
|
||||||
|
this.value.spec.vm.metadata.labels = {
|
||||||
|
...this.value.spec.vm.metadata.labels,
|
||||||
|
...this.value.metadata.labels
|
||||||
|
};
|
||||||
|
this.value.spec.vm.metadata.annotations = {
|
||||||
|
...this.value.spec.vm.metadata.annotations,
|
||||||
|
...this.value.metadata.annotations
|
||||||
|
};
|
||||||
const res = await this.value.save();
|
const res = await this.value.save();
|
||||||
|
|
||||||
await this.saveSecret(res);
|
await this.saveSecret(res);
|
||||||
@@ -219,6 +229,7 @@ export default {
|
|||||||
:errors="errors"
|
:errors="errors"
|
||||||
:apply-hooks="applyHooks"
|
:apply-hooks="applyHooks"
|
||||||
@finish="saveVMT"
|
@finish="saveVMT"
|
||||||
|
@error="e=>errors=e"
|
||||||
>
|
>
|
||||||
<NameNsDescription
|
<NameNsDescription
|
||||||
:value="templateValue"
|
:value="templateValue"
|
||||||
@@ -246,8 +257,12 @@ export default {
|
|||||||
:label="t('harvester.vmTemplate.tabs.basics')"
|
:label="t('harvester.vmTemplate.tabs.basics')"
|
||||||
>
|
>
|
||||||
<CpuMemory
|
<CpuMemory
|
||||||
|
class="mb-20"
|
||||||
:cpu="cpu"
|
:cpu="cpu"
|
||||||
:memory="memory"
|
:memory="memory"
|
||||||
|
:max-cpu="maxCpu"
|
||||||
|
:max-memory="maxMemory"
|
||||||
|
:enable-hot-plug="cpuMemoryHotplugEnabled"
|
||||||
:disabled="isConfig"
|
:disabled="isConfig"
|
||||||
@updateCpuMemory="updateCpuMemory"
|
@updateCpuMemory="updateCpuMemory"
|
||||||
/>
|
/>
|
||||||
@@ -358,6 +373,26 @@ export default {
|
|||||||
@update:value="value.setInstanceLabels($event)"
|
@update:value="value.setInstanceLabels($event)"
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
name="annotations"
|
||||||
|
:label="t('harvester.tab.annotations')"
|
||||||
|
:weight="-11"
|
||||||
|
>
|
||||||
|
<Banner color="info">
|
||||||
|
<t k="harvester.virtualMachine.annotations.banner" />
|
||||||
|
</Banner>
|
||||||
|
<KeyValue
|
||||||
|
key="annotations"
|
||||||
|
:value="value.annotations"
|
||||||
|
:protected-keys="value.systemAnnotations || []"
|
||||||
|
:toggle-filter="true"
|
||||||
|
:add-label="t('labels.addAnnotation')"
|
||||||
|
:mode="mode"
|
||||||
|
:read-allowed="false"
|
||||||
|
:value-can-be-empty="true"
|
||||||
|
@update:value="value.setAnnotations($event)"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
name="advanced"
|
name="advanced"
|
||||||
:label="t('harvester.tab.advanced')"
|
:label="t('harvester.tab.advanced')"
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { LVM_DRIVER } from '../models/harvester/storage.k8s.io.storageclass';
|
|||||||
import { DATA_ENGINE_V2 } from '../models/harvester/persistentvolumeclaim';
|
import { DATA_ENGINE_V2 } from '../models/harvester/persistentvolumeclaim';
|
||||||
import { GIBIBYTE } from '../utils/unit';
|
import { GIBIBYTE } from '../utils/unit';
|
||||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
||||||
|
import { isInternalStorageClass } from '../utils/storage-class';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterVolume',
|
name: 'HarvesterVolume',
|
||||||
@@ -206,11 +207,18 @@ export default {
|
|||||||
|
|
||||||
storageClassOptions() {
|
storageClassOptions() {
|
||||||
return this.storageClasses.filter((s) => !s.parameters?.backingImage).map((s) => {
|
return this.storageClasses.filter((s) => !s.parameters?.backingImage).map((s) => {
|
||||||
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
let label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||||
|
let disabled = false;
|
||||||
|
|
||||||
|
if (isInternalStorageClass(s.name)) {
|
||||||
|
label += ` (${ this.t('harvester.storage.internal.label') })`;
|
||||||
|
disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label,
|
label,
|
||||||
value: s.name,
|
value: s.name,
|
||||||
|
disabled
|
||||||
};
|
};
|
||||||
}) || [];
|
}) || [];
|
||||||
},
|
},
|
||||||
@@ -304,6 +312,10 @@ export default {
|
|||||||
return ['ReadWriteMany'];
|
return ['ReadWriteMany'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.value?.spec?.accessModes && this.value?.spec?.accessModes?.length > 0) {
|
||||||
|
return this.value.spec.accessModes;
|
||||||
|
}
|
||||||
|
|
||||||
const storageClassName = this.value.spec.storageClassName;
|
const storageClassName = this.value.spec.storageClassName;
|
||||||
const storageClass = this.storageClasses.find((sc) => sc.name === storageClassName);
|
const storageClass = this.storageClasses.find((sc) => sc.name === storageClassName);
|
||||||
|
|
||||||
@@ -374,10 +386,12 @@ export default {
|
|||||||
:generate-yaml="generateYaml"
|
:generate-yaml="generateYaml"
|
||||||
:apply-hooks="applyHooks"
|
:apply-hooks="applyHooks"
|
||||||
@finish="save"
|
@finish="save"
|
||||||
|
@error="e=>errors=e"
|
||||||
>
|
>
|
||||||
<NameNsDescription
|
<NameNsDescription
|
||||||
:value="value"
|
:value="value"
|
||||||
:namespaced="true"
|
:namespaced="true"
|
||||||
|
:name-required="false"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
@update:value="$emit('update:value', $event)"
|
@update:value="$emit('update:value', $event)"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ export default {
|
|||||||
:errors="errors"
|
:errors="errors"
|
||||||
:apply-hooks="applyHooks"
|
:apply-hooks="applyHooks"
|
||||||
@finish="save"
|
@finish="save"
|
||||||
|
@error="e=>errors=e"
|
||||||
>
|
>
|
||||||
<NameNsDescription
|
<NameNsDescription
|
||||||
:value="value"
|
:value="value"
|
||||||
|
|||||||
@@ -0,0 +1,323 @@
|
|||||||
|
<script>
|
||||||
|
import CruResource from '@shell/components/CruResource';
|
||||||
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import { NETWORK_ATTACHMENT } from '@shell/config/types';
|
||||||
|
import Loading from '@shell/components/Loading';
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
import { NETWORK_PROTOCOL, NETWORK_TYPE } from '@pkg/harvester/config/types';
|
||||||
|
import { set } from '@shell/utils/object';
|
||||||
|
import ArrayList from '@shell/components/form/ArrayList';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { HCI } from '../../types';
|
||||||
|
import ResourceTabs from '@shell/components/form/ResourceTabs/index';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'EditSubnet',
|
||||||
|
|
||||||
|
emits: ['update:value'],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
CruResource,
|
||||||
|
LabeledInput,
|
||||||
|
LabeledSelect,
|
||||||
|
NameNsDescription,
|
||||||
|
Tab,
|
||||||
|
RadioGroup,
|
||||||
|
ArrayList,
|
||||||
|
ResourceTabs,
|
||||||
|
Loading,
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
inheritAttrs: false,
|
||||||
|
|
||||||
|
created() {
|
||||||
|
const vpc = this.$route.query.vpc || '';
|
||||||
|
|
||||||
|
set(this.value, 'spec', this.value.spec || {
|
||||||
|
cidrBlock: '',
|
||||||
|
protocol: NETWORK_PROTOCOL.IPv4,
|
||||||
|
provider: '',
|
||||||
|
vpc,
|
||||||
|
gatewayIP: '',
|
||||||
|
excludeIps: [],
|
||||||
|
private: false
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
const hash = {
|
||||||
|
vpc: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VPC }),
|
||||||
|
nad: this.$store.dispatch(`${ inStore }/findAll`, { type: NETWORK_ATTACHMENT }),
|
||||||
|
};
|
||||||
|
|
||||||
|
await allHash(hash);
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
showAllowSubnets() {
|
||||||
|
return this.value?.spec?.private === true;
|
||||||
|
},
|
||||||
|
|
||||||
|
doneLocationOverride() {
|
||||||
|
return this.value.doneOverride;
|
||||||
|
},
|
||||||
|
|
||||||
|
allowSubnetTooltip() {
|
||||||
|
return this.t('harvester.subnet.allowSubnet.tooltip', null, true);
|
||||||
|
},
|
||||||
|
|
||||||
|
excludeIPsTooltip() {
|
||||||
|
return this.t('harvester.subnet.excludeIPs.tooltip', null, true);
|
||||||
|
},
|
||||||
|
|
||||||
|
protocolOptions() {
|
||||||
|
return Object.values(NETWORK_PROTOCOL);
|
||||||
|
},
|
||||||
|
|
||||||
|
provider: {
|
||||||
|
get() {
|
||||||
|
const raw = this.value.spec.provider;
|
||||||
|
|
||||||
|
if (!raw) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const vmNet = raw.split('.')[0] || '';
|
||||||
|
const ns = raw.split('.')[1] || '';
|
||||||
|
|
||||||
|
return `${ ns }/${ vmNet }`;
|
||||||
|
},
|
||||||
|
|
||||||
|
set(value) {
|
||||||
|
const ns = value.split('/')[0] || '';
|
||||||
|
const vmNet = value.split('/')[1] || '';
|
||||||
|
const provider = `${ vmNet }.${ ns }.ovn`;
|
||||||
|
|
||||||
|
set(this.value, 'spec.provider', provider);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
providerOptions() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const vmNets = this.$store.getters[`${ inStore }/all`](NETWORK_ATTACHMENT) || [];
|
||||||
|
|
||||||
|
return vmNets.filter((net) => net.vlanType === NETWORK_TYPE.OVERLAY).map((n) => ({
|
||||||
|
label: n.id,
|
||||||
|
value: n.id,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
vpcOptions() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const vpcs = this.$store.getters[`${ inStore }/all`](HCI.VPC) || [];
|
||||||
|
|
||||||
|
return vpcs.map((n) => ({
|
||||||
|
label: n.id,
|
||||||
|
value: n.id,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async saveSubnet(buttonCb) {
|
||||||
|
const errors = [];
|
||||||
|
const name = this.value?.metadata?.name;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!name) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('generic.name') }, true));
|
||||||
|
} else if (!this.value?.spec?.cidrBlock) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.subnet.cidrBlock.label') }, true));
|
||||||
|
} else if (!this.value?.spec?.provider) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.subnet.provider.label') }, true));
|
||||||
|
} else if (this.value.spec.excludeIps.includes('')) {
|
||||||
|
errors.push(this.t('harvester.validation.subnet.excludeIps'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
buttonCb(false);
|
||||||
|
this.errors = errors;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
await this.value.save();
|
||||||
|
buttonCb(true);
|
||||||
|
this.done();
|
||||||
|
} catch (e) {
|
||||||
|
this.errors = [e];
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Loading v-if="$fetchState.pending" />
|
||||||
|
<CruResource
|
||||||
|
v-else
|
||||||
|
:done-route="doneRoute"
|
||||||
|
:resource="value"
|
||||||
|
:mode="mode"
|
||||||
|
:apply-hooks="applyHooks"
|
||||||
|
:errors="errors"
|
||||||
|
@finish="saveSubnet"
|
||||||
|
@error="e=>errors=e"
|
||||||
|
>
|
||||||
|
<NameNsDescription
|
||||||
|
:value="value"
|
||||||
|
:mode="mode"
|
||||||
|
:namespaced="false"
|
||||||
|
@update:value="$emit('update:value', $event)"
|
||||||
|
/>
|
||||||
|
<ResourceTabs
|
||||||
|
class="mt-15"
|
||||||
|
:need-events="false"
|
||||||
|
:need-related="false"
|
||||||
|
:mode="mode"
|
||||||
|
:side-tabs="true"
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
name="Basic"
|
||||||
|
:label="t('generic.basic')"
|
||||||
|
:weight="-1"
|
||||||
|
class="bordered-table"
|
||||||
|
>
|
||||||
|
<div class="row mt-10">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="value.spec.cidrBlock"
|
||||||
|
class="mb-20"
|
||||||
|
required
|
||||||
|
:placeholder="t('harvester.subnet.cidrBlock.placeholder')"
|
||||||
|
:label="t('harvester.subnet.cidrBlock.label')"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="value.spec.protocol"
|
||||||
|
:label="t('harvester.subnet.protocol.label')"
|
||||||
|
:options="protocolOptions"
|
||||||
|
required
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-10">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="provider"
|
||||||
|
:label="t('harvester.subnet.provider.label')"
|
||||||
|
:options="providerOptions"
|
||||||
|
:tooltip="t('harvester.subnet.provider.tooltip')"
|
||||||
|
required
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="value.spec.vpc"
|
||||||
|
:label="t('harvester.subnet.vpc.label')"
|
||||||
|
:options="vpcOptions"
|
||||||
|
required
|
||||||
|
:disabled="true"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="value.spec.gateway"
|
||||||
|
class="mb-20"
|
||||||
|
:placeholder="t('harvester.subnet.gateway.placeholder')"
|
||||||
|
:label="t('harvester.subnet.gateway.label')"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<RadioGroup
|
||||||
|
v-model:value="value.spec.private"
|
||||||
|
name="enabled"
|
||||||
|
:options="[true, false]"
|
||||||
|
:label="t('harvester.subnet.private.label')"
|
||||||
|
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||||
|
:mode="mode"
|
||||||
|
tooltip-key="harvester.subnet.private.tooltip"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ArrayList
|
||||||
|
v-if="showAllowSubnets"
|
||||||
|
v-model:value="value.spec.allowSubnets"
|
||||||
|
:show-header="true"
|
||||||
|
class="mt-20"
|
||||||
|
:mode="mode"
|
||||||
|
:add-label="t('harvester.subnet.allowSubnet.addSubnet')"
|
||||||
|
>
|
||||||
|
<template #column-headers>
|
||||||
|
<div class="box">
|
||||||
|
<h3 class="key">
|
||||||
|
{{ t('harvester.subnet.allowSubnet.label') }}
|
||||||
|
<i
|
||||||
|
v-clean-tooltip="{content: allowSubnetTooltip, triggers: ['hover', 'touch', 'focus'] }"
|
||||||
|
v-stripped-aria-label="allowSubnetTooltip"
|
||||||
|
class="icon icon-info"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #columns="scope">
|
||||||
|
<div class="key">
|
||||||
|
<input
|
||||||
|
v-model="scope.row.value"
|
||||||
|
:placeholder="t('harvester.subnet.allowSubnet.placeholder')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ArrayList>
|
||||||
|
<ArrayList
|
||||||
|
v-model:value="value.spec.excludeIps"
|
||||||
|
:show-header="true"
|
||||||
|
class="mt-20"
|
||||||
|
:mode="mode"
|
||||||
|
:add-label="t('harvester.setting.storageNetwork.exclude.addIp')"
|
||||||
|
>
|
||||||
|
<template #column-headers>
|
||||||
|
<div class="box">
|
||||||
|
<h3 class="key">
|
||||||
|
{{ t('harvester.setting.storageNetwork.exclude.label') }}
|
||||||
|
<i
|
||||||
|
v-clean-tooltip="{content: excludeIPsTooltip, triggers: ['hover', 'touch', 'focus'] }"
|
||||||
|
v-stripped-aria-label="excludeIPsTooltip"
|
||||||
|
class="icon icon-info"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #columns="scope">
|
||||||
|
<div class="key">
|
||||||
|
<input
|
||||||
|
v-model="scope.row.value"
|
||||||
|
:placeholder="t('harvester.setting.storageNetwork.exclude.placeholder')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ArrayList>
|
||||||
|
</Tab>
|
||||||
|
</ResourceTabs>
|
||||||
|
</CruResource>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
<script>
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
|
import { _EDIT, _VIEW } from '@shell/config/query-params';
|
||||||
|
import { removeAt } from '@shell/utils/array';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'StaticRoutes',
|
||||||
|
|
||||||
|
emits: ['update:value'],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Array,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: _EDIT,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const rows = (this.value || []).map((row) => {
|
||||||
|
return {
|
||||||
|
cidr: row.cidr || '',
|
||||||
|
nextHopIP: row.nextHopIP || '',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return { rows };
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
isView() {
|
||||||
|
return this.mode === _VIEW;
|
||||||
|
},
|
||||||
|
|
||||||
|
showAdd() {
|
||||||
|
return !this.isView;
|
||||||
|
},
|
||||||
|
|
||||||
|
showRemove() {
|
||||||
|
return !this.isView;
|
||||||
|
},
|
||||||
|
nextHopIPTooltip() {
|
||||||
|
return this.t('harvester.vpc.staticRoutes.nextHopIP.tooltip');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.queueUpdate = debounce(this.update, 100);
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
add() {
|
||||||
|
this.rows.push({
|
||||||
|
cidr: '',
|
||||||
|
nextHopIP: '',
|
||||||
|
});
|
||||||
|
this.queueUpdate();
|
||||||
|
},
|
||||||
|
|
||||||
|
remove(idx) {
|
||||||
|
removeAt(this.rows, idx);
|
||||||
|
this.queueUpdate();
|
||||||
|
},
|
||||||
|
|
||||||
|
update() {
|
||||||
|
if (this.isView) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit('update:value', this.rows);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
v-if="rows.length"
|
||||||
|
class="static-route-row"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(row, idx) in rows"
|
||||||
|
:key="idx"
|
||||||
|
>
|
||||||
|
<div class="pool-headers cidr">
|
||||||
|
<span class="pool-cidr">
|
||||||
|
<t k="harvester.vpc.staticRoutes.cidr.label" />
|
||||||
|
</span>
|
||||||
|
<span class="pool-nextHopIP">
|
||||||
|
<t k="harvester.vpc.staticRoutes.nextHopIP.label" />
|
||||||
|
<i
|
||||||
|
v-clean-tooltip="{content: nextHopIPTooltip, triggers: ['hover', 'touch', 'focus'] }"
|
||||||
|
v-stripped-aria-label="nextHopIPTooltip"
|
||||||
|
class="icon icon-info"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="pool-row cidr">
|
||||||
|
<div class="pool-cidr">
|
||||||
|
<span v-if="isView">
|
||||||
|
{{ row.cidr }}
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
v-else
|
||||||
|
v-model="row.cidr"
|
||||||
|
type="text"
|
||||||
|
:placeholder="t('harvester.vpc.staticRoutes.cidr.placeholder')"
|
||||||
|
@input="queueUpdate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="pool-nextHopIP">
|
||||||
|
<span v-if="isView">
|
||||||
|
{{ row.nextHopIP }}
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
v-else
|
||||||
|
v-model="row.nextHopIP"
|
||||||
|
type="text"
|
||||||
|
:placeholder="t('harvester.vpc.staticRoutes.nextHopIP.placeholder')"
|
||||||
|
@input="queueUpdate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="showRemove"
|
||||||
|
type="button"
|
||||||
|
class="btn role-link pl-0"
|
||||||
|
@click="remove(idx)"
|
||||||
|
>
|
||||||
|
<t k="generic.remove" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="showAdd"
|
||||||
|
type="button"
|
||||||
|
class="btn role-tertiary add"
|
||||||
|
@click="add()"
|
||||||
|
>
|
||||||
|
<t k="generic.add" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.pool-headers, .pool-row {
|
||||||
|
display: grid;
|
||||||
|
grid-column-gap: $column-gutter;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&.cidr {
|
||||||
|
grid-template-columns: 40%+$column-gutter 40%+$column-gutter 15%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
<script>
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
|
import { _EDIT, _VIEW } from '@shell/config/query-params';
|
||||||
|
import { removeAt } from '@shell/utils/array';
|
||||||
|
import { HCI } from '../../types';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'VpcPeerings',
|
||||||
|
|
||||||
|
emits: ['update:value'],
|
||||||
|
|
||||||
|
components: { LabeledSelect },
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Array,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: _EDIT,
|
||||||
|
},
|
||||||
|
vpc: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
await this.$store.dispatch('harvester/findAll', { type: HCI.VPC });
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const rows = (this.value || []).map((row) => {
|
||||||
|
return {
|
||||||
|
localConnectIP: row.localConnectIP || '',
|
||||||
|
remoteVpc: row.remoteVpc || '',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return { rows };
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
isView() {
|
||||||
|
return this.mode === _VIEW;
|
||||||
|
},
|
||||||
|
|
||||||
|
isEdit() {
|
||||||
|
return this.mode === _EDIT;
|
||||||
|
},
|
||||||
|
|
||||||
|
showAdd() {
|
||||||
|
return !this.isView;
|
||||||
|
},
|
||||||
|
|
||||||
|
showRemove() {
|
||||||
|
return !this.isView;
|
||||||
|
},
|
||||||
|
|
||||||
|
remoteVpcOptions() {
|
||||||
|
const allVpcs = this.$store.getters['harvester/all'](HCI.VPC) || [];
|
||||||
|
|
||||||
|
// filter self vpc if editing
|
||||||
|
const vpcs = this.isEdit ? allVpcs.filter((v) => v.id !== this.vpc.id) : allVpcs;
|
||||||
|
|
||||||
|
return vpcs.map((n) => ({
|
||||||
|
label: n.id,
|
||||||
|
value: n.id,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
localConnectIPTooltip() {
|
||||||
|
return this.t('harvester.vpc.vpcPeerings.localConnectIP.tooltip');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.queueUpdate = debounce(this.update, 100);
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
add() {
|
||||||
|
this.rows.push({
|
||||||
|
localConnectIP: '',
|
||||||
|
remoteVpc: '',
|
||||||
|
});
|
||||||
|
this.queueUpdate();
|
||||||
|
},
|
||||||
|
|
||||||
|
remove(idx) {
|
||||||
|
removeAt(this.rows, idx);
|
||||||
|
this.queueUpdate();
|
||||||
|
},
|
||||||
|
|
||||||
|
update() {
|
||||||
|
if (this.isView) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$emit('update:value', this.rows);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
v-if="rows.length"
|
||||||
|
class="static-route-row"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(row, idx) in rows"
|
||||||
|
:key="idx"
|
||||||
|
>
|
||||||
|
<div class="pool-headers localConnectIP">
|
||||||
|
<span class="pool-localConnectIP">
|
||||||
|
<t k="harvester.vpc.vpcPeerings.localConnectIP.label" />
|
||||||
|
<i
|
||||||
|
v-clean-tooltip="{content: localConnectIPTooltip, triggers: ['hover', 'touch', 'focus'] }"
|
||||||
|
v-stripped-aria-label="localConnectPTooltip"
|
||||||
|
class="icon icon-info"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span class="pool-remoteVpc">
|
||||||
|
<t k="harvester.vpc.vpcPeerings.remoteVpc.label" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="pool-row localConnectIP">
|
||||||
|
<div class="pool-localConnectIP">
|
||||||
|
<span v-if="isView">
|
||||||
|
{{ row.localConnectIP }}
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
v-else
|
||||||
|
v-model="row.localConnectIP"
|
||||||
|
type="text"
|
||||||
|
:placeholder="t('harvester.vpc.vpcPeerings.localConnectIP.placeholder')"
|
||||||
|
@input="queueUpdate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="pool-remoteVpc">
|
||||||
|
<span v-if="isView">
|
||||||
|
{{ row.remoteVpc }}
|
||||||
|
</span>
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="row.remoteVpc"
|
||||||
|
:options="remoteVpcOptions"
|
||||||
|
:mode="mode"
|
||||||
|
@update:value="queueUpdate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="showRemove"
|
||||||
|
type="button"
|
||||||
|
class="btn role-link pl-0"
|
||||||
|
@click="remove(idx)"
|
||||||
|
>
|
||||||
|
<t k="generic.remove" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="showAdd"
|
||||||
|
type="button"
|
||||||
|
class="btn role-tertiary add"
|
||||||
|
@click="add()"
|
||||||
|
>
|
||||||
|
<t k="generic.add" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.pool-headers, .pool-row {
|
||||||
|
display: grid;
|
||||||
|
grid-column-gap: $column-gutter;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&.localConnectIP {
|
||||||
|
grid-template-columns: 40%+$column-gutter 40%+$column-gutter 15%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
<script>
|
||||||
|
import CruResource from '@shell/components/CruResource';
|
||||||
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||||
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
|
import Loading from '@shell/components/Loading';
|
||||||
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
import ResourceTabs from '@shell/components/form/ResourceTabs/index';
|
||||||
|
import StaticRoutes from './StaticRoutes';
|
||||||
|
import VpcPeerings from './VpcPeerings';
|
||||||
|
import { set } from '@shell/utils/object';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'EditVPC',
|
||||||
|
|
||||||
|
emits: ['update:value'],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
CruResource,
|
||||||
|
NameNsDescription,
|
||||||
|
Tab,
|
||||||
|
StaticRoutes,
|
||||||
|
ResourceTabs,
|
||||||
|
Loading,
|
||||||
|
VpcPeerings
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
inheritAttrs: false,
|
||||||
|
|
||||||
|
data() {
|
||||||
|
set(this.value, 'spec', this.value.spec || {
|
||||||
|
staticRoutes: [],
|
||||||
|
vpcPeerings: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
return { staticRoutes: [] };
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async saveVpc(buttonCb) {
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const name = this.value?.metadata?.name;
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('generic.name') }, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
buttonCb(false);
|
||||||
|
this.errors = errors;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
await this.value.save();
|
||||||
|
buttonCb(true);
|
||||||
|
this.done();
|
||||||
|
} catch (e) {
|
||||||
|
this.errors = [e];
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Loading v-if="$fetchState.pending" />
|
||||||
|
<CruResource
|
||||||
|
v-else
|
||||||
|
:done-route="doneRoute"
|
||||||
|
:resource="value"
|
||||||
|
:mode="mode"
|
||||||
|
:apply-hooks="applyHooks"
|
||||||
|
:errors="errors"
|
||||||
|
@finish="saveVpc"
|
||||||
|
@error="e=>errors=e"
|
||||||
|
>
|
||||||
|
<NameNsDescription
|
||||||
|
:value="value"
|
||||||
|
:mode="mode"
|
||||||
|
:namespaced="false"
|
||||||
|
@update:value="$emit('update:value', $event)"
|
||||||
|
/>
|
||||||
|
<ResourceTabs
|
||||||
|
class="mt-15"
|
||||||
|
:mode="mode"
|
||||||
|
:side-tabs="true"
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
name="staticRoutes"
|
||||||
|
:label="t('harvester.vpc.staticRoutes.label')"
|
||||||
|
:weight="-1"
|
||||||
|
class="bordered-table"
|
||||||
|
>
|
||||||
|
<StaticRoutes
|
||||||
|
v-model:value="value.spec.staticRoutes"
|
||||||
|
class="col span-12"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
name="vpcPeerings"
|
||||||
|
:label="t('harvester.vpc.vpcPeerings.label')"
|
||||||
|
:weight="-2"
|
||||||
|
class="bordered-table"
|
||||||
|
>
|
||||||
|
<VpcPeerings
|
||||||
|
v-model:value="value.spec.vpcPeerings"
|
||||||
|
class="col span-12"
|
||||||
|
:mode="mode"
|
||||||
|
:vpc="value"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
</ResourceTabs>
|
||||||
|
</CruResource>
|
||||||
|
</template>
|
||||||
+10
@@ -20,6 +20,10 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: 'create'
|
default: 'create'
|
||||||
},
|
},
|
||||||
|
osType: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
value: {
|
value: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
@@ -64,6 +68,12 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
|
osType(neu) {
|
||||||
|
if (neu === 'windows') {
|
||||||
|
this.id = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
value(neu) {
|
value(neu) {
|
||||||
this.yamlScript = neu;
|
this.yamlScript = neu;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
|
osType: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
userScript: {
|
userScript: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
@@ -203,6 +207,7 @@ export default {
|
|||||||
<DataTemplate
|
<DataTemplate
|
||||||
ref="userTemplate"
|
ref="userTemplate"
|
||||||
:value="userScript"
|
:value="userScript"
|
||||||
|
:os-type="osType"
|
||||||
type="user"
|
type="user"
|
||||||
:view-code="viewCode"
|
:view-code="viewCode"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
@@ -219,6 +224,7 @@ export default {
|
|||||||
ref="networkTemplate"
|
ref="networkTemplate"
|
||||||
:value="networkScript"
|
:value="networkScript"
|
||||||
type="network"
|
type="network"
|
||||||
|
:os-type="osType"
|
||||||
:view-code="viewCode"
|
:view-code="viewCode"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:config-id="configNetworkId"
|
:config-id="configNetworkId"
|
||||||
|
|||||||
@@ -2,23 +2,44 @@
|
|||||||
import UnitInput from '@shell/components/form/UnitInput';
|
import UnitInput from '@shell/components/form/UnitInput';
|
||||||
import InputOrDisplay from '@shell/components/InputOrDisplay';
|
import InputOrDisplay from '@shell/components/InputOrDisplay';
|
||||||
import { GIBIBYTE } from '../../utils/unit';
|
import { GIBIBYTE } from '../../utils/unit';
|
||||||
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
|
import { _VIEW } from '@shell/config/query-params';
|
||||||
|
import { HCI } from '../../types';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { HCI_SETTING } from '../../config/settings';
|
||||||
|
|
||||||
|
const DEFAULT_HOT_PLUG_TIMES = 4;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterEditCpuMemory',
|
name: 'HarvesterEditCpuMemory',
|
||||||
|
|
||||||
emits: ['updateCpuMemory'],
|
emits: ['updateCpuMemory'],
|
||||||
|
|
||||||
components: { UnitInput, InputOrDisplay },
|
components: {
|
||||||
|
UnitInput, InputOrDisplay, Checkbox
|
||||||
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
cpu: {
|
cpu: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
maxCpu: {
|
||||||
|
type: Number,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
memory: {
|
memory: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
maxMemory: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
enableHotPlug: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
mode: {
|
mode: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'create',
|
default: 'create',
|
||||||
@@ -29,21 +50,59 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
const res = await allHash({ settings: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SETTING }) });
|
||||||
|
|
||||||
|
this.settings = res.settings || [];
|
||||||
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
GIBIBYTE,
|
GIBIBYTE,
|
||||||
localCpu: this.cpu,
|
localCpu: this.cpu,
|
||||||
localMemory: this.memory
|
localMemory: this.memory,
|
||||||
|
maxLocalCpu: this.maxCpu,
|
||||||
|
maxLocalMemory: this.maxMemory,
|
||||||
|
localEnableHotPlug: this.enableHotPlug,
|
||||||
|
settings: []
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
cupDisplay() {
|
isView() {
|
||||||
|
return this.mode === _VIEW;
|
||||||
|
},
|
||||||
|
cpuDisplay() {
|
||||||
return `${ this.localCpu } C`;
|
return `${ this.localCpu } C`;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
maxCpuDisplay() {
|
||||||
|
return `${ this.maxLocalCpu } C`;
|
||||||
|
},
|
||||||
|
|
||||||
memoryDisplay() {
|
memoryDisplay() {
|
||||||
return `${ this.localMemory }`;
|
return `${ this.localMemory }`;
|
||||||
|
},
|
||||||
|
|
||||||
|
maxMemoryDisplay() {
|
||||||
|
return `${ this.maxLocalMemory }`;
|
||||||
|
},
|
||||||
|
|
||||||
|
cpuMemoryHotplugTooltip() {
|
||||||
|
return this.t('harvester.virtualMachine.hotplug.tooltip', { hotPlugTimes: this.maxHotplugRatio });
|
||||||
|
},
|
||||||
|
|
||||||
|
isCPUMemoryHotPlugFeatureEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('cpuMemoryHotplug');
|
||||||
|
},
|
||||||
|
|
||||||
|
maxHotplugRatio() {
|
||||||
|
const maxHotPlugRatioSetting = this.settings.find((s) => s.id === HCI_SETTING.MAX_HOTPLUG_RATIO);
|
||||||
|
const maxPlugRatio = maxHotPlugRatioSetting?.value ? parseInt(maxHotPlugRatioSetting.value, 10) : DEFAULT_HOT_PLUG_TIMES;
|
||||||
|
|
||||||
|
return maxPlugRatio;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -55,71 +114,161 @@ export default {
|
|||||||
if (neu && !neu.includes('null')) {
|
if (neu && !neu.includes('null')) {
|
||||||
this.localMemory = neu;
|
this.localMemory = neu;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
maxCpu(neu) {
|
||||||
|
this.maxLocalCpu = neu;
|
||||||
|
},
|
||||||
|
maxMemory(neu) {
|
||||||
|
if (neu && !neu.includes('null')) {
|
||||||
|
this.maxLocalMemory = neu;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enableHotPlug(neu) {
|
||||||
|
this.localEnableHotPlug = neu;
|
||||||
|
},
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
change() {
|
hotPlugChanged(neu) {
|
||||||
let memory = '';
|
// If hot plug is enabled, we need to update the maxCpu and maxMemory values
|
||||||
|
if (neu) {
|
||||||
if (String(this.localMemory).includes('Gi')) {
|
this.maxLocalCpu = this.localCpu ? this.localCpu * this.maxHotplugRatio : null;
|
||||||
memory = this.localMemory;
|
this.maxLocalMemory = this.localMemory ? `${ parseInt(this.localMemory, 10) * this.maxHotplugRatio }${ GIBIBYTE }` : null;
|
||||||
|
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, this.maxLocalCpu, this.maxLocalMemory, neu);
|
||||||
} else {
|
} else {
|
||||||
memory = `${ this.localMemory }${ GIBIBYTE }`;
|
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, '', null, neu);
|
||||||
}
|
}
|
||||||
if (memory.includes('null')) {
|
|
||||||
memory = null;
|
|
||||||
}
|
|
||||||
this.$emit('updateCpuMemory', this.localCpu, memory);
|
|
||||||
},
|
},
|
||||||
|
changeMemory() {
|
||||||
|
if (this.localEnableHotPlug) {
|
||||||
|
this.maxLocalMemory = this.localMemory ? `${ parseInt(this.localMemory, 10) * this.maxHotplugRatio }${ GIBIBYTE }` : null;
|
||||||
|
}
|
||||||
|
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, this.maxLocalCpu, this.maxLocalMemory, this.localEnableHotPlug);
|
||||||
|
},
|
||||||
|
changeCPU() {
|
||||||
|
if (this.localEnableHotPlug) {
|
||||||
|
this.maxLocalCpu = this.localCpu ? this.localCpu * this.maxHotplugRatio : null;
|
||||||
|
}
|
||||||
|
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, this.maxLocalCpu, this.maxLocalMemory, this.localEnableHotPlug);
|
||||||
|
},
|
||||||
|
changeMaxCPUMemory() {
|
||||||
|
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, this.maxLocalCpu, this.maxLocalMemory, this.localEnableHotPlug);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="row">
|
<div>
|
||||||
<div class="col span-6 mb-10">
|
<div class="row">
|
||||||
<InputOrDisplay
|
<div class="col span-6 mb-10">
|
||||||
name="CPU"
|
<InputOrDisplay
|
||||||
:value="cupDisplay"
|
name="CPU"
|
||||||
:mode="mode"
|
:value="cpuDisplay"
|
||||||
>
|
|
||||||
<UnitInput
|
|
||||||
v-model:value="localCpu"
|
|
||||||
label="CPU"
|
|
||||||
suffix="C"
|
|
||||||
:delay="0"
|
|
||||||
required
|
|
||||||
:disabled="disabled"
|
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
class="mb-20"
|
class="mb-10"
|
||||||
@update:value="change"
|
>
|
||||||
/>
|
<UnitInput
|
||||||
</InputOrDisplay>
|
v-model:value="localCpu"
|
||||||
|
label="CPU"
|
||||||
|
suffix="C"
|
||||||
|
:delay="0"
|
||||||
|
required
|
||||||
|
:disabled="disabled"
|
||||||
|
:mode="mode"
|
||||||
|
class="mb-20"
|
||||||
|
@update:value="changeCPU"
|
||||||
|
/>
|
||||||
|
</InputOrDisplay>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6 mb-10">
|
||||||
|
<InputOrDisplay
|
||||||
|
:name="t('harvester.virtualMachine.input.memory')"
|
||||||
|
:value="memoryDisplay"
|
||||||
|
:mode="mode"
|
||||||
|
>
|
||||||
|
<UnitInput
|
||||||
|
v-model:value="localMemory"
|
||||||
|
:label="t('harvester.virtualMachine.input.memory')"
|
||||||
|
:mode="mode"
|
||||||
|
:input-exponent="3"
|
||||||
|
:delay="0"
|
||||||
|
:increment="1024"
|
||||||
|
:output-modifier="true"
|
||||||
|
:disabled="disabled"
|
||||||
|
required
|
||||||
|
:suffix="GIBIBYTE"
|
||||||
|
class="mb-20"
|
||||||
|
@update:value="changeMemory"
|
||||||
|
/>
|
||||||
|
</InputOrDisplay>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
<div class="col span-6 mb-10">
|
v-if="isCPUMemoryHotPlugFeatureEnabled"
|
||||||
<InputOrDisplay
|
class="row"
|
||||||
:name="t('harvester.virtualMachine.input.memory')"
|
>
|
||||||
:value="memoryDisplay"
|
<Checkbox
|
||||||
:mode="mode"
|
v-model:value="localEnableHotPlug"
|
||||||
>
|
class="check"
|
||||||
<UnitInput
|
type="checkbox"
|
||||||
v-model:value="localMemory"
|
:label="t('harvester.virtualMachine.hotplug.title')"
|
||||||
:label="t('harvester.virtualMachine.input.memory')"
|
:disabled="isView"
|
||||||
|
@update:value="hotPlugChanged"
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
v-clean-tooltip="{content: cpuMemoryHotplugTooltip, triggers: ['hover', 'touch', 'focus'] }"
|
||||||
|
v-stripped-aria-label="cpuMemoryHotplugTooltip"
|
||||||
|
class="icon icon-info"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="localEnableHotPlug && isCPUMemoryHotPlugFeatureEnabled"
|
||||||
|
class="row"
|
||||||
|
>
|
||||||
|
<div class="col span-6 mb-10">
|
||||||
|
<InputOrDisplay
|
||||||
|
:name="t('harvester.virtualMachine.input.maxCpu')"
|
||||||
|
:value="maxCpuDisplay"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:input-exponent="3"
|
class="mt-20"
|
||||||
:delay="0"
|
>
|
||||||
:increment="1024"
|
<UnitInput
|
||||||
:output-modifier="true"
|
v-model:value="maxLocalCpu"
|
||||||
:disabled="disabled"
|
:label="t('harvester.virtualMachine.input.maxCpu')"
|
||||||
required
|
suffix="C"
|
||||||
:suffix="GIBIBYTE"
|
:delay="0"
|
||||||
class="mb-20"
|
:disabled="disabled"
|
||||||
@update:value="change"
|
:mode="mode"
|
||||||
/>
|
class="mt-20"
|
||||||
</InputOrDisplay>
|
@update:value="changeMaxCPUMemory"
|
||||||
|
/>
|
||||||
|
</InputOrDisplay>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6 mb-10">
|
||||||
|
<InputOrDisplay
|
||||||
|
:name="t('harvester.virtualMachine.input.maxMemory')"
|
||||||
|
:value="maxMemoryDisplay"
|
||||||
|
:mode="mode"
|
||||||
|
class="mt-20"
|
||||||
|
>
|
||||||
|
<UnitInput
|
||||||
|
v-model:value="maxLocalMemory"
|
||||||
|
:label="t('harvester.virtualMachine.input.maxMemory')"
|
||||||
|
:mode="mode"
|
||||||
|
:input-exponent="3"
|
||||||
|
:delay="0"
|
||||||
|
:increment="1024"
|
||||||
|
:output-modifier="true"
|
||||||
|
:disabled="disabled"
|
||||||
|
:suffix="GIBIBYTE"
|
||||||
|
class="mt-20"
|
||||||
|
@update:value="changeMaxCPUMemory"
|
||||||
|
/>
|
||||||
|
</InputOrDisplay>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -101,9 +101,7 @@ export default {
|
|||||||
...formatted,
|
...formatted,
|
||||||
];
|
];
|
||||||
|
|
||||||
if (devices.length > 0) {
|
set(this.value.domain.devices, 'hostDevices', devices);
|
||||||
set(this.value.domain.devices, 'hostDevices', devices);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { LabeledInput } from '@components/Form/LabeledInput';
|
|||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
import ModalWithCard from '@shell/components/ModalWithCard';
|
import ModalWithCard from '@shell/components/ModalWithCard';
|
||||||
|
|
||||||
import { _VIEW, _EDIT } from '@shell/config/query-params';
|
import { _VIEW } from '@shell/config/query-params';
|
||||||
|
|
||||||
import { NAMESPACE } from '@shell/config/types';
|
import { NAMESPACE } from '@shell/config/types';
|
||||||
import { HCI } from '../../types';
|
import { HCI } from '../../types';
|
||||||
@@ -90,9 +90,6 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
sshOption() {
|
sshOption() {
|
||||||
if (this.mode === _VIEW || this.mode === _EDIT) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const out = this.$store.getters['harvester/all'](HCI.SSH).map( (O) => {
|
const out = this.$store.getters['harvester/all'](HCI.SSH).map( (O) => {
|
||||||
return {
|
return {
|
||||||
label: O.id,
|
label: O.id,
|
||||||
|
|||||||
@@ -84,9 +84,7 @@ export default {
|
|||||||
...formatted,
|
...formatted,
|
||||||
];
|
];
|
||||||
|
|
||||||
if (devices.length > 0) {
|
set(this.value.domain.devices, 'hostDevices', devices);
|
||||||
set(this.value.domain.devices, 'hostDevices', devices);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { SOURCE_TYPE } from '../../../config/harvester-map';
|
|||||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../../config/harvester';
|
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../../config/harvester';
|
||||||
import { HCI } from '../../../types';
|
import { HCI } from '../../../types';
|
||||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
||||||
|
import { OFF } from '../../../models/kubevirt.io.virtualmachine';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
emits: ['update:value'],
|
emits: ['update:value'],
|
||||||
@@ -266,7 +267,17 @@ export default {
|
|||||||
|
|
||||||
isLonghornV2(volume) {
|
isLonghornV2(volume) {
|
||||||
return volume?.pvc?.isLonghornV2 || volume?.pvc?.storageClass?.isLonghornV2;
|
return volume?.pvc?.isLonghornV2 || volume?.pvc?.storageClass?.isLonghornV2;
|
||||||
}
|
},
|
||||||
|
|
||||||
|
isResizeDisabled(volume) {
|
||||||
|
if (this.isCreate) return false;
|
||||||
|
if (volume.newCreateId) return false;
|
||||||
|
|
||||||
|
const isStopped = this.vm.stateDisplay === OFF;
|
||||||
|
const isLonghornV2 = this.isLonghornV2(volume);
|
||||||
|
|
||||||
|
return !isStopped || isLonghornV2;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -347,6 +358,7 @@ export default {
|
|||||||
:is-edit="isEdit"
|
:is-edit="isEdit"
|
||||||
:is-view="isView"
|
:is-view="isView"
|
||||||
:is-virtual-type="isVirtualType"
|
:is-virtual-type="isVirtualType"
|
||||||
|
:is-resize-disabled="isResizeDisabled(rows[i])"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:idx="i"
|
:idx="i"
|
||||||
:validate-required="validateRequired"
|
:validate-required="validateRequired"
|
||||||
|
|||||||
+3
-1
@@ -111,7 +111,7 @@ export default {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pvc.metadata?.annotations?.[HCI_ANNOTATIONS.GOLDEN_IMAGE] === 'true') {
|
if (pvc.isGoldenImageVolume) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,10 +142,12 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update this.value with existing volume spec, then update to upstream component
|
||||||
this.value.accessModes = pvcResource.spec.accessModes[0];
|
this.value.accessModes = pvcResource.spec.accessModes[0];
|
||||||
this.value.size = pvcResource.spec.resources.requests.storage;
|
this.value.size = pvcResource.spec.resources.requests.storage;
|
||||||
this.value.storageClassName = pvcResource.spec.storageClassName;
|
this.value.storageClassName = pvcResource.spec.storageClassName;
|
||||||
this.value.volumeMode = pvcResource.spec.volumeMode;
|
this.value.volumeMode = pvcResource.spec.volumeMode;
|
||||||
|
this.update();
|
||||||
},
|
},
|
||||||
|
|
||||||
'value.type'(neu) {
|
'value.type'(neu) {
|
||||||
|
|||||||
+26
-12
@@ -8,6 +8,7 @@ import { Banner } from '@components/Banner';
|
|||||||
import { PVC } from '@shell/config/types';
|
import { PVC } from '@shell/config/types';
|
||||||
import { formatSi, parseSi } from '@shell/utils/units';
|
import { formatSi, parseSi } from '@shell/utils/units';
|
||||||
import { HCI } from '../../../../types';
|
import { HCI } from '../../../../types';
|
||||||
|
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map';
|
import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map';
|
||||||
import { _VIEW } from '@shell/config/query-params';
|
import { _VIEW } from '@shell/config/query-params';
|
||||||
import LabelValue from '@shell/components/LabelValue';
|
import LabelValue from '@shell/components/LabelValue';
|
||||||
@@ -63,13 +64,19 @@ export default {
|
|||||||
isVirtualType: {
|
isVirtualType: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
}
|
},
|
||||||
|
|
||||||
|
isResizeDisabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
GIBIBYTE,
|
GIBIBYTE,
|
||||||
VOLUME_TYPE,
|
VOLUME_TYPE,
|
||||||
|
HCI_ANNOTATIONS,
|
||||||
InterfaceOption,
|
InterfaceOption,
|
||||||
loading: false,
|
loading: false,
|
||||||
images: [],
|
images: [],
|
||||||
@@ -90,12 +97,23 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
imagesOption() {
|
imagesOption() {
|
||||||
return this.images.filter((c) => c.isReady).sort((a, b) => a.creationTimestamp > b.creationTimestamp ? -1 : 1).map( (I) => {
|
return this.images
|
||||||
return {
|
.filter((image) => {
|
||||||
label: this.imageOptionLabel(I),
|
if (!image.isReady) return false;
|
||||||
value: I.id
|
|
||||||
};
|
// exclude internal images created during upgrade
|
||||||
});
|
const isInternalCreatedImage =
|
||||||
|
image.namespace === 'harvester-system' &&
|
||||||
|
image.labels?.[HCI_ANNOTATIONS.UPGRADE];
|
||||||
|
|
||||||
|
return !isInternalCreatedImage;
|
||||||
|
})
|
||||||
|
.sort((a, b) => (a.creationTimestamp > b.creationTimestamp ? -1 : 1))
|
||||||
|
.map((image) => ({
|
||||||
|
label: this.imageOptionLabel(image),
|
||||||
|
value: image.id,
|
||||||
|
disabled: image.isImportedImage
|
||||||
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
imageName() {
|
imageName() {
|
||||||
@@ -122,10 +140,6 @@ export default {
|
|||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
|
return this.$store.getters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
|
||||||
},
|
},
|
||||||
|
|
||||||
isLonghornV2() {
|
|
||||||
return this.value.pvc?.isLonghornV2 || this.value.pvc?.storageClass?.isLonghornV2;
|
|
||||||
},
|
|
||||||
|
|
||||||
selectedImage() {
|
selectedImage() {
|
||||||
return this.$store.getters['harvester/all'](HCI.IMAGE)?.find( (I) => this.value.image === I.id);
|
return this.$store.getters['harvester/all'](HCI.IMAGE)?.find( (I) => this.value.image === I.id);
|
||||||
},
|
},
|
||||||
@@ -337,7 +351,7 @@ export default {
|
|||||||
:label="t('harvester.fields.size')"
|
:label="t('harvester.fields.size')"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:required="validateRequired"
|
:required="validateRequired"
|
||||||
:disable="isLonghornV2"
|
:disabled="isResizeDisabled"
|
||||||
:suffix="GIBIBYTE"
|
:suffix="GIBIBYTE"
|
||||||
@update:value="update"
|
@update:value="update"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { ucFirst } from '@shell/utils/string';
|
|||||||
import { LVM_DRIVER } from '../../../../models/harvester/storage.k8s.io.storageclass';
|
import { LVM_DRIVER } from '../../../../models/harvester/storage.k8s.io.storageclass';
|
||||||
import { DATA_ENGINE_V2 } from '../../../../models/harvester/persistentvolumeclaim';
|
import { DATA_ENGINE_V2 } from '../../../../models/harvester/persistentvolumeclaim';
|
||||||
import { GIBIBYTE } from '../../../../utils/unit';
|
import { GIBIBYTE } from '../../../../utils/unit';
|
||||||
|
import { isInternalStorageClass } from '../../../../utils/storage-class';
|
||||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -55,7 +56,12 @@ export default {
|
|||||||
isVirtualType: {
|
isVirtualType: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
}
|
},
|
||||||
|
|
||||||
|
isResizeDisabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
@@ -118,19 +124,22 @@ export default {
|
|||||||
|
|
||||||
storageClassOptions() {
|
storageClassOptions() {
|
||||||
return this.storageClasses.filter((s) => !s.parameters?.backingImage).map((s) => {
|
return this.storageClasses.filter((s) => !s.parameters?.backingImage).map((s) => {
|
||||||
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
let label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||||
|
let disabled = false;
|
||||||
|
|
||||||
|
if (isInternalStorageClass(s.name)) {
|
||||||
|
label += ` (${ this.t('harvester.storage.internal.label') })`;
|
||||||
|
disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label,
|
label,
|
||||||
value: s.name,
|
value: s.name,
|
||||||
|
disabled
|
||||||
};
|
};
|
||||||
}) || [];
|
}) || [];
|
||||||
},
|
},
|
||||||
|
|
||||||
isLonghornV2() {
|
|
||||||
return this.value.pvc?.isLonghornV2 || this.value.pvc?.storageClass?.isLonghornV2;
|
|
||||||
},
|
|
||||||
|
|
||||||
isLonghornStorageClass() {
|
isLonghornStorageClass() {
|
||||||
const selectedSC = this.storageClasses.find((sc) => sc.name === this.value?.storageClassName) || {};
|
const selectedSC = this.storageClasses.find((sc) => sc.name === this.value?.storageClassName) || {};
|
||||||
|
|
||||||
@@ -302,7 +311,7 @@ export default {
|
|||||||
:mode="mode"
|
:mode="mode"
|
||||||
:required="validateRequired"
|
:required="validateRequired"
|
||||||
:label="t('harvester.fields.size')"
|
:label="t('harvester.fields.size')"
|
||||||
:disabled="isLonghornV2"
|
:disabled="isResizeDisabled"
|
||||||
:suffix="GIBIBYTE"
|
:suffix="GIBIBYTE"
|
||||||
@update:value="update"
|
@update:value="update"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import Tabbed from '@shell/components/Tabbed';
|
import Tabbed from '@shell/components/Tabbed';
|
||||||
import { clone } from '@shell/utils/object';
|
import { clone, set } from '@shell/utils/object';
|
||||||
import Tab from '@shell/components/Tabbed/Tab';
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
import { Checkbox } from '@components/Form/Checkbox';
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
import CruResource from '@shell/components/CruResource';
|
import CruResource from '@shell/components/CruResource';
|
||||||
@@ -16,7 +16,6 @@ import PodAffinity from '@shell/components/form/PodAffinity';
|
|||||||
import VGpuDevices from './VirtualMachineVGpuDevices/index';
|
import VGpuDevices from './VirtualMachineVGpuDevices/index';
|
||||||
import UsbDevices from './VirtualMachineUSBDevices/index';
|
import UsbDevices from './VirtualMachineUSBDevices/index';
|
||||||
import KeyValue from '@shell/components/form/KeyValue';
|
import KeyValue from '@shell/components/form/KeyValue';
|
||||||
|
|
||||||
import { clear } from '@shell/utils/array';
|
import { clear } from '@shell/utils/array';
|
||||||
import { saferDump } from '@shell/utils/create-yaml';
|
import { saferDump } from '@shell/utils/create-yaml';
|
||||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
@@ -115,13 +114,11 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
machineTypeOptions() {
|
machineTypeOptions() {
|
||||||
return [{
|
return this.machineTypes.map((type) => {
|
||||||
label: 'None',
|
if (!type) return { label: 'None', value: '' };
|
||||||
value: ''
|
|
||||||
}, {
|
return { label: type, value: type };
|
||||||
label: 'q35',
|
});
|
||||||
value: 'q35'
|
|
||||||
}];
|
|
||||||
},
|
},
|
||||||
|
|
||||||
templateOptions() {
|
templateOptions() {
|
||||||
@@ -240,6 +237,9 @@ export default {
|
|||||||
}
|
}
|
||||||
const versions = await this.$store.dispatch('harvester/findAll', { type: HCI.VM_VERSION });
|
const versions = await this.$store.dispatch('harvester/findAll', { type: HCI.VM_VERSION });
|
||||||
const curVersion = versions.find( (V) => V.id === id);
|
const curVersion = versions.find( (V) => V.id === id);
|
||||||
|
|
||||||
|
if (!curVersion?.spec?.vm) return;
|
||||||
|
|
||||||
const cloneVersionVM = clone(curVersion.spec.vm);
|
const cloneVersionVM = clone(curVersion.spec.vm);
|
||||||
|
|
||||||
delete cloneVersionVM.spec?.template?.spec?.accessCredentials;
|
delete cloneVersionVM.spec?.template?.spec?.accessCredentials;
|
||||||
@@ -247,7 +247,6 @@ export default {
|
|||||||
delete cloneVersionVM.spec?.template?.metadata?.annotations?.[HCI_ANNOTATIONS.DYNAMIC_SSHKEYS_USERS];
|
delete cloneVersionVM.spec?.template?.metadata?.annotations?.[HCI_ANNOTATIONS.DYNAMIC_SSHKEYS_USERS];
|
||||||
|
|
||||||
const claimTemplate = parseVolumeClaimTemplates(cloneVersionVM);
|
const claimTemplate = parseVolumeClaimTemplates(cloneVersionVM);
|
||||||
|
|
||||||
const deleteDataSource = claimTemplate.map((volume) => {
|
const deleteDataSource = claimTemplate.map((volume) => {
|
||||||
if (volume?.spec?.dataSource) {
|
if (volume?.spec?.dataSource) {
|
||||||
delete volume.spec.dataSource;
|
delete volume.spec.dataSource;
|
||||||
@@ -258,6 +257,17 @@ export default {
|
|||||||
|
|
||||||
cloneVersionVM.metadata.annotations[HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE] = JSON.stringify(deleteDataSource);
|
cloneVersionVM.metadata.annotations[HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE] = JSON.stringify(deleteDataSource);
|
||||||
|
|
||||||
|
// Update instance labels, labels and annotations
|
||||||
|
this.value.spec.template.metadata.labels = cloneVersionVM.spec.template.metadata.labels || {};
|
||||||
|
this.value.metadata.labels = {
|
||||||
|
...(curVersion.metadata?.labels || {}),
|
||||||
|
...(cloneVersionVM.metadata?.labels || {})
|
||||||
|
};
|
||||||
|
this.value.metadata.annotations = {
|
||||||
|
...(curVersion.metadata?.annotations || {}),
|
||||||
|
...(cloneVersionVM.metadata?.annotations || {})
|
||||||
|
};
|
||||||
|
|
||||||
this.getInitConfig({
|
this.getInitConfig({
|
||||||
value: cloneVersionVM, existUserData: true, fromTemplate: true
|
value: cloneVersionVM, existUserData: true, fromTemplate: true
|
||||||
});
|
});
|
||||||
@@ -328,6 +338,15 @@ export default {
|
|||||||
saveVM(buttonCb) {
|
saveVM(buttonCb) {
|
||||||
clear(this.errors);
|
clear(this.errors);
|
||||||
|
|
||||||
|
this.validateCPUMemory();
|
||||||
|
|
||||||
|
// block create VM flow if has validation errors
|
||||||
|
if (this.errors.length) {
|
||||||
|
buttonCb(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isSingle) {
|
if (this.isSingle) {
|
||||||
this.saveSingle(buttonCb);
|
this.saveSingle(buttonCb);
|
||||||
} else {
|
} else {
|
||||||
@@ -335,6 +354,18 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
validateCPUMemory() {
|
||||||
|
const { cpu, memory } = this;
|
||||||
|
|
||||||
|
if ((!cpu)) {
|
||||||
|
this.errors.push(this.t('validation.required', { key: this.t('harvester.virtualMachine.input.cpu') }, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((!memory)) {
|
||||||
|
this.errors.push(this.t('validation.required', { key: this.t('harvester.virtualMachine.input.memory') }, true));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async saveSingle(buttonCb) {
|
async saveSingle(buttonCb) {
|
||||||
this.parseVM();
|
this.parseVM();
|
||||||
this.value.spec.template.spec.hostname = this.hostname ? this.hostname : this.value.metadata.name;
|
this.value.spec.template.spec.hostname = this.hostname ? this.hostname : this.value.metadata.name;
|
||||||
@@ -412,12 +443,21 @@ export default {
|
|||||||
}
|
}
|
||||||
const cloneDeepNewVM = clone(this.value);
|
const cloneDeepNewVM = clone(this.value);
|
||||||
|
|
||||||
|
// new VM
|
||||||
delete cloneDeepNewVM?.metadata;
|
delete cloneDeepNewVM?.metadata;
|
||||||
delete cloneDeepNewVM?.__clone;
|
delete cloneDeepNewVM?.__clone;
|
||||||
|
|
||||||
|
// old VM
|
||||||
delete this.cloneVM?.metadata;
|
delete this.cloneVM?.metadata;
|
||||||
delete this.cloneVM?.__clone;
|
delete this.cloneVM?.__clone;
|
||||||
|
|
||||||
|
// add empty hostDevices to old VM as CRD does not have it.
|
||||||
|
const devicesObj = this.cloneVM?.spec?.template?.spec?.domain?.devices;
|
||||||
|
|
||||||
|
if (devicesObj && devicesObj.hostDevices === undefined) {
|
||||||
|
set(devicesObj, 'hostDevices', []);
|
||||||
|
}
|
||||||
|
|
||||||
const oldVM = JSON.parse(JSON.stringify(this.cloneVM));
|
const oldVM = JSON.parse(JSON.stringify(this.cloneVM));
|
||||||
const newVM = JSON.parse(JSON.stringify(cloneDeepNewVM));
|
const newVM = JSON.parse(JSON.stringify(cloneDeepNewVM));
|
||||||
|
|
||||||
@@ -485,6 +525,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
generateYaml() {
|
generateYaml() {
|
||||||
|
this.showYaml = this?.$refs?.vmCruResource?.showYaml || false;
|
||||||
this.parseVM();
|
this.parseVM();
|
||||||
const out = saferDump(this.value);
|
const out = saferDump(this.value);
|
||||||
|
|
||||||
@@ -498,6 +539,7 @@ export default {
|
|||||||
<CruResource
|
<CruResource
|
||||||
v-if="spec"
|
v-if="spec"
|
||||||
id="vm"
|
id="vm"
|
||||||
|
ref="vmCruResource"
|
||||||
:done-route="doneRoute"
|
:done-route="doneRoute"
|
||||||
:resource="value"
|
:resource="value"
|
||||||
:cancel-event="true"
|
:cancel-event="true"
|
||||||
@@ -509,6 +551,7 @@ export default {
|
|||||||
:apply-hooks="applyHooks"
|
:apply-hooks="applyHooks"
|
||||||
@finish="saveVM"
|
@finish="saveVM"
|
||||||
@cancel="cancelAction"
|
@cancel="cancelAction"
|
||||||
|
@error="e=>errors=e"
|
||||||
>
|
>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
v-if="isCreate"
|
v-if="isCreate"
|
||||||
@@ -581,9 +624,13 @@ export default {
|
|||||||
:label="t('harvester.virtualMachine.detail.tabs.basics')"
|
:label="t('harvester.virtualMachine.detail.tabs.basics')"
|
||||||
>
|
>
|
||||||
<CpuMemory
|
<CpuMemory
|
||||||
|
class="mb-20"
|
||||||
:cpu="cpu"
|
:cpu="cpu"
|
||||||
|
:max-cpu="maxCpu"
|
||||||
:memory="memory"
|
:memory="memory"
|
||||||
|
:max-memory="maxMemory"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
|
:enable-hot-plug="cpuMemoryHotplugEnabled"
|
||||||
@updateCpuMemory="updateCpuMemory"
|
@updateCpuMemory="updateCpuMemory"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -751,10 +798,31 @@ export default {
|
|||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
|
<Tab
|
||||||
|
name="annotations"
|
||||||
|
:label="t('harvester.tab.annotations')"
|
||||||
|
:weight="-11"
|
||||||
|
>
|
||||||
|
<Banner color="info">
|
||||||
|
<t k="harvester.virtualMachine.annotations.banner" />
|
||||||
|
</Banner>
|
||||||
|
<KeyValue
|
||||||
|
key="annotations"
|
||||||
|
:value="value.annotations"
|
||||||
|
:protected-keys="value.systemAnnotations || []"
|
||||||
|
:toggle-filter="toggler"
|
||||||
|
:add-label="t('labels.addAnnotation')"
|
||||||
|
:mode="mode"
|
||||||
|
:read-allowed="false"
|
||||||
|
:value-can-be-empty="true"
|
||||||
|
@update:value="value.setAnnotations($event)"
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
|
||||||
<Tab
|
<Tab
|
||||||
name="advanced"
|
name="advanced"
|
||||||
:label="t('harvester.tab.advanced')"
|
:label="t('harvester.tab.advanced')"
|
||||||
:weight="-11"
|
:weight="-12"
|
||||||
>
|
>
|
||||||
<div class="row mb-20">
|
<div class="row mb-20">
|
||||||
<div class="col span-6">
|
<div class="col span-6">
|
||||||
@@ -852,6 +920,7 @@ export default {
|
|||||||
ref="yamlEditor"
|
ref="yamlEditor"
|
||||||
:user-script="userScript"
|
:user-script="userScript"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
|
:os-type="osType"
|
||||||
:view-code="isWindows"
|
:view-code="isWindows"
|
||||||
:namespace="value.metadata.namespace"
|
:namespace="value.metadata.namespace"
|
||||||
:network-script="networkScript"
|
:network-script="networkScript"
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export default {
|
|||||||
const rows = (this.value || []).map((row) => {
|
const rows = (this.value || []).map((row) => {
|
||||||
let type = 'cidr';
|
let type = 'cidr';
|
||||||
|
|
||||||
if (row.rangeStart && row.rangeEnd) {
|
if (row.rangeStart || row.rangeEnd) {
|
||||||
type = 'range';
|
type = 'range';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ export default {
|
|||||||
:apply-hooks="applyHooks"
|
:apply-hooks="applyHooks"
|
||||||
:yaml-modifiers="yamlModifiers"
|
:yaml-modifiers="yamlModifiers"
|
||||||
@finish="save"
|
@finish="save"
|
||||||
|
@error="e=>errors=e"
|
||||||
>
|
>
|
||||||
<NameNsDescription
|
<NameNsDescription
|
||||||
:value="value"
|
:value="value"
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export default {
|
|||||||
:labels="[t('generic.disabled'),t('generic.enabled')]"
|
:labels="[t('generic.disabled'),t('generic.enabled')]"
|
||||||
:options="[false, true]"
|
:options="[false, true]"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
@input="onToggle"
|
@update:value="onToggle"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -235,6 +235,7 @@ export default {
|
|||||||
:apply-hooks="applyHooks"
|
:apply-hooks="applyHooks"
|
||||||
:yaml-modifiers="yamlModifiers"
|
:yaml-modifiers="yamlModifiers"
|
||||||
@finish="save"
|
@finish="save"
|
||||||
|
@error="e=>errors=e"
|
||||||
>
|
>
|
||||||
<NameNsDescription
|
<NameNsDescription
|
||||||
:value="value"
|
:value="value"
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ export default {
|
|||||||
:subtypes="[]"
|
:subtypes="[]"
|
||||||
:can-yaml="false"
|
:can-yaml="false"
|
||||||
:validation-passed="fvFormIsValid"
|
:validation-passed="fvFormIsValid"
|
||||||
@error="e=>errors = e"
|
@error="e=>errors=e"
|
||||||
@finish="save"
|
@finish="save"
|
||||||
@cancel="done"
|
@cancel="done"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ export default {
|
|||||||
:mode="mode"
|
:mode="mode"
|
||||||
:errors="errors"
|
:errors="errors"
|
||||||
@finish="save"
|
@finish="save"
|
||||||
|
@error="e=>errors=e"
|
||||||
>
|
>
|
||||||
<NameNsDescription
|
<NameNsDescription
|
||||||
:value="value"
|
:value="value"
|
||||||
|
|||||||
@@ -373,6 +373,7 @@ export default {
|
|||||||
:mode="mode"
|
:mode="mode"
|
||||||
:errors="errors"
|
:errors="errors"
|
||||||
@finish="save"
|
@finish="save"
|
||||||
|
@error="e=>errors=e"
|
||||||
>
|
>
|
||||||
<NameNsDescription
|
<NameNsDescription
|
||||||
:value="value"
|
:value="value"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import { NORMAN } from '@shell/config/types';
|
||||||
import { HCI } from '../types';
|
import { HCI } from '../types';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -13,10 +14,24 @@ export default {
|
|||||||
const harvesterSettings = await this.$store.dispatch('harvester/findAll', { type: HCI.SETTING });
|
const harvesterSettings = await this.$store.dispatch('harvester/findAll', { type: HCI.SETTING });
|
||||||
|
|
||||||
this.harvesterSettings = harvesterSettings;
|
this.harvesterSettings = harvesterSettings;
|
||||||
|
|
||||||
|
const clusterRoleTemplateBindingSchema = this.$store.getters['rancher/schemaFor'](NORMAN.CLUSTER_ROLE_TEMPLATE_BINDING);
|
||||||
|
|
||||||
|
if (clusterRoleTemplateBindingSchema) {
|
||||||
|
const normanBindings = await this.$store.dispatch('rancher/findAll', { type: NORMAN.CLUSTER_ROLE_TEMPLATE_BINDING }, { root: true });
|
||||||
|
|
||||||
|
this.clusterRoleTemplateBinding = normanBindings;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return { harvesterSettings: [] };
|
const user = this.$store.getters['auth/v3User'];
|
||||||
|
|
||||||
|
return {
|
||||||
|
harvesterSettings: [],
|
||||||
|
clusterRoleTemplateBinding: [],
|
||||||
|
user,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
@@ -31,19 +46,28 @@ export default {
|
|||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
return isMatch;
|
return isMatch;
|
||||||
|
},
|
||||||
|
|
||||||
|
isClusterOwner() {
|
||||||
|
const template = this.clusterRoleTemplateBinding.find((template) => {
|
||||||
|
return template.userId === this.user?.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
return template?.roleTemplateId === 'cluster-owner';
|
||||||
|
},
|
||||||
|
|
||||||
|
shouldShowError() {
|
||||||
|
return this.isClusterOwner && !this.isMatch && this.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="isMatch">
|
<div>
|
||||||
{{ value }}
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
{{ value }}
|
{{ value }}
|
||||||
<p
|
<p
|
||||||
v-if="value"
|
v-if="shouldShowError"
|
||||||
class="text-error"
|
class="text-error"
|
||||||
>
|
>
|
||||||
{{ t('harvester.backup.matchTarget') }}
|
{{ t('harvester.backup.matchTarget') }}
|
||||||
|
|||||||
@@ -35,11 +35,26 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
// Return VM instance IP and VM annotation IP
|
|
||||||
ips() {
|
ips() {
|
||||||
return [...this.vmiIp, ...this.networkAnnotationIP]
|
if (this.vmiIp.length) {
|
||||||
.filter(Boolean)
|
return [...this.vmiIp, ...this.networkAnnotationIP]
|
||||||
.sort((a, b) => a.ip < b.ip ? -1 : 1);
|
.filter(Boolean)
|
||||||
|
.sort((a, b) => a.ip < b.ip ? -1 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.customAnnotationIP;
|
||||||
|
},
|
||||||
|
|
||||||
|
customAnnotationIP() {
|
||||||
|
const annotationIp = get(this.row, `metadata.annotations."${ HCI_ANNOTATIONS.CUSTOM_IP }"`);
|
||||||
|
|
||||||
|
if (annotationIp && isIpv4(annotationIp)) {
|
||||||
|
return [{
|
||||||
|
name: 'custom-ip', ip: annotationIp, isCustom: true
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
},
|
},
|
||||||
|
|
||||||
networkAnnotationIP() {
|
networkAnnotationIP() {
|
||||||
@@ -97,12 +112,13 @@ export default {
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="showIP">
|
<div v-if="showIP">
|
||||||
<span
|
<span
|
||||||
v-for="{ip, name} in ips"
|
v-for="{ ip, name, isCustom } in ips"
|
||||||
:key="ip"
|
:key="`${ip}-${name}`"
|
||||||
>
|
>
|
||||||
<CopyToClipboardText
|
<CopyToClipboardText
|
||||||
v-clean-tooltip="name"
|
v-clean-tooltip="isCustom ? t('harvester.formatters.harvesterIpAddress.customIpTooltip') : name"
|
||||||
:text="ip"
|
:text="ip"
|
||||||
|
:plain="isCustom"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+184
-11
@@ -49,9 +49,18 @@ dialog:
|
|||||||
}
|
}
|
||||||
protip: "Tip: Hold the {alternateLabel} key while clicking action to bypass this confirmation"
|
protip: "Tip: Hold the {alternateLabel} key while clicking action to bypass this confirmation"
|
||||||
stop:
|
stop:
|
||||||
message: "Are you sure you want to continue stop the {type} {names}?"
|
message: "Are you sure you want to stop the {type} {names}?"
|
||||||
pause:
|
pause:
|
||||||
message: "Are you sure you want to continue pause the {type} {names}?"
|
message: "Are you sure you want to pause the {type} {names}?"
|
||||||
|
restart:
|
||||||
|
message: "Are you sure you want to restart the {type} {names}?"
|
||||||
|
softreboot:
|
||||||
|
message: "Are you sure you want to soft reboot the {type} {names}?"
|
||||||
|
promptRemove:
|
||||||
|
title: "Delete {type}"
|
||||||
|
warningMessage: "Deleting the selected {type} permanently removes all resources on {thisOrThese} {type}. This action is irreversible. Do you want to continue?"
|
||||||
|
confirmName: "Type <b>{nameToMatch}</b> to delete the {type}:"
|
||||||
|
protip: "Tip: Hold the {alternateLabel} key while clicking Delete to bypass the confirmation step."
|
||||||
|
|
||||||
harvester:
|
harvester:
|
||||||
branding:
|
branding:
|
||||||
@@ -99,6 +108,9 @@ harvester:
|
|||||||
tip: Please enter a virtual machine name!
|
tip: Please enter a virtual machine name!
|
||||||
success: 'Virtual machine { name } cloned successfully.'
|
success: 'Virtual machine { name } cloned successfully.'
|
||||||
failed: 'Failed clone virtual machine!'
|
failed: 'Failed clone virtual machine!'
|
||||||
|
osImage:
|
||||||
|
title: Delete Image
|
||||||
|
message: Are you sure you want to delete the image { name } ?
|
||||||
downloadImage:
|
downloadImage:
|
||||||
title: Download Image
|
title: Download Image
|
||||||
banner: 'This action takes a while depending on the image size ({ size }). Please be patient.'
|
banner: 'This action takes a while depending on the image size ({ size }). Please be patient.'
|
||||||
@@ -124,13 +136,31 @@ harvester:
|
|||||||
delete: Delete
|
delete: Delete
|
||||||
bundle:
|
bundle:
|
||||||
title: Generate a Support Bundle
|
title: Generate a Support Bundle
|
||||||
|
titleDescription: Collect system-related logs in Harvester to assist with troubleshooting and support.
|
||||||
|
tip: Each field below specifies an aspect of the support bundle. For detailed explanations, please refer to the <a href="{doc}" target="_blank">documentation</a>.
|
||||||
url: Issue URL
|
url: Issue URL
|
||||||
description: Description
|
description: Description
|
||||||
requiredDesc: Description is required!
|
namespaces:
|
||||||
titleDescription: Collect system-related logs in Harvester to help with troubleshooting and support.
|
label: Namespaces
|
||||||
|
tooltip: Select additional namespaces to include in the support bundle.<br/>See docs support-bundle-namespaces for detail.
|
||||||
|
selectAll: 'Select All'
|
||||||
|
unselectAll: 'Unselect All'
|
||||||
|
timeout:
|
||||||
|
label: Timeout
|
||||||
|
tooltip: Minutes allows for completion of the support bundle generation process.<br/>See docs support-bundle-timeout for detail.
|
||||||
|
expiration:
|
||||||
|
label: Expiration
|
||||||
|
tooltip: Minutes before deleting packaged but not downloaded support bundle.<br/>See docs support-bundle-expiration for detail.
|
||||||
|
nodeTimeout:
|
||||||
|
label: Node Collection Timeout
|
||||||
|
tooltip: Minutes allowed for collecting logs/configurations on nodes.<br/>See docs support-bundle-node-collection-timeout for detail.
|
||||||
hotplug:
|
hotplug:
|
||||||
success: 'Volume { diskName } is mounted to the virtual machine { vm }.'
|
success: 'Volume { diskName } is mounted to the virtual machine { vm }.'
|
||||||
title: Add Volume
|
title: Add Volume
|
||||||
|
cpuMemoryHotplug:
|
||||||
|
success: 'CPU and Memory are updated to the virtual machine { vm }.'
|
||||||
|
title: Edit CPU and Memory
|
||||||
|
maxResourcesMessage: 'You can increase the CPU to maximum { maxCpu }C and memory to maximum { maxMemory }.'
|
||||||
hotunplug:
|
hotunplug:
|
||||||
success: 'Volume { name } is detached successfully.'
|
success: 'Volume { name } is detached successfully.'
|
||||||
snapshot:
|
snapshot:
|
||||||
@@ -187,6 +217,7 @@ harvester:
|
|||||||
suspendSchedule: Suspend
|
suspendSchedule: Suspend
|
||||||
restoreExistingVM: Replace Existing
|
restoreExistingVM: Replace Existing
|
||||||
migrate: Migrate
|
migrate: Migrate
|
||||||
|
cpuAndMemoryHotplug: Edit CPU and Memory
|
||||||
abortMigration: Abort Migration
|
abortMigration: Abort Migration
|
||||||
createTemplate: Generate Template
|
createTemplate: Generate Template
|
||||||
enableMaintenance: Enable Maintenance Mode
|
enableMaintenance: Enable Maintenance Mode
|
||||||
@@ -220,6 +251,8 @@ harvester:
|
|||||||
{count, plural,
|
{count, plural,
|
||||||
=1 {core}
|
=1 {core}
|
||||||
other {cores}}
|
other {cores}}
|
||||||
|
harvesterIpAddress:
|
||||||
|
customIpTooltip: "Custom IP (set via annotation)"
|
||||||
tableHeaders:
|
tableHeaders:
|
||||||
imageEncryption: Encryption
|
imageEncryption: Encryption
|
||||||
size: Size
|
size: Size
|
||||||
@@ -267,6 +300,7 @@ harvester:
|
|||||||
quotas: Quotas
|
quotas: Quotas
|
||||||
snapshots: Snapshots
|
snapshots: Snapshots
|
||||||
instanceLabel: Instance Labels
|
instanceLabel: Instance Labels
|
||||||
|
annotations: Annotations
|
||||||
fields:
|
fields:
|
||||||
version: Version
|
version: Version
|
||||||
name: Name
|
name: Name
|
||||||
@@ -367,6 +401,8 @@ harvester:
|
|||||||
ruleFileTip: 'The file you have chosen ends in an extension that we do not support. We only accept image files that end in .img, .iso, .qcow, .qcow2, .raw.'
|
ruleFileTip: 'The file you have chosen ends in an extension that we do not support. We only accept image files that end in .img, .iso, .qcow, .qcow2, .raw.'
|
||||||
hash:
|
hash:
|
||||||
sha512: 'Invalid SHA512 checksum.'
|
sha512: 'Invalid SHA512 checksum.'
|
||||||
|
subnet:
|
||||||
|
excludeIps: 'Exclude IPs cannot be empty. Please remove or fill in the exclude IPs.'
|
||||||
|
|
||||||
dashboard:
|
dashboard:
|
||||||
label: Dashboard
|
label: Dashboard
|
||||||
@@ -540,6 +576,10 @@ harvester:
|
|||||||
virtualMachine:
|
virtualMachine:
|
||||||
label: Virtual Machines
|
label: Virtual Machines
|
||||||
osType: OS Type
|
osType: OS Type
|
||||||
|
hotplug:
|
||||||
|
title: Enable CPU and memory hotplug
|
||||||
|
tooltip: The default maximum CPU and maximum memory are {hotPlugTimes} times based on CPU and memory.
|
||||||
|
restartVMMessage: Restart action is required for the virtual machine configuration change to take effect
|
||||||
instance:
|
instance:
|
||||||
singleInstance:
|
singleInstance:
|
||||||
multipleInstance:
|
multipleInstance:
|
||||||
@@ -665,7 +705,7 @@ harvester:
|
|||||||
network:
|
network:
|
||||||
label: Network Data Template
|
label: Network Data Template
|
||||||
title: "Network Data:"
|
title: "Network Data:"
|
||||||
tip: "The network-data configuration allows you to customize the instance’s networking interfaces by assigning subnet configuration, virtual device creation (bonds, bridges, VLANs) routes and DNS configuration. <a href='https://cloudinit.readthedocs.io/en/latest/reference/network-config-format-v1.html' target='_blank'>Learn more</a>"
|
tip: "The network-data configuration allows you to customize the instance's networking interfaces by assigning subnet configuration, virtual device creation (bonds, bridges, VLANs) routes and DNS configuration. <a href='https://cloudinit.readthedocs.io/en/latest/reference/network-config-format-v1.html' target='_blank'>Learn more</a>"
|
||||||
scheduling:
|
scheduling:
|
||||||
affinity:
|
affinity:
|
||||||
anyNode: 'Run virtual machine on any available node'
|
anyNode: 'Run virtual machine on any available node'
|
||||||
@@ -685,6 +725,9 @@ harvester:
|
|||||||
invalidUser: Invalid Username.
|
invalidUser: Invalid Username.
|
||||||
input:
|
input:
|
||||||
name: Name
|
name: Name
|
||||||
|
cpu: CPU
|
||||||
|
maxCpu: Maximum CPU
|
||||||
|
maxMemory: Maximum Memory
|
||||||
memory: Memory
|
memory: Memory
|
||||||
image: Image
|
image: Image
|
||||||
sshKey: SSHKey
|
sshKey: SSHKey
|
||||||
@@ -745,6 +788,7 @@ harvester:
|
|||||||
dedicatedResources: Dedicated Resources
|
dedicatedResources: Dedicated Resources
|
||||||
down: Virtual machine not running
|
down: Virtual machine not running
|
||||||
affinityRules: Affinity Rules
|
affinityRules: Affinity Rules
|
||||||
|
phase: Phase
|
||||||
sourceNode: Source Node
|
sourceNode: Source Node
|
||||||
targetNode: Target Node
|
targetNode: Target Node
|
||||||
started: Started
|
started: Started
|
||||||
@@ -781,7 +825,9 @@ harvester:
|
|||||||
instanceLabels:
|
instanceLabels:
|
||||||
banner: These labels are automatically synchronized to the virtual machine instance.
|
banner: These labels are automatically synchronized to the virtual machine instance.
|
||||||
labels:
|
labels:
|
||||||
banner: These key values are added as labels to the virtual machine.
|
banner: These key values are added as labels to the virtual machine.
|
||||||
|
annotations:
|
||||||
|
banner: These key values are added as annotations to the virtual machine.
|
||||||
|
|
||||||
volume:
|
volume:
|
||||||
label: Volumes
|
label: Volumes
|
||||||
@@ -842,8 +888,12 @@ harvester:
|
|||||||
=1 {1 image is uploading, please do not refresh or close the page.}
|
=1 {1 image is uploading, please do not refresh or close the page.}
|
||||||
other {{count} images are uploading, please do not refresh or close the page.}
|
other {{count} images are uploading, please do not refresh or close the page.}
|
||||||
}
|
}
|
||||||
|
osUpgrade:
|
||||||
|
uploading: "{name} is uploading, please do not refresh or close the page."
|
||||||
checksum: Checksum
|
checksum: Checksum
|
||||||
checksumTip: Validate the image using the SHA512 checksum, if specified.
|
checksumTip: Validate the image using the SHA512 checksum, if specified.
|
||||||
|
tooltip:
|
||||||
|
imported: Created automatically by the vm-import-controller
|
||||||
|
|
||||||
vmTemplate:
|
vmTemplate:
|
||||||
label: Templates
|
label: Templates
|
||||||
@@ -870,14 +920,18 @@ harvester:
|
|||||||
upgradeNode: Upgrading Node
|
upgradeNode: Upgrading Node
|
||||||
upgradeSysService: Upgrading System Service
|
upgradeSysService: Upgrading System Service
|
||||||
upgradeImage: Download Upgrade Image
|
upgradeImage: Download Upgrade Image
|
||||||
osUpgrade: OS Upgrade
|
osUpgrade: Cluster Upgrade
|
||||||
uploadNew: Upload New Image
|
uploadNew: Upload New Image
|
||||||
|
deleteHeader: Please select an image to delete.
|
||||||
|
deleteExisting: Delete Existing Image
|
||||||
selectExisting: Select Existing Image
|
selectExisting: Select Existing Image
|
||||||
createRepository: Creating Upgrade Repository
|
createRepository: Creating Upgrade Repository
|
||||||
succeeded: Succeeded
|
succeeded: Succeeded
|
||||||
releaseTip: Please read the upgrade documentation carefully. You can view details on the <a href="{url}" target="_blank">Harvester Release Notes</a>.
|
releaseTip: Please read the upgrade documentation carefully. You can view details on the <a href="{url}" target="_blank">Harvester Release Notes</a>.
|
||||||
checkReady: I have read and understood the upgrade instructions related to this Harvester version.
|
checkReady: I have read and understood the upgrade instructions related to this Harvester version.
|
||||||
pending: Pending
|
pending: Pending
|
||||||
|
upload:
|
||||||
|
duplicatedFile: The file you are trying to upload already exists.
|
||||||
repoInfo:
|
repoInfo:
|
||||||
upgradeStatus: Upgrade Status
|
upgradeStatus: Upgrade Status
|
||||||
os: OS
|
os: OS
|
||||||
@@ -975,6 +1029,57 @@ harvester:
|
|||||||
progress: Restore in progress
|
progress: Restore in progress
|
||||||
complete: Restore completed
|
complete: Restore completed
|
||||||
|
|
||||||
|
subnet:
|
||||||
|
cidrBlock:
|
||||||
|
tooltip: The subnet range in CIDR notation. Note that the CIDR blocks of different Subnets' within the same VPC cannot overlap.
|
||||||
|
label: CIDR Block
|
||||||
|
placeholder: e.g. 172.20.0.0/16
|
||||||
|
protocol:
|
||||||
|
label: Protocol
|
||||||
|
provider:
|
||||||
|
tooltip: Network provider for this Subnet. Must be one of the Virtual Machine Networks in OverlayNetwork type.
|
||||||
|
label: Provider
|
||||||
|
vpc:
|
||||||
|
label: Virtual Private Cloud
|
||||||
|
gateway:
|
||||||
|
label: Gateway IP
|
||||||
|
placeholder: e.g. 172.20.0.1
|
||||||
|
private:
|
||||||
|
label: Private Subnet
|
||||||
|
tooltip: Enable network isolation for this Subnet. When enabled, VMs can only communicate within this subnet, even if other subnets exist under the same VPC.
|
||||||
|
allowSubnet:
|
||||||
|
label: Allow Subnets
|
||||||
|
tooltip: You can specify certain subnets which can communicate with this subnet.
|
||||||
|
addSubnet: Add Allow Subnet
|
||||||
|
placeholder: e.g. 172.16.0.0/16
|
||||||
|
excludeIPs:
|
||||||
|
tooltip: The IP address list to reserve from automatic assignment. The gateway IP address is always excluded and will be automatically added to the list.
|
||||||
|
|
||||||
|
vpc:
|
||||||
|
noAddonEnabled:
|
||||||
|
prefix: The kubeovn-operator add-on is not enabled, click
|
||||||
|
middle: here
|
||||||
|
suffix: to enable the add-on to successfully create VPC and subnet.
|
||||||
|
label: Virtual Private Cloud
|
||||||
|
noChild: There is no subnet defined in this Virtual Private Cloud.
|
||||||
|
createSubnet: Create Subnet
|
||||||
|
staticRoutes:
|
||||||
|
label: Static Routes
|
||||||
|
cidr:
|
||||||
|
label: CIDR
|
||||||
|
placeholder: e.g. 172.16.0.0/16
|
||||||
|
nextHopIP:
|
||||||
|
tooltip: The localConnectIP on the other end of the peering VPC.
|
||||||
|
label: Next Hop IP
|
||||||
|
placeholder: e.g. 169.254.0.2
|
||||||
|
vpcPeerings:
|
||||||
|
label: VPC Peerings
|
||||||
|
localConnectIP:
|
||||||
|
tooltip: The designated IP address in CIDR notation of the VPC peering endpoint. Note that both IP addresses (for the VPC and the other VPC) should belong to the same subnet range and should not conflict with the cidrBlock in any existing subnets.
|
||||||
|
label: Local Connect IP
|
||||||
|
placeholder: e.g. 169.254.0.1/16
|
||||||
|
remoteVpc:
|
||||||
|
label: Remote VPC
|
||||||
network:
|
network:
|
||||||
label: Virtual Machine Networks
|
label: Virtual Machine Networks
|
||||||
tabs:
|
tabs:
|
||||||
@@ -1017,6 +1122,7 @@ harvester:
|
|||||||
accessKeyId: Specify your access key ID
|
accessKeyId: Specify your access key ID
|
||||||
secretAccessKey: Specify your secret access key
|
secretAccessKey: Specify your secret access key
|
||||||
cert: Upload a self-signed SSL certificate
|
cert: Upload a self-signed SSL certificate
|
||||||
|
noSearchMatch: No settings match your search.
|
||||||
vlanChangeTip: The newly modified default network interface only applies to newly added nodes, not existing ones.
|
vlanChangeTip: The newly modified default network interface only applies to newly added nodes, not existing ones.
|
||||||
defaultPhysicalNIC: Default Network Interface
|
defaultPhysicalNIC: Default Network Interface
|
||||||
modifiedMessage: Settings that have been customized from default settings are tagged with 'Modified'.
|
modifiedMessage: Settings that have been customized from default settings are tagged with 'Modified'.
|
||||||
@@ -1030,6 +1136,9 @@ harvester:
|
|||||||
publicCertificate: Public Certificate
|
publicCertificate: Public Certificate
|
||||||
privateKey: Private Key
|
privateKey: Private Key
|
||||||
ca: CA
|
ca: CA
|
||||||
|
rancherCluster:
|
||||||
|
kubeConfig: Rancher KubeConfig
|
||||||
|
removeUpstreamClusterWhenNamespaceIsDeleted: Remove Upstream Cluster When Namespace Is Deleted
|
||||||
storageNetwork:
|
storageNetwork:
|
||||||
range:
|
range:
|
||||||
placeholder: e.g. 172.16.0.0/24
|
placeholder: e.g. 172.16.0.0/24
|
||||||
@@ -1038,14 +1147,28 @@ harvester:
|
|||||||
clusterNetwork: Cluster Network
|
clusterNetwork: Cluster Network
|
||||||
vlan: VLAN ID
|
vlan: VLAN ID
|
||||||
exclude:
|
exclude:
|
||||||
label: Exclude
|
label: Exclude IPs
|
||||||
placeholder: e.g. 172.16.0.1/32
|
placeholder: e.g. 172.16.0.1
|
||||||
invalid: '"Exclude list" is invalid.'
|
invalid: '"Exclude list" is invalid.'
|
||||||
addIp: Add Exclude IP
|
addIp: Add Exclude IP
|
||||||
warning: 'WARNING: <br/> Any change to storage-network requires shutting down all virtual machines before applying this setting. <br/> Users have to ensure the cluster network is configured and VLAN Configuration will cover all nodes and ensure the network connectivity is working and expected in all nodes.'
|
warning: 'WARNING: <br/> Any change to storage-network requires shutting down all virtual machines before applying this setting. <br/> Users have to ensure the cluster network is configured and VLAN Configuration will cover all nodes and ensure the network connectivity is working and expected in all nodes.'
|
||||||
tip: 'Specify an IP range in the IPv4 CIDR format. <code>Number of IPs Required = Number of Nodes * 2 + Number of Disks * 2 + Number of Images to Download/Upload </code>. For more information about storage network settings, see the <a href="{url}" target="_blank">documentation</a>.'
|
tip: 'Specify an IP range in the IPv4 CIDR format. <code>Number of IPs Required = Number of Nodes * 2 + Number of Disks * 2 + Number of Images to Download/Upload </code>. For more information about storage network settings, see the <a href="{url}" target="_blank">documentation</a>.'
|
||||||
vmForceDeletionPolicy:
|
vmForceDeletionPolicy:
|
||||||
period: Period
|
period: Period
|
||||||
|
vmMigrationNetwork:
|
||||||
|
parseError: "Failed to parse existing configuration."
|
||||||
|
fetchError: "Failed to load required network resources: {error}. Please refresh the page or try again later."
|
||||||
|
clusterNetwork: Cluster Network
|
||||||
|
vlan: VLAN ID
|
||||||
|
range:
|
||||||
|
placeholder: e.g. 172.16.0.0/24
|
||||||
|
label: IP Range
|
||||||
|
invalid: '"Range" is invalid.'
|
||||||
|
exclude:
|
||||||
|
label: Excluded IPs
|
||||||
|
placeholder: e.g. 172.16.0.1/32
|
||||||
|
invalid: '"Exclude list" is invalid.'
|
||||||
|
addButton: Add Exclude IP
|
||||||
ratio : Ratio
|
ratio : Ratio
|
||||||
autoRotateRKE2Certs:
|
autoRotateRKE2Certs:
|
||||||
expiringInHours: Expiring in
|
expiringInHours: Expiring in
|
||||||
@@ -1059,6 +1182,11 @@ harvester:
|
|||||||
provisioner: Provisioner
|
provisioner: Provisioner
|
||||||
volumeSnapshotClassName: Volume Snapshot Class Name
|
volumeSnapshotClassName: Volume Snapshot Class Name
|
||||||
backupVolumeSnapshotClassName: Backup Volume Snapshot Class Name
|
backupVolumeSnapshotClassName: Backup Volume Snapshot Class Name
|
||||||
|
csiOnlineExpandValidation:
|
||||||
|
provisioner: Provisioner
|
||||||
|
value: Value
|
||||||
|
invalidJsonFormat: "Configuration value is not a valid JSON format: {error}"
|
||||||
|
failedToLoadDrivers: "Failed to load CSI drivers. Error: {error}"
|
||||||
containerdRegistry:
|
containerdRegistry:
|
||||||
mirrors:
|
mirrors:
|
||||||
mirrors: Mirrors
|
mirrors: Mirrors
|
||||||
@@ -1080,10 +1208,19 @@ harvester:
|
|||||||
addConfig: Add Configuration
|
addConfig: Add Configuration
|
||||||
|
|
||||||
upgrade:
|
upgrade:
|
||||||
selectExitImage: Please select the OS image to upgrade.
|
unknownImageName: Image name is not found.
|
||||||
imageUrl: Please input a valid image URL.
|
imageUrl: Please input a valid image URL.
|
||||||
chooseFile: Please select to upload an image.
|
chooseFile: Please select to upload an image.
|
||||||
checksum: Checksum
|
checksum: Checksum
|
||||||
|
networkError: Unable to upload the image. Resolve network issues that may have occurred and try again.
|
||||||
|
cancelUpload: Cancelled the image upload.
|
||||||
|
uploadSuccess: "{name} uploaded successfully. Press Upgrade button to start the cluster upgrade process."
|
||||||
|
deleteImage: Please select an image to delete.
|
||||||
|
deleteSuccess: "{name} deleted successfully."
|
||||||
|
imagePreloadStrategy: Image Preload Strategy
|
||||||
|
restoreVM: Restore VM
|
||||||
|
strategyType: Strategy Type
|
||||||
|
concurrency: Concurrency
|
||||||
harvesterMonitoring:
|
harvesterMonitoring:
|
||||||
label: Harvester Monitoring
|
label: Harvester Monitoring
|
||||||
section:
|
section:
|
||||||
@@ -1245,6 +1382,32 @@ harvester:
|
|||||||
allowedTopologies:
|
allowedTopologies:
|
||||||
title: Allowed Topologies
|
title: Allowed Topologies
|
||||||
tooltip: Allowed Topologies helps scheduling virtual machines on hosts which match all of below expressions.
|
tooltip: Allowed Topologies helps scheduling virtual machines on hosts which match all of below expressions.
|
||||||
|
internal:
|
||||||
|
label: Internal Storage Class
|
||||||
|
cannotDeleteTooltip: Internal storage class volumes cannot be deleted
|
||||||
|
cannotDeleteOrDefaultTooltip: Internal storage classes cannot be deleted or set as default
|
||||||
|
cdiSettings:
|
||||||
|
label: CDI Settings
|
||||||
|
volumeModeAccessModes:
|
||||||
|
label: Volume Mode / Access Modes
|
||||||
|
tooltip: Specifies the default volume mode and access modes.
|
||||||
|
add: Add
|
||||||
|
volumeMode: Volume Mode
|
||||||
|
accessModes:
|
||||||
|
label: Access Modes
|
||||||
|
volumeSnapshotClass:
|
||||||
|
label: Volume Snapshot Class
|
||||||
|
tooltip: Sets the Volume Snapshot Class name to be used when taking snapshots of PVCs under this StorageClass.
|
||||||
|
cloneStrategy:
|
||||||
|
label: Clone Strategy
|
||||||
|
tooltip: Defines the clone strategy to use for volumes created with this StorageClass.
|
||||||
|
copy: Copy
|
||||||
|
snapshot: Snapshot
|
||||||
|
csiClone: CSI Clone
|
||||||
|
fileSystemOverhead:
|
||||||
|
label: File System Overhead
|
||||||
|
tooltip: Specifies the percentage of filesystem overhead to consider when calculating PVC size.
|
||||||
|
placeholder: e.g. 0.05 (up to 3 decimal places, between 0 and 1)
|
||||||
|
|
||||||
vlanConfig:
|
vlanConfig:
|
||||||
title: Network Configuration
|
title: Network Configuration
|
||||||
@@ -1549,6 +1712,7 @@ advancedSettings:
|
|||||||
'harv-backup-target': Custom backup target to store virtual machine backups.
|
'harv-backup-target': Custom backup target to store virtual machine backups.
|
||||||
'branding': Branding allows administrators to globally re-brand the UI by customizing the Harvester product name, logos, and color scheme.
|
'branding': Branding allows administrators to globally re-brand the UI by customizing the Harvester product name, logos, and color scheme.
|
||||||
'harv-csi-driver-config': Configure additional information for CSI drivers.
|
'harv-csi-driver-config': Configure additional information for CSI drivers.
|
||||||
|
'harv-csi-online-expand-validation': Allow online volume expansion for specific CSI drivers.
|
||||||
'harv-containerd-registry': Containerd Registry Configuration to connect private registries.
|
'harv-containerd-registry': Containerd Registry Configuration to connect private registries.
|
||||||
'harv-log-level': Configure Harvester server log level. Defaults to Info.
|
'harv-log-level': Configure Harvester server log level. Defaults to Info.
|
||||||
'harv-server-version': Harvester server version.
|
'harv-server-version': Harvester server version.
|
||||||
@@ -1567,7 +1731,7 @@ advancedSettings:
|
|||||||
'harv-vm-force-reset-policy': Configuration for the force-reset action when a virtual machine is stuck on a node that is down.
|
'harv-vm-force-reset-policy': Configuration for the force-reset action when a virtual machine is stuck on a node that is down.
|
||||||
'harv-ssl-parameters': Custom SSL Parameters for TLS validation.
|
'harv-ssl-parameters': Custom SSL Parameters for TLS validation.
|
||||||
'harv-storage-network': 'Longhorn storage-network setting.'
|
'harv-storage-network': 'Longhorn storage-network setting.'
|
||||||
'harv-support-bundle-namespaces': Specify resources in other namespaces to be collected by the support package.
|
'harv-support-bundle-namespaces': Select additional namespaces to include in the support bundle.
|
||||||
'harv-auto-disk-provision-paths': Specify the disks(using glob pattern) that Harvester will automatically add as virtual machine storage.
|
'harv-auto-disk-provision-paths': Specify the disks(using glob pattern) that Harvester will automatically add as virtual machine storage.
|
||||||
'harv-support-bundle-image': Support bundle image configuration. Find different versions in <a href="https://hub.docker.com/r/rancher/support-bundle-kit/tags" target="_blank">rancher/support-bundle-kit</a>.
|
'harv-support-bundle-image': Support bundle image configuration. Find different versions in <a href="https://hub.docker.com/r/rancher/support-bundle-kit/tags" target="_blank">rancher/support-bundle-kit</a>.
|
||||||
'harv-release-download-url': This setting allows you to configure the <code>upgrade release download</code> URL address. Harvester will get the ISO URL and checksum value from the (<code>$URL</code>/<code>$VERSION</code>/version.yaml) file hosted by the configured URL.
|
'harv-release-download-url': This setting allows you to configure the <code>upgrade release download</code> URL address. Harvester will get the ISO URL and checksum value from the (<code>$URL</code>/<code>$VERSION</code>/version.yaml) file hosted by the configured URL.
|
||||||
@@ -1577,6 +1741,10 @@ advancedSettings:
|
|||||||
'harv-kubeconfig-default-token-ttl-minutes': 'TTL (in minutes) applied on Harvester administration kubeconfig files. Default is 0, which means to never expire.'
|
'harv-kubeconfig-default-token-ttl-minutes': 'TTL (in minutes) applied on Harvester administration kubeconfig files. Default is 0, which means to never expire.'
|
||||||
'harv-longhorn-v2-data-engine-enabled': 'Enable the Longhorn V2 data engine. Default is false. <ul><li>Changing this setting will restart RKE2 on all nodes. This will not affect running VM workloads.</li><li>If you see "not enough hugepages-2Mi capacity" errors when enabling this setting, wait a minute for the error to clear. If the error remains, reboot the affected node.</li></ul>'
|
'harv-longhorn-v2-data-engine-enabled': 'Enable the Longhorn V2 data engine. Default is false. <ul><li>Changing this setting will restart RKE2 on all nodes. This will not affect running VM workloads.</li><li>If you see "not enough hugepages-2Mi capacity" errors when enabling this setting, wait a minute for the error to clear. If the error remains, reboot the affected node.</li></ul>'
|
||||||
'harv-additional-guest-memory-overhead-ratio': 'The ratio for kubevirt to adjust the VM overhead memory. The value could be zero, empty value or floating number between 1.0 and 10.0, default to 1.5.'
|
'harv-additional-guest-memory-overhead-ratio': 'The ratio for kubevirt to adjust the VM overhead memory. The value could be zero, empty value or floating number between 1.0 and 10.0, default to 1.5.'
|
||||||
|
'harv-upgrade-config': 'Configure image preloading and VM restore options for upgrades. See related fields in <a href="{url}" target="_blank" rel="noopener">settings/upgrade-config</a>'
|
||||||
|
'harv-vm-migration-network': 'Segregated network for VM migration traffic.'
|
||||||
|
'harv-rancher-cluster': 'Configure Rancher cluster integration settings for guest cluster management.'
|
||||||
|
'harv-max-hotplug-ratio': 'The ratio for kubevirt to limit the maximum CPU and memory that can be hotplugged to a VM. The value could be an integer between 1 and 20, default to 4.'
|
||||||
|
|
||||||
typeLabel:
|
typeLabel:
|
||||||
kubevirt.io.virtualmachine: |-
|
kubevirt.io.virtualmachine: |-
|
||||||
@@ -1634,6 +1802,11 @@ typeLabel:
|
|||||||
one { Virtual Machine Backup }
|
one { Virtual Machine Backup }
|
||||||
other { Virtual Machine Backups }
|
other { Virtual Machine Backups }
|
||||||
}
|
}
|
||||||
|
kubeovn.io.vpc: |-
|
||||||
|
{count, plural,
|
||||||
|
one { Virtual Private Cloud }
|
||||||
|
other { Virtual Private Clouds }
|
||||||
|
}
|
||||||
harvesterhci.io.cloudtemplate: |-
|
harvesterhci.io.cloudtemplate: |-
|
||||||
{count, plural,
|
{count, plural,
|
||||||
one { Cloud Configuration Template }
|
one { Cloud Configuration Template }
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ export default {
|
|||||||
|
|
||||||
const hash = {
|
const hash = {
|
||||||
vms: this.fetchClusterResources(HCI.VM),
|
vms: this.fetchClusterResources(HCI.VM),
|
||||||
|
pvcs: this.fetchClusterResources(PVC),
|
||||||
nodes: this.fetchClusterResources(NODE),
|
nodes: this.fetchClusterResources(NODE),
|
||||||
events: this.fetchClusterResources(EVENT),
|
events: this.fetchClusterResources(EVENT),
|
||||||
metricNodes: this.fetchClusterResources(METRIC.NODE),
|
metricNodes: this.fetchClusterResources(METRIC.NODE),
|
||||||
@@ -221,6 +222,7 @@ export default {
|
|||||||
nodes: [],
|
nodes: [],
|
||||||
metricNodes: [],
|
metricNodes: [],
|
||||||
vms: [],
|
vms: [],
|
||||||
|
pvcs: [],
|
||||||
monitoring: {},
|
monitoring: {},
|
||||||
VM_DASHBOARD_METRICS_URL,
|
VM_DASHBOARD_METRICS_URL,
|
||||||
CLUSTER_METRICS_SUMMARY_URL,
|
CLUSTER_METRICS_SUMMARY_URL,
|
||||||
@@ -285,6 +287,14 @@ export default {
|
|||||||
out[resource.type].name = this.t(`typeLabel."${ resource.spoofed.name }"`, { count: out[resource.type].total });
|
out[resource.type].name = this.t(`typeLabel."${ resource.spoofed.name }"`, { count: out[resource.type].total });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (resource.type === PVC) {
|
||||||
|
// filter out the golden image volumes
|
||||||
|
const goldenImageVolumeCount = (this.pvcs || []).filter((pvc) => pvc.isGoldenImageVolume).length;
|
||||||
|
|
||||||
|
out[resource.type].useful = out[resource.type].useful - goldenImageVolumeCount;
|
||||||
|
out[resource.type].total = out[resource.type].total - goldenImageVolumeCount;
|
||||||
|
}
|
||||||
|
|
||||||
if (resource.type === HCI.BLOCK_DEVICE) {
|
if (resource.type === HCI.BLOCK_DEVICE) {
|
||||||
let total = 0;
|
let total = 0;
|
||||||
let errorCount = 0;
|
let errorCount = 0;
|
||||||
|
|||||||
@@ -2,13 +2,12 @@
|
|||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import Loading from '@shell/components/Loading';
|
import Loading from '@shell/components/Loading';
|
||||||
import { VIEW_IN_API, DEV } from '@shell/store/prefs';
|
|
||||||
import { MANAGEMENT } from '@shell/config/types';
|
import { MANAGEMENT } from '@shell/config/types';
|
||||||
import { allHash } from '@shell/utils/promise';
|
import { allHash } from '@shell/utils/promise';
|
||||||
import Tabbed from '@shell/components/Tabbed/index.vue';
|
import Tabbed from '@shell/components/Tabbed/index.vue';
|
||||||
import Tab from '@shell/components/Tabbed/Tab.vue';
|
import Tab from '@shell/components/Tabbed/Tab.vue';
|
||||||
import Settings from '@pkg/harvester/components/SettingList.vue';
|
import Settings from '@pkg/harvester/components/SettingList.vue';
|
||||||
import { HCI_ALLOWED_SETTINGS, HCI_SINGLE_CLUSTER_ALLOWED_SETTING } from '../config/settings';
|
import { HCI_ALLOWED_SETTINGS, HCI_SINGLE_CLUSTER_ALLOWED_SETTING, HCI_SETTING } from '../config/settings';
|
||||||
import { HCI } from '../types';
|
import { HCI } from '../types';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -21,14 +20,6 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async fetch() {
|
async fetch() {
|
||||||
let isDev;
|
|
||||||
|
|
||||||
try {
|
|
||||||
isDev = this.$store.getters['prefs/get'](VIEW_IN_API);
|
|
||||||
} catch {
|
|
||||||
isDev = this.$store.getters['prefs/get'](DEV);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSingleProduct = !!this.$store.getters['isSingleProduct'];
|
const isSingleProduct = !!this.$store.getters['isSingleProduct'];
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
@@ -77,7 +68,7 @@ export default {
|
|||||||
};
|
};
|
||||||
|
|
||||||
s.hide = s.canHide = (s.kind === 'json' || s.kind === 'multiline' || s.customFormatter === 'json' || s.data.customFormatter === 'json');
|
s.hide = s.canHide = (s.kind === 'json' || s.kind === 'multiline' || s.customFormatter === 'json' || s.data.customFormatter === 'json');
|
||||||
s.hasActions = !s.readOnly || isDev;
|
s.hasActions = s.id === HCI_SETTING.SERVER_VERSION ? true : !s.readOnly;
|
||||||
initSettings.push(s);
|
initSettings.push(s);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -94,7 +85,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return { initSettings: [] };
|
return { initSettings: [], searchQuery: '' };
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
@@ -125,7 +116,15 @@ export default {
|
|||||||
return {
|
return {
|
||||||
...s,
|
...s,
|
||||||
description: isHarvester ? `advancedSettings.descriptions.harv-${ s.id }` : `advancedSettings.descriptions.${ s.id }`,
|
description: isHarvester ? `advancedSettings.descriptions.harv-${ s.id }` : `advancedSettings.descriptions.${ s.id }`,
|
||||||
customized: (!s.readOnly && s.data.value && s.data.value !== s.data.default) || s.data.hasCustomized
|
customized: (!s.readOnly && s.data.value && (
|
||||||
|
s.kind === 'json' ? (() => {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(JSON.parse(s.data.value)) !== JSON.stringify(JSON.parse(s.data.default));
|
||||||
|
} catch {
|
||||||
|
return s.data.value !== s.data.default;
|
||||||
|
}
|
||||||
|
})() : s.data.value !== s.data.default
|
||||||
|
)) || s.data.hasCustomized
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -144,9 +143,19 @@ export default {
|
|||||||
{{ t('harvester.setting.modifiedMessage') }}
|
{{ t('harvester.setting.modifiedMessage') }}
|
||||||
</div>
|
</div>
|
||||||
</Banner>
|
</Banner>
|
||||||
|
<div class="fixed-header-actions harvester-settings-search">
|
||||||
|
<div class="search row">
|
||||||
|
<input
|
||||||
|
v-model="searchQuery"
|
||||||
|
type="search"
|
||||||
|
class="input-sm search-box"
|
||||||
|
:aria-label="t('sortableTable.searchLabel')"
|
||||||
|
:placeholder="t('sortableTable.search')"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<Tabbed
|
<Tabbed
|
||||||
class="mt-30"
|
class="mt-15"
|
||||||
>
|
>
|
||||||
<Tab
|
<Tab
|
||||||
name="advanced"
|
name="advanced"
|
||||||
@@ -155,6 +164,7 @@ export default {
|
|||||||
>
|
>
|
||||||
<Settings
|
<Settings
|
||||||
:settings="settings"
|
:settings="settings"
|
||||||
|
:search-query="searchQuery"
|
||||||
category="advanced"
|
category="advanced"
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
@@ -165,6 +175,7 @@ export default {
|
|||||||
>
|
>
|
||||||
<Settings
|
<Settings
|
||||||
:settings="settings"
|
:settings="settings"
|
||||||
|
:search-query="searchQuery"
|
||||||
category="ui"
|
category="ui"
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
@@ -176,4 +187,20 @@ export default {
|
|||||||
.settings-banner {
|
.settings-banner {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.harvester-settings-search {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
height: 40px;
|
||||||
|
margin-left: 10px;
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -83,6 +83,34 @@ export default {
|
|||||||
:rows="rows"
|
:rows="rows"
|
||||||
:schema="schema"
|
:schema="schema"
|
||||||
:headers="headers"
|
:headers="headers"
|
||||||
/>
|
>
|
||||||
|
<template #cell:name="{ row }">
|
||||||
|
<td>
|
||||||
|
<div>
|
||||||
|
<router-link
|
||||||
|
v-if="row?.detailLocation"
|
||||||
|
:to="row.detailLocation"
|
||||||
|
>
|
||||||
|
{{ row.nameDisplay }}
|
||||||
|
<i
|
||||||
|
v-if="row.isInternalStorageClass && typeof row.isInternalStorageClass === 'function' ? row.isInternalStorageClass() : false"
|
||||||
|
v-clean-tooltip="t('harvester.storage.internal.cannotDeleteOrDefaultTooltip')"
|
||||||
|
class="icon icon-info text-info"
|
||||||
|
style="margin-left: 0.4em;"
|
||||||
|
/>
|
||||||
|
</router-link>
|
||||||
|
<span v-else>
|
||||||
|
{{ row.nameDisplay }}
|
||||||
|
<i
|
||||||
|
v-if="row.isInternalStorageClass && typeof row.isInternalStorageClass === 'function' ? row.isInternalStorageClass() : false"
|
||||||
|
v-clean-tooltip="t('harvester.storage.internal.cannotDeleteOrDefaultTooltip')"
|
||||||
|
class="icon icon-info text-info"
|
||||||
|
style="margin-left: 0.4em;"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</template>
|
||||||
|
</ResourceTable>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import ResourceTable from '@shell/components/ResourceTable';
|
|||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import { defaultTableSortGenerationFn } from '@shell/components/ResourceTable.vue';
|
import { defaultTableSortGenerationFn } from '@shell/components/ResourceTable.vue';
|
||||||
import FilterLabel from '../components/FilterLabel';
|
import FilterLabel from '../components/FilterLabel';
|
||||||
|
import { HCI as HCI_ANNOTATIONS } from '../config/labels-annotations';
|
||||||
|
import { isInternalStorageClass } from '../utils/storage-class';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ListHarvesterImage',
|
name: 'ListHarvesterImage',
|
||||||
@@ -53,6 +55,13 @@ export default {
|
|||||||
|
|
||||||
return base;
|
return base;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isInternalStorageClass(row) {
|
||||||
|
const name = row?.spec?.targetStorageClassName ||
|
||||||
|
row?.metadata?.annotations?.[HCI_ANNOTATIONS.STORAGE_CLASS];
|
||||||
|
|
||||||
|
return isInternalStorageClass(name);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -90,6 +99,11 @@ export default {
|
|||||||
v-if="row.isEncrypted"
|
v-if="row.isEncrypted"
|
||||||
class="icon icon-lock"
|
class="icon icon-lock"
|
||||||
/>
|
/>
|
||||||
|
<i
|
||||||
|
v-if="row.isImportedImage"
|
||||||
|
v-clean-tooltip="t('harvester.image.tooltip.imported')"
|
||||||
|
class="icon icon-info"
|
||||||
|
/>
|
||||||
</router-link>
|
</router-link>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
{{ row.nameDisplay }}
|
{{ row.nameDisplay }}
|
||||||
|
|||||||
@@ -6,10 +6,9 @@ import {
|
|||||||
} from '@shell/config/types';
|
} from '@shell/config/types';
|
||||||
import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers';
|
import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers';
|
||||||
import HarvesterVolumeState from '../formatters/HarvesterVolumeState';
|
import HarvesterVolumeState from '../formatters/HarvesterVolumeState';
|
||||||
|
|
||||||
import { allSettled } from '../utils/promise';
|
import { allSettled } from '../utils/promise';
|
||||||
import { HCI, VOLUME_SNAPSHOT } from '../types';
|
import { HCI, VOLUME_SNAPSHOT } from '../types';
|
||||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
import { INTERNAL_STORAGE_CLASS } from '../config/types';
|
||||||
|
|
||||||
const schema = {
|
const schema = {
|
||||||
id: HCI.VOLUME,
|
id: HCI.VOLUME,
|
||||||
@@ -72,7 +71,7 @@ export default {
|
|||||||
},
|
},
|
||||||
filterRows() {
|
filterRows() {
|
||||||
// we only show the non golden image PVCs in the list
|
// we only show the non golden image PVCs in the list
|
||||||
return this.rows.filter((pvc) => pvc?.metadata?.annotations?.[HCI_ANNOTATIONS.GOLDEN_IMAGE] !== 'true');
|
return this.rows.filter((pvc) => !pvc?.isGoldenImageVolume);
|
||||||
},
|
},
|
||||||
headers() {
|
headers() {
|
||||||
return [
|
return [
|
||||||
@@ -132,6 +131,10 @@ export default {
|
|||||||
|
|
||||||
getVMName(row) {
|
getVMName(row) {
|
||||||
return row.attachVM?.metadata?.name || '';
|
return row.attachVM?.metadata?.name || '';
|
||||||
|
},
|
||||||
|
|
||||||
|
isInternalStorageClass(storageClassName) {
|
||||||
|
return this.$store.getters['type-map/labelFor'](INTERNAL_STORAGE_CLASS, storageClassName);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -172,7 +175,7 @@ export default {
|
|||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #col:name="{row}">
|
<template #col:name="{ row }">
|
||||||
<td>
|
<td>
|
||||||
<span>
|
<span>
|
||||||
<router-link
|
<router-link
|
||||||
|
|||||||
@@ -0,0 +1,285 @@
|
|||||||
|
<script>
|
||||||
|
import Loading from '@shell/components/Loading';
|
||||||
|
import ResourceTable from '@shell/components/ResourceTable';
|
||||||
|
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester';
|
||||||
|
import { NAME, AGE, NAMESPACE, STATE } from '@shell/config/table-headers';
|
||||||
|
import { PROVIDER, PROTOCOL, CIDR_BLOCK } from '@pkg/harvester/config/table-headers';
|
||||||
|
import { HCI } from '../types';
|
||||||
|
import { VPC } from '../config/query-params';
|
||||||
|
import { ADD_ONS } from '../config/harvester-map';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import MessageLink from '@shell/components/MessageLink';
|
||||||
|
import Banner from '@components/Banner/Banner.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterVPC',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
ResourceTable,
|
||||||
|
Loading,
|
||||||
|
MessageLink,
|
||||||
|
Banner
|
||||||
|
},
|
||||||
|
|
||||||
|
inheritAttrs: false,
|
||||||
|
|
||||||
|
props: {
|
||||||
|
schema: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const hash = await allHash({ addons: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS }) });
|
||||||
|
|
||||||
|
this.enabledKubeOvnAddon = hash.addons.find((addon) => addon.name === ADD_ONS.KUBEOVN_OPERATOR)?.spec?.enabled === true;
|
||||||
|
|
||||||
|
if (this.enabledKubeOvnAddon) {
|
||||||
|
try {
|
||||||
|
await allHash({
|
||||||
|
rows: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SUBNET }),
|
||||||
|
vpcs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VPC }),
|
||||||
|
});
|
||||||
|
this.$store.dispatch('type-map/configureType', { match: HCI.SUBNET, isCreatable: this.enabledKubeOvnAddon });
|
||||||
|
} catch (e) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error('Error fetching VPC and Subnet data:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
HCI,
|
||||||
|
hasBothSchema: false,
|
||||||
|
enabledKubeOvnAddon: false,
|
||||||
|
to: `${ HCI.ADD_ONS }/kube-system/${ ADD_ONS.KUBEOVN_OPERATOR }?mode=edit`
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
headers() {
|
||||||
|
return [
|
||||||
|
STATE,
|
||||||
|
NAME,
|
||||||
|
NAMESPACE,
|
||||||
|
CIDR_BLOCK,
|
||||||
|
PROTOCOL,
|
||||||
|
PROVIDER,
|
||||||
|
AGE
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
rows() {
|
||||||
|
return this.$store.getters[`harvester/all`](HCI.SUBNET) || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
vpcWithoutSubnets() {
|
||||||
|
const vpcs = this.$store.getters[`harvester/all`](HCI.VPC) || [];
|
||||||
|
|
||||||
|
const out = vpcs.map((v) => {
|
||||||
|
const hasChild = v.status?.subnets?.length > 0 || false;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...v,
|
||||||
|
hasChild
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
|
isSubnetCreatable() {
|
||||||
|
return (this.subnetSchema?.collectionMethods || []).includes('POST');
|
||||||
|
},
|
||||||
|
|
||||||
|
rowsWithFakeVpcs() {
|
||||||
|
const fakeRows = this.vpcWithoutSubnets.map((vpc) => {
|
||||||
|
return {
|
||||||
|
groupByLabel: vpc.id,
|
||||||
|
isFake: true,
|
||||||
|
mainRowKey: vpc.id,
|
||||||
|
nameDisplay: vpc.id,
|
||||||
|
groupByVpc: vpc.id,
|
||||||
|
availableActions: []
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return [...this.rows, ...fakeRows];
|
||||||
|
},
|
||||||
|
|
||||||
|
createVPCLocation() {
|
||||||
|
const location = {
|
||||||
|
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`,
|
||||||
|
params: {
|
||||||
|
product: HARVESTER_PRODUCT,
|
||||||
|
resource: HCI.VPC,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return location;
|
||||||
|
},
|
||||||
|
|
||||||
|
vpcSchema() {
|
||||||
|
return this.$store.getters[`harvester/schemaFor`](HCI.VPC);
|
||||||
|
},
|
||||||
|
|
||||||
|
subnetSchema() {
|
||||||
|
return this.$store.getters[`harvester/schemaFor`](HCI.SUBNET);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
groupLabel(group) {
|
||||||
|
return `${ this.t('harvester.vpc.label') }: ${ group.key }`;
|
||||||
|
},
|
||||||
|
|
||||||
|
slotName(vpc) {
|
||||||
|
return `main-row:${ vpc }`;
|
||||||
|
},
|
||||||
|
|
||||||
|
createSubnetLocation(group) {
|
||||||
|
const vpc = group.key;
|
||||||
|
|
||||||
|
const location = {
|
||||||
|
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`,
|
||||||
|
params: {
|
||||||
|
product: HARVESTER_PRODUCT,
|
||||||
|
resource: HCI.SUBNET,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
location.query = { [VPC]: vpc };
|
||||||
|
|
||||||
|
return location;
|
||||||
|
},
|
||||||
|
|
||||||
|
showVpcAction(event, group) {
|
||||||
|
const vpc = group.key;
|
||||||
|
|
||||||
|
const resource = this.$store.getters[`harvester/byId`](HCI.VPC, vpc);
|
||||||
|
|
||||||
|
this.$store.commit(`action-menu/show`, {
|
||||||
|
resources: [resource],
|
||||||
|
elem: event.target
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
typeDisplay() {
|
||||||
|
return this.t('harvester.vpc.label');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Loading v-if="$fetchState.pending" />
|
||||||
|
<div v-else-if="!enabledKubeOvnAddon">
|
||||||
|
<Banner color="warning">
|
||||||
|
<MessageLink
|
||||||
|
:to="to"
|
||||||
|
prefix-label="harvester.vpc.noAddonEnabled.prefix"
|
||||||
|
middle-label="harvester.vpc.noAddonEnabled.middle"
|
||||||
|
suffix-label="harvester.vpc.noAddonEnabled.suffix"
|
||||||
|
/>
|
||||||
|
</Banner>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<Masthead
|
||||||
|
:schema="vpcSchema"
|
||||||
|
:type-display="t('harvester.vpc.label')"
|
||||||
|
:resource="HCI.VPC"
|
||||||
|
:create-location="createVPCLocation"
|
||||||
|
:create-button-label="t('harvester.clusterNetwork.create.button.label')"
|
||||||
|
/>
|
||||||
|
<ResourceTable
|
||||||
|
:rows="rowsWithFakeVpcs"
|
||||||
|
:headers="headers"
|
||||||
|
:schema="subnetSchema"
|
||||||
|
:groupable="true"
|
||||||
|
group-by="groupByVpc"
|
||||||
|
>
|
||||||
|
<template #header-middle>
|
||||||
|
<div />
|
||||||
|
</template>
|
||||||
|
<template #group-by="{group}">
|
||||||
|
<div class="group-bar">
|
||||||
|
<div class="group-tab">
|
||||||
|
<span>
|
||||||
|
{{ groupLabel(group) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<router-link
|
||||||
|
v-if="isSubnetCreatable"
|
||||||
|
class="btn btn-sm role-secondary mr-5"
|
||||||
|
:to="createSubnetLocation(group)"
|
||||||
|
>
|
||||||
|
{{ t('harvester.vpc.createSubnet') }}
|
||||||
|
</router-link>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm role-multi-action actions mr-10"
|
||||||
|
@click="showVpcAction($event, group)"
|
||||||
|
>
|
||||||
|
<i class="icon icon-actions" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template
|
||||||
|
v-for="(vpc) in vpcWithoutSubnets"
|
||||||
|
:key="vpc.id"
|
||||||
|
v-slot:[slotName(vpc.id)]
|
||||||
|
>
|
||||||
|
<tr
|
||||||
|
v-show="!vpc.hasChild"
|
||||||
|
class="main-row"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
class="empty text-center"
|
||||||
|
colspan="12"
|
||||||
|
>
|
||||||
|
{{ t('harvester.vpc.noChild') }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</ResourceTable>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.state {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.icon-warning {
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.group-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.right {
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-tab {
|
||||||
|
&, &::after {
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
right: -20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
SPAN {
|
||||||
|
color: var(--body-text) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -22,8 +22,8 @@ export const VM_HEADERS = [
|
|||||||
{
|
{
|
||||||
name: 'CPU',
|
name: 'CPU',
|
||||||
label: 'CPU',
|
label: 'CPU',
|
||||||
sort: ['spec.template.spec.domain.cpu.cores'],
|
sort: ['displayCPU'],
|
||||||
value: 'spec.template.spec.domain.cpu.cores',
|
value: 'displayCPU',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
dashIfEmpty: true,
|
dashIfEmpty: true,
|
||||||
},
|
},
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user