mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-03 20:56:06 +08:00
Compare commits
187
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
089573f193 | ||
|
|
5d0c4f0c5a | ||
|
|
57bb1e8562 | ||
|
|
30ff129691 | ||
|
|
9175f16bfc | ||
|
|
7c5e0f93d1 | ||
|
|
8a7d3dffbe | ||
|
|
b7810c5970 | ||
|
|
72dedd46d1 | ||
|
|
24f716ab20 | ||
|
|
38ad0fe1a7 | ||
|
|
c84d262099 | ||
|
|
dbb53eae00 | ||
|
|
4afacd754a | ||
|
|
10d159e2c7 | ||
|
|
de755a662a | ||
|
|
f1200f4508 | ||
|
|
0ca2b0591a | ||
|
|
44f003309c | ||
|
|
7c1596c965 | ||
|
|
a5130986a8 | ||
|
|
e4ae7cec5e | ||
|
|
2c7b59daed | ||
|
|
224e34315b | ||
|
|
9f56f9eccc | ||
|
|
dd76956972 | ||
|
|
d92a53d9a7 | ||
|
|
afc3f0d87f | ||
|
|
a607b604f8 | ||
|
|
4d7b0f7350 | ||
|
|
c570b5870b | ||
|
|
af36163b29 | ||
|
|
b3dbc2f0d3 | ||
|
|
fed86d4c24 | ||
|
|
d18ebfb26b | ||
|
|
477206d8d0 | ||
|
|
fd0836d489 | ||
|
|
2e3536abc6 | ||
|
|
45c0dce61b | ||
|
|
6b8b1f68eb | ||
|
|
bb645c00db | ||
|
|
ed8c6e81ca | ||
|
|
ed692b579d | ||
|
|
f02074a25e | ||
|
|
a8f8f0d288 | ||
|
|
9fa9b3837b | ||
|
|
4b3bdf8a59 | ||
|
|
7bdf40d737 | ||
|
|
a1e7345437 | ||
|
|
9322a77b20 | ||
|
|
ed7d7ffd2e | ||
|
|
2ef8e2e0aa | ||
|
|
a0ea6beedb | ||
|
|
85a723e20c | ||
|
|
bbe6b1bdf0 | ||
|
|
81a572e5b5 | ||
|
|
1a727f0001 | ||
|
|
91862244e1 | ||
|
|
c7b0e9ee7a | ||
|
|
d9337ff188 | ||
|
|
c16922c228 | ||
|
|
bbade7e50c | ||
|
|
193bf092d5 | ||
|
|
308b6ddbd7 | ||
|
|
c850b78da2 | ||
|
|
0fe9557eb8 | ||
|
|
b6953d73f7 | ||
|
|
8a89a9c502 | ||
|
|
d5f17bfdf1 | ||
|
|
9d0bb6e1f4 | ||
|
|
2ec37d81d9 | ||
|
|
e50bff8b5e | ||
|
|
e898181834 | ||
|
|
a0f877a23e | ||
|
|
a805490930 | ||
|
|
887a122358 | ||
|
|
1f48e8aea7 | ||
|
|
b2c1a3b068 | ||
|
|
df1566f394 | ||
|
|
0a3b122316 | ||
|
|
d497613cc4 | ||
|
|
91494f5832 | ||
|
|
c61d30a607 | ||
|
|
9428a07850 | ||
|
|
28603d3ab3 | ||
|
|
f863b881a9 | ||
|
|
083ab9f3e9 | ||
|
|
5d52c368a1 | ||
|
|
d72f379ab1 | ||
|
|
2931248fdb | ||
|
|
7166a5b1bb | ||
|
|
00c884cda0 | ||
|
|
1c65b71e35 | ||
|
|
3e4bc31a9d | ||
|
|
bc4bbd9a40 | ||
|
|
a6666ef384 | ||
|
|
92588c9adc | ||
|
|
832df26efc | ||
|
|
f7869cb241 | ||
|
|
27c0124682 | ||
|
|
6c69579c41 | ||
|
|
145b534c5d | ||
|
|
ce017a0e40 | ||
|
|
72a6f77fd2 | ||
|
|
275b15ad62 | ||
|
|
06c9a8b3d9 | ||
|
|
36b1406738 | ||
|
|
06b13a9cc5 | ||
|
|
2a3cd32447 | ||
|
|
40dc31226b | ||
|
|
def9e1a24e | ||
|
|
da25b28f7f | ||
|
|
10abbd0452 | ||
|
|
8a7cc69a69 | ||
|
|
c188f3367d | ||
|
|
26d85a7760 | ||
|
|
6eb845b46b | ||
|
|
8c20ddb440 | ||
|
|
a92f27d80c | ||
|
|
f523a161b6 | ||
|
|
ad2273b5ba | ||
|
|
171939e66b | ||
|
|
18bfbbaac1 | ||
|
|
e79f493cb6 | ||
|
|
e84c0fe886 | ||
|
|
bbfeb55af9 | ||
|
|
e7446e7e23 | ||
|
|
adacaf65fa | ||
|
|
7c761b2829 | ||
|
|
cc8532ff99 | ||
|
|
ede281a42d | ||
|
|
d34279d98f | ||
|
|
efd93c5e52 | ||
|
|
8d338b5f72 | ||
|
|
f59c81da31 | ||
|
|
cf6d44601e | ||
|
|
89b43060df | ||
|
|
7194d52000 | ||
|
|
a8ea0645e4 | ||
|
|
713173b0e4 | ||
|
|
6ed13bcec1 | ||
|
|
02519820d7 | ||
|
|
2604101eb2 | ||
|
|
1cb16f0c74 | ||
|
|
752cf2e93a | ||
|
|
f4ea7b6ea0 | ||
|
|
a6405989e9 | ||
|
|
fb8a81fd0f | ||
|
|
3a5ead3c2c | ||
|
|
def4b64cd3 | ||
|
|
a191b845d8 | ||
|
|
32a57d791a | ||
|
|
2a7e51ef4c | ||
|
|
f726fb99e5 | ||
|
|
9c19ae1f9b | ||
|
|
6ece2d0284 | ||
|
|
aec424b007 | ||
|
|
ff80b91311 | ||
|
|
52252c16f7 | ||
|
|
19cf1e9302 | ||
|
|
ddb8e8106b | ||
|
|
9af5ff7b12 | ||
|
|
fff6153f15 | ||
|
|
1ecac7adf4 | ||
|
|
408818904a | ||
|
|
b96637e6d9 | ||
|
|
8bdaca777f | ||
|
|
d278687e75 | ||
|
|
3ea8edb824 | ||
|
|
e33302050b | ||
|
|
8fefb59802 | ||
|
|
872aec1c10 | ||
|
|
8814bd4cda | ||
|
|
72a17cd9c2 | ||
|
|
8d79b69748 | ||
|
|
47760a8426 | ||
|
|
e6fc641a29 | ||
|
|
90b85d2be7 | ||
|
|
ee9575f710 | ||
|
|
7d97e6821c | ||
|
|
42fc9d8fae | ||
|
|
0f03fc0805 | ||
|
|
6d4a7cd1b1 | ||
|
|
f69d445ef7 | ||
|
|
d358a0a618 | ||
|
|
47841e49b1 | ||
|
|
57c7f0f856 |
@@ -14,4 +14,3 @@ dist-pkg
|
|||||||
pkg/harvester/index.ts
|
pkg/harvester/index.ts
|
||||||
pkg/harvester/store/*
|
pkg/harvester/store/*
|
||||||
scripts/pkgfile.js
|
scripts/pkgfile.js
|
||||||
tmp
|
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
addAssignees: author
|
|
||||||
addReviewers: true
|
|
||||||
numberOfReviewers: 0
|
|
||||||
reviewers:
|
|
||||||
- a110605
|
|
||||||
- houhoucoop
|
|
||||||
+17
-12
@@ -1,14 +1,19 @@
|
|||||||
pull_request_rules:
|
pull_request_rules:
|
||||||
- name: Ask to resolve conflict
|
- name: Automatically open backport PR to release-harvester-v1.5
|
||||||
conditions:
|
conditions:
|
||||||
- conflict
|
- base=main
|
||||||
actions:
|
actions:
|
||||||
comment:
|
backport:
|
||||||
message: This pull request is now in conflict. Could you fix it @{{author}}? 🙏
|
branches:
|
||||||
|
- "release-harvester-v1.5"
|
||||||
|
assignees:
|
||||||
|
- "{{ author }}"
|
||||||
|
labels:
|
||||||
|
- "v1.5 backport PR"
|
||||||
|
|
||||||
commands_restrictions:
|
- name: Ask to resolve conflict
|
||||||
backport:
|
conditions:
|
||||||
conditions:
|
- conflict
|
||||||
- or:
|
actions:
|
||||||
- sender-permission>=write
|
comment:
|
||||||
- sender=github-actions[bot]
|
message: This pull request is now in conflict. Could you fix it @{{author}}? 🙏
|
||||||
@@ -2,13 +2,18 @@
|
|||||||
### Summary
|
### Summary
|
||||||
|
|
||||||
### PR Checklists
|
### PR Checklists
|
||||||
- Are backend engineers aware of UI changes ?
|
- Do we need to backport this PR change to the [Harvester Dashboard](https://github.com/harvester/dashboard)?
|
||||||
|
- [ ] Yes, the relevant PR is at:
|
||||||
|
- Are backend engineers aware of UI changes?
|
||||||
- [ ] Yes, the backend owner is:
|
- [ ] 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 or video
|
### Test screenshot/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 -->
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
# .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
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"$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"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
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"
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
name: "[PR Management] Request Backport via Mergify"
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
types: [closed]
|
|
||||||
branches: [main]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
comment-backport:
|
|
||||||
if: github.event.pull_request.merged == true
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: ${{ github.base_ref }}
|
|
||||||
|
|
||||||
- name: Post Mergify backport command
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
pr_number=${{ github.event.pull_request.number }}
|
|
||||||
repo="${{ github.repository }}"
|
|
||||||
|
|
||||||
labels_json='${{ toJson(github.event.pull_request.labels.*.name) }}'
|
|
||||||
labels=$(echo "$labels_json" | jq -r '.[] // empty')
|
|
||||||
|
|
||||||
echo "Labels on PR: $labels"
|
|
||||||
|
|
||||||
matches=$(echo "$labels" | grep -oE '^require backport/v[0-9]+\.[0-9]+$' || true)
|
|
||||||
|
|
||||||
if [[ -z "$matches" ]]; then
|
|
||||||
echo "No back‑port labels found — skipping."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
branches=$(echo "$matches" \
|
|
||||||
| sed -E 's/^require backport\/v/release-harvester-v/' \
|
|
||||||
| sort -u | tr '\n' ' ')
|
|
||||||
branches=${branches%% }
|
|
||||||
|
|
||||||
cmd="@Mergifyio backport $branches"
|
|
||||||
echo "Posting Mergify command: $cmd"
|
|
||||||
gh pr comment "$pr_number" --repo "$repo" --body "$cmd"
|
|
||||||
@@ -11,19 +11,18 @@ defaults:
|
|||||||
working-directory: ./
|
working-directory: ./
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-version:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@v4
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Check package version
|
- name: Run tests
|
||||||
env:
|
uses: ./.github/actions/lint
|
||||||
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: check-version
|
needs: lint
|
||||||
uses: ./.github/workflows/build-extension-catalog.yml
|
uses: ./.github/workflows/build-extension-catalog.yml
|
||||||
permissions:
|
permissions:
|
||||||
actions: write
|
actions: write
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- 'release-harvester-v*'
|
- 'release-harvester-v*'
|
||||||
- 'main'
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
setup-target-branch:
|
setup-target-branch:
|
||||||
@@ -18,17 +17,14 @@ jobs:
|
|||||||
- name: Determine target branch
|
- name: Determine target branch
|
||||||
id: get-version
|
id: get-version
|
||||||
run: |
|
run: |
|
||||||
if [[ "${{ github.ref_name }}" == "main" ]]; then
|
if [[ "${{ github.ref_name }}" =~ ^release-harvester-v([0-9]+\.[0-9]+)$ ]]; 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
|
||||||
@@ -132,7 +128,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.7.0
|
uses: helm/chart-releaser-action@v1.4.1
|
||||||
with:
|
with:
|
||||||
charts_dir: ./charts
|
charts_dir: ./charts
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -22,27 +22,26 @@ jobs:
|
|||||||
- name: Determine release tag
|
- name: Determine release tag
|
||||||
id: determine_tag
|
id: determine_tag
|
||||||
run: |
|
run: |
|
||||||
if [[ "${{ github.event.release.tag_name }}" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
|
if [[ "${{ github.ref_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
|
||||||
check-version:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@v4
|
with:
|
||||||
- name: Check package version
|
fetch-depth: 1
|
||||||
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
|
||||||
- check-version
|
- lint
|
||||||
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,20 +5,14 @@ on:
|
|||||||
- v[1-9].*
|
- v[1-9].*
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-version:
|
build-validation:
|
||||||
runs-on: ubuntu-latest
|
name: Build Test
|
||||||
steps:
|
uses: ./.github/workflows/run-lint.yaml
|
||||||
- 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: check-version
|
needs:
|
||||||
|
- build-validation
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
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,7 +7,6 @@ on:
|
|||||||
- main
|
- main
|
||||||
- 'release-*'
|
- 'release-*'
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, reopened, edited, synchronize]
|
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- 'release-*'
|
- 'release-*'
|
||||||
@@ -18,56 +17,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # Need full history for commit-lint
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
uses: ./.github/actions/lint
|
uses: ./.github/actions/lint
|
||||||
|
|
||||||
- name: Validate PR title lint
|
|
||||||
if: github.event_name == 'pull_request'
|
|
||||||
shell: bash
|
|
||||||
run: echo "${{ github.event.pull_request.title }}" | npx commitlint
|
|
||||||
|
|
||||||
- name: Validate commit messages
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
echo "GITHUB_EVENT_NAME=$GITHUB_EVENT_NAME"
|
|
||||||
echo "GITHUB_BASE_SHA=$GITHUB_BASE_SHA"
|
|
||||||
echo "GITHUB_HEAD_SHA=$GITHUB_HEAD_SHA"
|
|
||||||
echo "GITHUB_BEFORE=$GITHUB_BEFORE"
|
|
||||||
echo "GITHUB_AFTER=$GITHUB_AFTER"
|
|
||||||
|
|
||||||
if [ "$GITHUB_EVENT_NAME" = "pull_request" ] && [ -n "$GITHUB_BASE_SHA" ] && [ -n "$GITHUB_HEAD_SHA" ]; then
|
|
||||||
FROM="$GITHUB_BASE_SHA"
|
|
||||||
TO="$GITHUB_HEAD_SHA"
|
|
||||||
elif [ -n "$GITHUB_BEFORE" ] && [ -n "$GITHUB_AFTER" ]; then
|
|
||||||
if [ "$GITHUB_BEFORE" = "0000000000000000000000000000000000000000" ]; then
|
|
||||||
# first push to HEAD
|
|
||||||
FROM=""
|
|
||||||
TO="$GITHUB_AFTER"
|
|
||||||
else
|
|
||||||
FROM="$GITHUB_BEFORE"
|
|
||||||
TO="$GITHUB_AFTER"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "No valid commit range found, skipping commitlint."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "FROM=$FROM"
|
|
||||||
echo "TO=$TO"
|
|
||||||
|
|
||||||
if [ -z "$FROM" ]; then
|
|
||||||
echo "Linting last commit $TO"
|
|
||||||
npx commitlint --last --verbose
|
|
||||||
|
|
||||||
else
|
|
||||||
echo "Linting commits from $FROM to $TO"
|
|
||||||
npx commitlint --from "$FROM" --to "$TO" --verbose
|
|
||||||
fi
|
|
||||||
env:
|
|
||||||
GITHUB_EVENT_NAME: ${{ github.event_name }}
|
|
||||||
GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
|
||||||
GITHUB_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
||||||
GITHUB_BEFORE: ${{ github.event.before }}
|
|
||||||
GITHUB_AFTER: ${{ github.event.after }}
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
npx --no -- commitlint --edit $1
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
yarn lint
|
|
||||||
@@ -1,168 +1,62 @@
|
|||||||
# harvester-ui-extension
|
# harvester-ui-extension
|
||||||
|
|
||||||
The Harvester UI Extension is a Rancher extension that provides the user interface for [Harvester](https://harvesterhci.io) within the [Rancher Dashboard](https://github.com/rancher/dashboard).
|
Rancher Extension used in [rancher/dashboard](https://github.com/rancher/dashboard) for [Harvester](https://harvesterhci.io) Dashboard UI.
|
||||||
|
|
||||||
> **Note:**
|
`
|
||||||
> This extension is available starting from **Rancher 2.10.0**. Ensure your Rancher version is **2.10.0 or later** to access Harvester integration.
|
From Rancher 2.10.0, Harvester integration is provided by this new Harvester UI Extension.
|
||||||
|
`
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
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).
|
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.
|
||||||
|
|
||||||
|
## Running for Development
|
||||||
|
|
||||||
## Development Setup
|
This is what you probably want to get started.
|
||||||
|
|
||||||
|
### Prerequisite
|
||||||
|
|
||||||
|
Nodejs version >= 20
|
||||||
|
|
||||||
Ensure **Node.js v20 or later** is installed for development and debugging.
|
|
||||||
|
|
||||||
### Standalone Mode
|
### Standalone Mode
|
||||||
|
|
||||||
Run the extension standalone with hot reload at `https://localhost:8005`.
|
For development, serve with hot reload at https://localhost:8005
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
yarn install
|
yarn install
|
||||||
|
|
||||||
# Start the development server
|
# Use RANCHER_ENV to specify the endpoint for your Harvester API
|
||||||
RANCHER_ENV=harvester API=https://your-harvester-ip yarn dev
|
RANCHER_ENV=harvester API=https://your-harvester-ip yarn dev
|
||||||
|
|
||||||
# Example with specific server version
|
# Use VUE_APP_SERVER_VERSION to specify the Harvester server version
|
||||||
RANCHER_ENV=harvester VUE_APP_SERVER_VERSION=v1.5.0 API=https://192.168.1.123 yarn dev
|
# in case your are running a dev build of Harvester
|
||||||
```
|
RANCHER_ENV=harvester VUE_APP_SERVER_VERSION=your-harvester-server-version API=https://your-harvester-ip yarn dev
|
||||||
|
|
||||||
You may also define environment variables in a `.env` file:
|
# or put your env variables into a .env file
|
||||||
|
|
||||||
```env
|
# Example
|
||||||
RANCHER_ENV=harvester
|
RANCHER_ENV=harvester VUE_APP_SERVER_VERSION=v1.4.0 API=https://192.168.1.123 yarn dev
|
||||||
VUE_APP_SERVER_VERSION=v1.5.0
|
|
||||||
API=https://192.168.1.123
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Rancher Integration Mode
|
### Rancher Integration Mode
|
||||||
|
|
||||||
To run as a Rancher extension, follow the [Rancher UI Extension Guide](https://extensions.rancher.io/extensions/next/extensions-getting-started#running-the-app).
|
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).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
API=https://your-rancher-ip yarn dev
|
API=https://your-rancher-ip yarn dev
|
||||||
|
|
||||||
|
# Example
|
||||||
|
API=https://192.168.1.123 yarn dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## Commit Message Guidelines
|
|
||||||
|
|
||||||
This project uses [commit-lint](https://commitlint.js.org/) with [Conventional Commits](https://www.conventionalcommits.org/) to ensure consistent and meaningful commit messages.
|
|
||||||
|
|
||||||
### Commit Message Format
|
|
||||||
|
|
||||||
All commit messages must follow the conventional commit format:
|
|
||||||
|
|
||||||
```
|
|
||||||
<type>[optional scope]: <description>
|
|
||||||
|
|
||||||
[optional body]
|
|
||||||
|
|
||||||
[optional footer(s)]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Supported Types
|
|
||||||
|
|
||||||
- **feat**: New features
|
|
||||||
- **fix**: Bug fixes
|
|
||||||
- **docs**: Documentation changes
|
|
||||||
- **style**: Code style changes (formatting, missing semicolons, etc.)
|
|
||||||
- **refactor**: Code refactoring
|
|
||||||
- **perf**: Performance improvements
|
|
||||||
- **test**: Adding or updating tests
|
|
||||||
- **build**: Build system or external dependencies
|
|
||||||
- **ci**: CI/CD changes
|
|
||||||
- **chore**: Other changes that don't modify src or test files
|
|
||||||
- **revert**: Reverts a previous commit
|
|
||||||
- **wip**: Work in progress
|
|
||||||
- **deps**: Dependency updates
|
|
||||||
- **security**: Security fixes
|
|
||||||
|
|
||||||
### Examples
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Feature
|
|
||||||
git commit -m "feat: add new virtual machine creation wizard"
|
|
||||||
|
|
||||||
# Bug fix
|
|
||||||
git commit -m "fix: resolve memory leak in VM console"
|
|
||||||
|
|
||||||
# Documentation
|
|
||||||
git commit -m "docs: update installation instructions"
|
|
||||||
|
|
||||||
# Breaking change
|
|
||||||
git commit -m "feat!: change API endpoint structure
|
|
||||||
|
|
||||||
BREAKING CHANGE: The /api/v1/vms endpoint has been replaced with /api/v2/vms"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Git Hooks
|
|
||||||
|
|
||||||
The project uses [Husky](https://typicode.github.io/husky/) to automatically validate commit messages and run linting before commits:
|
|
||||||
|
|
||||||
- **pre-commit**: Runs ESLint to ensure code quality
|
|
||||||
- **commit-msg**: Validates commit message format using commit-lint
|
|
||||||
|
|
||||||
These hooks are automatically installed when you run `yarn install`.
|
|
||||||
|
|
||||||
### Manual Validation
|
|
||||||
|
|
||||||
You can manually validate commit messages:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Validate the last commit
|
|
||||||
yarn commitlint
|
|
||||||
|
|
||||||
# Validate a specific commit
|
|
||||||
npx commitlint --from <commit-hash>
|
|
||||||
|
|
||||||
# Validate a range of commits
|
|
||||||
npx commitlint --from <start-hash> --to <end-hash>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Branch Structure
|
|
||||||
|
|
||||||
- **`main`** – Main development branch
|
|
||||||
- **`release-harvester-vX.Y`** – Stable release branches per version series
|
|
||||||
- **`vX.Y-head`** – Testing branches for ongoing changes to extension builds in each release series
|
|
||||||
|
|
||||||
> **Note:**
|
|
||||||
> The `vX.Y-head` branches are auto-generated and kept in sync with release branches. Use these for testing the latest changes in each version series.
|
|
||||||
|
|
||||||
## Testing Guidelines
|
|
||||||
|
|
||||||
### UI Extension Testing
|
|
||||||
|
|
||||||
To validate changes in a release series, switch to the appropriate `vX.Y-head` branch. For main branch testing, use `main-head`.
|
|
||||||
|
|
||||||
- Examples:
|
|
||||||
- Test `1.0.x` series → `v1.0-head`
|
|
||||||
- Test `1.5.x` series → `v1.5-head`
|
|
||||||
|
|
||||||
**Steps:**
|
|
||||||
1. Navigate to **Rancher UI** → **Local** → **App** → **Repositories**
|
|
||||||
2. Refresh the Harvester repository using the target `vX.Y-head` branch
|
|
||||||
3. Go to the **Extensions** page and install the desired version
|
|
||||||
|
|
||||||
### Standalone Mode Testing
|
|
||||||
|
|
||||||
To test the standalone UI, configure Harvester to load the UI from an external source.
|
|
||||||
|
|
||||||
- Examples of `ui-index`:
|
|
||||||
- Main branch → `https://releases.rancher.com/harvester-ui/dashboard/latest/index.html`
|
|
||||||
- Release series `1.5.x` → `https://releases.rancher.com/harvester-ui/dashboard/release-harvester-v1.5/index.html`
|
|
||||||
|
|
||||||
**Steps:**
|
|
||||||
1. Go to **Harvester UI** → **Advanced** → **Settings** → **UI**
|
|
||||||
2. Set **ui-source** to `External`
|
|
||||||
3. Set **ui-index** to the desired URL
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
If you want to contribute, start by reading this document, then visit our [Getting Started guide](https://extensions.rancher.io/extensions/next/extensions-getting-started) to learn how to develop and submit changes.
|
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.
|
||||||
|
|
||||||
## 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");
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
extends: ['@commitlint/config-conventional'],
|
|
||||||
rules: {
|
|
||||||
// Enforce conventional commit format
|
|
||||||
'type-enum': [
|
|
||||||
2,
|
|
||||||
'always',
|
|
||||||
[
|
|
||||||
'feat', // New features
|
|
||||||
'fix', // Bug fixes
|
|
||||||
'docs', // Documentation changes
|
|
||||||
'style', // Code style changes (formatting, missing semicolons, etc.)
|
|
||||||
'refactor', // Code refactoring
|
|
||||||
'perf', // Performance improvements
|
|
||||||
'test', // Adding or updating tests
|
|
||||||
'build', // Build system or external dependencies
|
|
||||||
'ci', // CI/CD changes
|
|
||||||
'chore', // Other changes that don't modify src or test files
|
|
||||||
'revert', // Reverts a previous commit
|
|
||||||
'wip', // Work in progress
|
|
||||||
'deps', // Dependency updates
|
|
||||||
'security', // Security fixes
|
|
||||||
]
|
|
||||||
],
|
|
||||||
// Allows the scope to be empty. [0] means the rule is disabled.
|
|
||||||
'scope-empty': [0, 'always'],
|
|
||||||
// Allows any string for the scope.
|
|
||||||
'scope-case': [2, 'always', 'kebab-case'],
|
|
||||||
'type-case': [2, 'always', 'lower-case'],
|
|
||||||
'type-empty': [2, 'never'],
|
|
||||||
'subject-case': [0, 'never'],
|
|
||||||
'subject-empty': [2, 'never'],
|
|
||||||
'subject-full-stop': [2, 'never', '.'],
|
|
||||||
'subject-max-length': [0, 'never'],
|
|
||||||
'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'),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
+16
-22
@@ -1,36 +1,36 @@
|
|||||||
{
|
{
|
||||||
"name": "harvester-ui-extension",
|
"name": "harvester-ui-extension",
|
||||||
"version": "1.7.0-rc7",
|
"version": "1.5.0-rc4",
|
||||||
"private": false,
|
"private": false,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/plugin-transform-class-static-block": "7.28.3",
|
"@rancher/shell": "3.0.2-rc.6",
|
||||||
"@rancher/shell": "3.0.8-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.19.0",
|
"@types/node": "~20.10.0",
|
||||||
"cronstrue": "2.59.0",
|
"cronstrue": "2.50.0",
|
||||||
"d3-color": "3.1.0",
|
"d3-color": "3.1.0",
|
||||||
"ejs": "3.1.10",
|
"ejs": "3.1.9",
|
||||||
"follow-redirects": "1.15.11",
|
"follow-redirects": "1.15.2",
|
||||||
"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.21",
|
"@types/lodash": "4.17.5",
|
||||||
"merge": "2.1.1",
|
"merge": "2.1.1",
|
||||||
"node-forge": "1.3.3",
|
"node-forge": "1.3.1",
|
||||||
"nth-check": "2.1.1",
|
"nth-check": "2.1.1",
|
||||||
"qs": "6.14.0",
|
"qs": "6.11.1",
|
||||||
"roarr": "7.21.2",
|
"roarr": "7.0.4",
|
||||||
"semver": "7.7.3",
|
"semver": "7.5.4",
|
||||||
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
|
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -41,16 +41,10 @@
|
|||||||
"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,16 +1,14 @@
|
|||||||
<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_SINGLE_CLUSTER_ALLOWED_SETTING, HCI_SETTING } from '../config/settings';
|
import { HCI_ALLOWED_SETTINGS, HCI_SETTING } from '../config/settings';
|
||||||
import { DOC } from '../config/doc-links';
|
|
||||||
import { docLink } from '../utils/feature-flags';
|
|
||||||
|
|
||||||
const CATEGORY = {
|
const CATEGORY = {
|
||||||
ui: [
|
ui: [
|
||||||
'branding',
|
'branding',
|
||||||
'ui-source',
|
'ui-source',
|
||||||
|
'ui-plugin-index',
|
||||||
'ui-index',
|
'ui-index',
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
@@ -21,7 +19,6 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
AsyncButton,
|
AsyncButton,
|
||||||
Banner,
|
Banner,
|
||||||
ActionMenu
|
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
@@ -33,23 +30,15 @@ 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)
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -59,85 +48,12 @@ 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)) {
|
||||||
@@ -158,21 +74,25 @@ 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) {
|
||||||
const setting = this.filteredSettings.find((setting) => setting.id === s.id);
|
this.categorySettings.find((setting) => {
|
||||||
|
if (setting.id === s.id) {
|
||||||
if (setting) {
|
setting.hide = !setting.hide;
|
||||||
setting.hide = !setting.hide;
|
}
|
||||||
this.originalHideMap[setting.id] = setting.hide;
|
});
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async testConnect(buttonDone, value) {
|
async testConnect(buttonDone, value) {
|
||||||
@@ -198,19 +118,6 @@ 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 {};
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -219,7 +126,7 @@ export default {
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
v-for="(setting, i) in filteredSettings"
|
v-for="(setting, i) in categorySettings"
|
||||||
:key="i"
|
:key="i"
|
||||||
class="advanced-setting mb-20"
|
class="advanced-setting mb-20"
|
||||||
>
|
>
|
||||||
@@ -241,7 +148,7 @@ export default {
|
|||||||
Experimental
|
Experimental
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<h2 v-clean-html="t(setting.description, getDocLinkParams(setting) || {}, true)">
|
<h2 v-clean-html="t(setting.description, {}, true)">
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -249,12 +156,15 @@ export default {
|
|||||||
:id="setting.id"
|
:id="setting.id"
|
||||||
class="action"
|
class="action"
|
||||||
>
|
>
|
||||||
<ActionMenu
|
<button
|
||||||
:resource="setting.data"
|
aria-haspopup="true"
|
||||||
:button-aria-label="t('advancedSettings.edit.label')"
|
aria-expanded="false"
|
||||||
data-testid="action-button"
|
type="button"
|
||||||
button-role="tertiary"
|
class="btn btn-sm role-multi-action actions"
|
||||||
/>
|
@click="showActionMenu($event, setting)"
|
||||||
|
>
|
||||||
|
<i class="icon icon-actions" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div value>
|
<div value>
|
||||||
@@ -311,12 +221,6 @@ 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>
|
||||||
|
|
||||||
@@ -367,8 +271,4 @@ 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>
|
<p class="mb-5">
|
||||||
{{ t('harvester.upgradePage.upgradeInfo.moreNotes') }} <a
|
{{ t('harvester.upgradePage.upgradeInfo.moreNotes') }} <a
|
||||||
:href="releaseVersion"
|
:href="releaseVersion"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|||||||
@@ -257,12 +257,8 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
reconnect() {
|
|
||||||
this.$refs.novncConsole.reconnect();
|
|
||||||
},
|
|
||||||
|
|
||||||
softReboot() {
|
softReboot() {
|
||||||
this.vmResource.doSoftReboot();
|
this.vmResource.softrebootVM();
|
||||||
},
|
},
|
||||||
|
|
||||||
showKeysModal() {
|
showKeysModal() {
|
||||||
@@ -310,13 +306,6 @@ export default {
|
|||||||
{{ t("harvester.action.softreboot") }}
|
{{ t("harvester.action.softreboot") }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
|
||||||
class="btn btn-sm bg-primary"
|
|
||||||
@click="reconnect"
|
|
||||||
>
|
|
||||||
{{ t("harvester.action.reconnect") }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<v-dropdown
|
<v-dropdown
|
||||||
v-if="!hideCustomKeysBar"
|
v-if="!hideCustomKeysBar"
|
||||||
ref="customKeyPopover"
|
ref="customKeyPopover"
|
||||||
@@ -383,15 +372,3 @@ export default {
|
|||||||
background: rgb(40, 40, 40);
|
background: rgb(40, 40, 40);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.vm-console .v-popper__arrow-container {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.vm-console .v-popper__popper{
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
.vm-console .v-popper__inner{
|
|
||||||
overflow-y: visible;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,294 +0,0 @@
|
|||||||
<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,5 +1,6 @@
|
|||||||
<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';
|
||||||
|
|
||||||
@@ -61,7 +62,7 @@ export default {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class="row"
|
class="row"
|
||||||
@input="update"
|
@update:value="update"
|
||||||
>
|
>
|
||||||
<div class="col span-12">
|
<div class="col span-12">
|
||||||
<LabeledInput
|
<LabeledInput
|
||||||
|
|||||||
@@ -1,154 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
||||||
import { RadioGroup } from '@components/Form/Radio';
|
|
||||||
import UnitInput from '@shell/components/form/UnitInput';
|
|
||||||
import { MEBIBYTE } from '../../utils/unit';
|
|
||||||
import { Banner } from '@components/Banner';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'KubevirtMigration',
|
|
||||||
|
|
||||||
components: {
|
|
||||||
LabeledInput,
|
|
||||||
UnitInput,
|
|
||||||
RadioGroup,
|
|
||||||
Banner
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
|
||||||
registerBeforeHook: {
|
|
||||||
type: Function,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
value: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({
|
|
||||||
value: '',
|
|
||||||
default: '{}'
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
const migration = this.parseJSON(this.value?.value) || this.parseJSON(this.value?.default) || {};
|
|
||||||
|
|
||||||
return {
|
|
||||||
MEBIBYTE,
|
|
||||||
migration,
|
|
||||||
parseError: null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
this.registerBeforeHook?.(this.willSave, 'willSave');
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
parseJSON(string) {
|
|
||||||
try {
|
|
||||||
return JSON.parse(string);
|
|
||||||
} catch (e) {
|
|
||||||
this.parseError = this.t('kubevirtMigration.parseError', { error: e.message });
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.error('Failed to parse JSON:', e.message);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
updateValue() {
|
|
||||||
if (this.value) {
|
|
||||||
this.value.value = JSON.stringify(this.migration);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
useDefault() {
|
|
||||||
if (this.value?.default) {
|
|
||||||
const defaultMigration = this.parseJSON(this.value.default) || {};
|
|
||||||
|
|
||||||
this.migration = defaultMigration;
|
|
||||||
this.updateValue();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async willSave() {
|
|
||||||
this.updateValue();
|
|
||||||
|
|
||||||
return Promise.resolve();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<Banner
|
|
||||||
v-if="parseError"
|
|
||||||
color="error"
|
|
||||||
>
|
|
||||||
{{ parseError }}
|
|
||||||
</Banner>
|
|
||||||
|
|
||||||
<div class="migration-field">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value.number="migration.parallelMigrationsPerCluster"
|
|
||||||
:label="t('harvester.setting.kubevirtMigration.parallelMigrationsPerCluster')"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
/>
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value.number="migration.parallelOutboundMigrationsPerNode"
|
|
||||||
:label="t('harvester.setting.kubevirtMigration.parallelOutboundMigrationsPerNode')"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
/>
|
|
||||||
<UnitInput
|
|
||||||
v-model:value="migration.bandwidthPerMigration"
|
|
||||||
min="0"
|
|
||||||
:label="t('harvester.setting.kubevirtMigration.bandwidthPerMigration')"
|
|
||||||
:mode="mode"
|
|
||||||
:suffix="MEBIBYTE"
|
|
||||||
:tooltip="t('harvester.setting.kubevirtMigration.bandwidthPerMigrationTooltip', _, true)"
|
|
||||||
/>
|
|
||||||
<UnitInput
|
|
||||||
v-model:value="migration.completionTimeoutPerGiB"
|
|
||||||
:label="t('harvester.setting.kubevirtMigration.completionTimeoutPerGiB')"
|
|
||||||
:mode="mode"
|
|
||||||
:suffix="migration.completionTimeoutPerGiB === 1 ? 'Second' : 'Seconds'"
|
|
||||||
min="10"
|
|
||||||
/>
|
|
||||||
<UnitInput
|
|
||||||
v-model:value="migration.progressTimeout"
|
|
||||||
:label="t('harvester.setting.kubevirtMigration.progressTimeout')"
|
|
||||||
:mode="mode"
|
|
||||||
:suffix="migration.progressTimeout === 1 ? 'Second' : 'Seconds'"
|
|
||||||
min="10"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
v-for="field in ['allowAutoConverge','allowPostCopy','unsafeMigrationOverride','allowWorkloadDisruption','disableTLS','matchSELinuxLevelOnMigration']"
|
|
||||||
:key="field"
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
class="mb-5"
|
|
||||||
:for="field"
|
|
||||||
>{{ t(`harvester.setting.kubevirtMigration.${field}`) }}</label>
|
|
||||||
<RadioGroup
|
|
||||||
:id="field"
|
|
||||||
v-model:value="migration[field]"
|
|
||||||
:options="[
|
|
||||||
{ label: t('advancedSettings.edit.trueOption'), value: true },
|
|
||||||
{ label: t('advancedSettings.edit.falseOption'), value: false },
|
|
||||||
]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.migration-field {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,222 +0,0 @@
|
|||||||
<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,9 +3,6 @@ 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',
|
||||||
|
|
||||||
@@ -14,92 +11,47 @@ export default {
|
|||||||
mixins: [CreateEditView],
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
async fetch() {
|
async fetch() {
|
||||||
this.loading = true;
|
|
||||||
|
|
||||||
await this.$store.dispatch('harvester/findAll', { type: NAMESPACE });
|
await this.$store.dispatch('harvester/findAll', { type: NAMESPACE });
|
||||||
|
|
||||||
if (this.customSupportBundleFeatureEnabled) {
|
|
||||||
try {
|
|
||||||
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/namespaces?link=supportbundle');
|
|
||||||
const response = await this.$store.dispatch('harvester/request', { url });
|
|
||||||
|
|
||||||
this.defaultNamespaces = response.data || [];
|
|
||||||
} catch (error) {
|
|
||||||
this.defaultNamespaces = [];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.defaultNamespaces = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loading = false;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
let namespaces = [];
|
||||||
const namespacesStr = this.value?.value || this.value?.default || '';
|
const namespacesStr = this.value?.value || this.value?.default || '';
|
||||||
const namespaces = namespacesStr ? namespacesStr.split(',') : [];
|
|
||||||
|
|
||||||
return {
|
if (namespacesStr) {
|
||||||
namespaces,
|
namespaces = namespacesStr.split(',');
|
||||||
defaultNamespaces: [],
|
}
|
||||||
loading: true
|
|
||||||
};
|
return { namespaces };
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
customSupportBundleFeatureEnabled() {
|
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('customSupportBundle');
|
|
||||||
},
|
|
||||||
|
|
||||||
allNamespaces() {
|
|
||||||
return this.$store.getters['harvester/all'](NAMESPACE).map((ns) => ns.id);
|
|
||||||
},
|
|
||||||
|
|
||||||
filteredNamespaces() {
|
|
||||||
if (!this.customSupportBundleFeatureEnabled) {
|
|
||||||
return this.allNamespaces;
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultIds = this.defaultNamespaces.map((ns) => ns.id);
|
|
||||||
|
|
||||||
return this.allNamespaces.filter((ns) => !defaultIds.includes(ns));
|
|
||||||
},
|
|
||||||
|
|
||||||
namespaceOptions() {
|
namespaceOptions() {
|
||||||
const mappedNamespaces = this.filteredNamespaces.map((ns) => ({ label: ns, value: ns }));
|
return this.$store.getters['harvester/all'](NAMESPACE).map((N) => {
|
||||||
|
return {
|
||||||
if (!this.customSupportBundleFeatureEnabled) {
|
label: N.id,
|
||||||
return mappedNamespaces;
|
value: N.id
|
||||||
}
|
};
|
||||||
|
});
|
||||||
const allSelected =
|
|
||||||
this.namespaces.length === this.filteredNamespaces.length &&
|
|
||||||
this.filteredNamespaces.every((ns) => this.namespaces.includes(ns));
|
|
||||||
|
|
||||||
const controlOption = allSelected ? { label: this.t('harvester.modal.bundle.namespaces.unselectAll'), value: UNSELECT_ALL } : { label: this.t('harvester.modal.bundle.namespaces.selectAll'), value: SELECT_ALL };
|
|
||||||
|
|
||||||
return [controlOption, ...mappedNamespaces];
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
update(selected) {
|
update() {
|
||||||
if (selected.includes(SELECT_ALL)) {
|
const namespaceStr = this.namespaces.join(',');
|
||||||
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 = this.namespaces.join(',');
|
this.value['value'] = namespaceStr;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
'value.value'(newVal) {
|
'value.value': {
|
||||||
const raw = newVal || this.value.default || '';
|
handler(neu) {
|
||||||
|
if (neu === this.value.default || !neu) {
|
||||||
this.namespaces = raw ? raw.split(',') : [];
|
this.namespaces = [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -110,7 +62,6 @@ 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"
|
||||||
|
|||||||
@@ -1,163 +0,0 @@
|
|||||||
<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')]"
|
||||||
@update:value="update"
|
@input="update"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LabeledInput
|
<LabeledInput
|
||||||
|
|||||||
@@ -1,270 +0,0 @@
|
|||||||
<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,13 +1,8 @@
|
|||||||
// 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`,
|
||||||
KSMTUNED_MODE: `/host/#ksmtuned-mode`,
|
STORAGE_NETWORK_EXAMPLE: `/advanced/storagenetwork#configuration-example`,
|
||||||
UPGRADE_URL: `/upgrade/index`,
|
KSMTUNED_MODE: `/host/#ksmtuned-mode`,
|
||||||
UPGRADE_CONFIG_URL: `/advanced/index#upgrade-config`,
|
UPGRADE_URL: `/upgrade/index`
|
||||||
STORAGE_NETWORK_EXAMPLE: `/advanced/storagenetwork#configuration-example`,
|
|
||||||
SUPPORT_BUNDLE_NAMESPACES: `/advanced/index/#support-bundle-namespaces`,
|
|
||||||
VPC_CONFIGURATION_EXAMPLES: `/networking/kubeovn-vpc#vpc-peering-configuration-examples`,
|
|
||||||
NETWORK_POLICY: `/networking/kubeovn-vm-isolation/#network-policies`,
|
|
||||||
TRANSPARENT_HUGEPAGES: `https://docs.kernel.org/admin-guide/mm/transhuge.html`,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,75 +1,69 @@
|
|||||||
import semver from 'semver';
|
// https://github.com/harvester/dashboard/releases/tag/v1.3.0
|
||||||
|
const featuresV130 = [
|
||||||
|
'supportHarvesterClusterVersion'
|
||||||
|
];
|
||||||
|
|
||||||
const FEATURE_FLAGS = {
|
// https://github.com/harvester/dashboard/releases/tag/v1.3.1
|
||||||
'v1.3.0': [
|
const featuresV131 = [
|
||||||
'supportHarvesterClusterVersion'
|
...featuresV130,
|
||||||
],
|
'autoRotateRke2CertsSetting',
|
||||||
'v1.3.1': [
|
'supportBundleNodeCollectionTimeoutSetting'
|
||||||
'autoRotateRke2CertsSetting',
|
];
|
||||||
'supportBundleNodeCollectionTimeoutSetting'
|
|
||||||
],
|
// https://github.com/harvester/dashboard/releases/tag/v1.3.2
|
||||||
'v1.3.2': [
|
const featuresV132 = [
|
||||||
'kubeconfigDefaultTokenTTLMinutesSetting',
|
...featuresV131,
|
||||||
'improveMaintenanceMode',
|
'kubeconfigDefaultTokenTTLMinutesSetting',
|
||||||
],
|
'improveMaintenanceMode',
|
||||||
'v1.3.3': [],
|
];
|
||||||
'v1.4.0': [
|
|
||||||
'cpuPinning',
|
// TODO: add v1.3.3 official release note
|
||||||
'usbPassthrough',
|
// https://github.com/harvester/dashboard/releases/tag/v1.3.3-dev-20250105
|
||||||
'volumeEncryption',
|
const featuresV133 = [
|
||||||
'schedulingVMBackup',
|
...featuresV132,
|
||||||
'vmSnapshotQuota',
|
];
|
||||||
'longhornV2LVMSupport',
|
|
||||||
'improveMaintenanceMode',
|
// https://github.com/harvester/dashboard/releases/tag/v1.4.0
|
||||||
'upgradeConfigSetting'
|
const featuresV140 = [
|
||||||
],
|
...featuresV133,
|
||||||
'v1.4.1': [],
|
'cpuPinning',
|
||||||
'v1.4.2': [
|
'usbPassthrough',
|
||||||
'refreshIntervalInSecond',
|
'volumeEncryption',
|
||||||
'allowEmptySnapshotClassName'
|
'schedulingVMBackup',
|
||||||
],
|
'vmSnapshotQuota',
|
||||||
'v1.4.3': [],
|
'longhornV2LVMSupport',
|
||||||
'v1.5.0': [
|
'improveMaintenanceMode',
|
||||||
'tpmPersistentState',
|
];
|
||||||
'efiPersistentState',
|
|
||||||
'untaggedNetworkSetting',
|
// https://github.com/harvester/dashboard/releases/tag/v1.4.1
|
||||||
'skipSingleReplicaDetachedVol',
|
const featuresV141 = [
|
||||||
'thirdPartyStorage',
|
...featuresV140
|
||||||
'liveMigrationProgress'
|
];
|
||||||
],
|
|
||||||
'v1.5.1': [],
|
// TODO: add v1.4.2 official release note
|
||||||
'v1.6.0': [
|
const featuresV142 = [
|
||||||
'customSupportBundle',
|
...featuresV141,
|
||||||
'csiOnlineExpandValidation',
|
'refreshIntervalInSecond',
|
||||||
'vmNetworkMigration',
|
'allowEmptySnapshotClassName'
|
||||||
'kubeovnVpcSubnet',
|
];
|
||||||
'rancherClusterSetting',
|
|
||||||
'cpuMemoryHotplug',
|
// TODO: add v1.5.0 official release note
|
||||||
'cdiSettings',
|
const featuresV150 = [
|
||||||
'vmCloneRunStrategy',
|
...featuresV142,
|
||||||
],
|
'tpmPersistentState',
|
||||||
'v1.6.1': [],
|
'efiPersistentState',
|
||||||
'v1.7.0': [
|
'untaggedNetworkSetting',
|
||||||
'vmMachineTypeAuto',
|
'skipSingleReplicaDetachedVol',
|
||||||
'lhV2VolExpansion',
|
'thirdPartyStorage'
|
||||||
'l2VlanTrunkMode',
|
];
|
||||||
'kubevirtMigration',
|
|
||||||
'hotplugNic'
|
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();
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
LOGGING,
|
LOGGING,
|
||||||
STORAGE_CLASS,
|
STORAGE_CLASS,
|
||||||
SECRET,
|
SECRET,
|
||||||
NETWORK_POLICY
|
|
||||||
} from '@shell/config/types';
|
} from '@shell/config/types';
|
||||||
import { HCI, VOLUME_SNAPSHOT } from '../types';
|
import { HCI, VOLUME_SNAPSHOT } from '../types';
|
||||||
import {
|
import {
|
||||||
@@ -25,7 +24,6 @@ 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 {
|
||||||
@@ -34,8 +32,7 @@ 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;
|
||||||
@@ -225,7 +222,6 @@ 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,
|
||||||
@@ -259,7 +255,7 @@ export function init($plugin, store) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (isSingleVirtualCluster) {
|
if (isSingleVirtualCluster) {
|
||||||
headers(NAMESPACE, [STATE, NAME_UNLINKED, DESCRIPTION, AGE]);
|
headers(NAMESPACE, [STATE, NAME_UNLINKED, AGE]);
|
||||||
basicType([NAMESPACE]);
|
basicType([NAMESPACE]);
|
||||||
virtualType({
|
virtualType({
|
||||||
labelKey: 'harvester.namespace.label',
|
labelKey: 'harvester.namespace.label',
|
||||||
@@ -425,8 +421,6 @@ export function init($plugin, store) {
|
|||||||
[
|
[
|
||||||
HCI.CLUSTER_NETWORK,
|
HCI.CLUSTER_NETWORK,
|
||||||
HCI.NETWORK_ATTACHMENT,
|
HCI.NETWORK_ATTACHMENT,
|
||||||
HCI.VPC,
|
|
||||||
NETWORK_POLICY,
|
|
||||||
HCI.LB,
|
HCI.LB,
|
||||||
HCI.IP_POOL,
|
HCI.IP_POOL,
|
||||||
],
|
],
|
||||||
@@ -457,7 +451,6 @@ export function init($plugin, store) {
|
|||||||
HCI.PCI_DEVICE,
|
HCI.PCI_DEVICE,
|
||||||
HCI.SR_IOVGPU_DEVICE,
|
HCI.SR_IOVGPU_DEVICE,
|
||||||
HCI.VGPU_DEVICE,
|
HCI.VGPU_DEVICE,
|
||||||
HCI.MIG_CONFIGURATION,
|
|
||||||
HCI.USB_DEVICE,
|
HCI.USB_DEVICE,
|
||||||
HCI.ADD_ONS,
|
HCI.ADD_ONS,
|
||||||
HCI.SECRET,
|
HCI.SECRET,
|
||||||
@@ -554,36 +547,6 @@ 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(NETWORK_POLICY, { hiddenNamespaceGroupButton: true, canYaml: false });
|
|
||||||
|
|
||||||
virtualType({
|
|
||||||
labelKey: 'harvester.networkPolicy.label',
|
|
||||||
name: NETWORK_POLICY,
|
|
||||||
namespaced: true,
|
|
||||||
weight: 186,
|
|
||||||
route: {
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
|
||||||
params: { resource: NETWORK_POLICY }
|
|
||||||
},
|
|
||||||
exact: false,
|
|
||||||
ifHaveType: NETWORK_POLICY,
|
|
||||||
});
|
|
||||||
|
|
||||||
configureType(HCI.SNAPSHOT, {
|
configureType(HCI.SNAPSHOT, {
|
||||||
isCreatable: false,
|
isCreatable: false,
|
||||||
location: {
|
location: {
|
||||||
@@ -850,26 +813,6 @@ export function init($plugin, store) {
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
virtualType({
|
|
||||||
labelKey: 'harvester.migconfiguration.label',
|
|
||||||
group: 'advanced',
|
|
||||||
weight: 12,
|
|
||||||
name: HCI.MIG_CONFIGURATION,
|
|
||||||
namespaced: false,
|
|
||||||
route: {
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
|
||||||
params: { resource: HCI.MIG_CONFIGURATION }
|
|
||||||
},
|
|
||||||
exact: false,
|
|
||||||
ifHaveType: HCI.MIG_CONFIGURATION,
|
|
||||||
});
|
|
||||||
|
|
||||||
configureType(HCI.MIG_CONFIGURATION, {
|
|
||||||
isCreatable: false,
|
|
||||||
hiddenNamespaceGroupButton: true,
|
|
||||||
canYaml: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
virtualType({
|
virtualType({
|
||||||
labelKey: 'harvester.usb.label',
|
labelKey: 'harvester.usb.label',
|
||||||
group: 'advanced',
|
group: 'advanced',
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
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',
|
||||||
@@ -72,7 +70,6 @@ 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 = {
|
||||||
@@ -83,14 +80,3 @@ 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,7 +35,6 @@ 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',
|
||||||
@@ -50,7 +49,6 @@ export const HCI = {
|
|||||||
STORAGE_CLASS: 'harvesterhci.io/storageClassName',
|
STORAGE_CLASS: 'harvesterhci.io/storageClassName',
|
||||||
STORAGE_NETWORK: 'storage-network.settings.harvesterhci.io',
|
STORAGE_NETWORK: 'storage-network.settings.harvesterhci.io',
|
||||||
ADDON_EXPERIMENTAL: 'addon.harvesterhci.io/experimental',
|
ADDON_EXPERIMENTAL: 'addon.harvesterhci.io/experimental',
|
||||||
ADDON_DISPLAYNAME: 'addon.harvesterhci.io/displayName',
|
|
||||||
VOLUME_ERROR: 'longhorn.io/volume-scheduling-error',
|
VOLUME_ERROR: 'longhorn.io/volume-scheduling-error',
|
||||||
VOLUME_FOR_VM: 'harvesterhci.io/volumeForVirtualMachine',
|
VOLUME_FOR_VM: 'harvesterhci.io/volumeForVirtualMachine',
|
||||||
KVM_AMD_CPU: 'cpu-feature.node.kubevirt.io/svm',
|
KVM_AMD_CPU: 'cpu-feature.node.kubevirt.io/svm',
|
||||||
@@ -68,13 +66,4 @@ 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,2 +1 @@
|
|||||||
export const CLUSTER_NETWORK = 'clusterNetwork';
|
export const CLUSTER_NETWORK = 'clusterNetwork';
|
||||||
export const VPC = 'vpc';
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export const HCI_SETTING = {
|
|||||||
LOG_LEVEL: 'log-level',
|
LOG_LEVEL: 'log-level',
|
||||||
SERVER_VERSION: 'server-version',
|
SERVER_VERSION: 'server-version',
|
||||||
UI_INDEX: 'ui-index',
|
UI_INDEX: 'ui-index',
|
||||||
|
UI_PLUGIN_INDEX: 'ui-plugin-index',
|
||||||
UPGRADE_CHECKER_ENABLED: 'upgrade-checker-enabled',
|
UPGRADE_CHECKER_ENABLED: 'upgrade-checker-enabled',
|
||||||
UPGRADE_CHECKER_URL: 'upgrade-checker-url',
|
UPGRADE_CHECKER_URL: 'upgrade-checker-url',
|
||||||
VLAN: 'vlan',
|
VLAN: 'vlan',
|
||||||
@@ -27,18 +28,12 @@ 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',
|
|
||||||
KUBEVIRT_MIGRATION: 'kubevirt-migration'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HCI_ALLOWED_SETTINGS = {
|
export const HCI_ALLOWED_SETTINGS = {
|
||||||
@@ -57,21 +52,18 @@ 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.CSI_ONLINE_EXPAND_VALIDATION]: {
|
[HCI_SETTING.SERVER_VERSION]: { readOnly: true },
|
||||||
kind: 'json', from: 'import', featureFlag: 'csiOnlineExpandValidation'
|
[HCI_SETTING.UPGRADE_CHECKER_ENABLED]: { kind: 'boolean' },
|
||||||
},
|
[HCI_SETTING.UPGRADE_CHECKER_URL]: { kind: 'url' },
|
||||||
[HCI_SETTING.SERVER_VERSION]: { readOnly: true },
|
[HCI_SETTING.HTTP_PROXY]: { kind: 'json', from: 'import' },
|
||||||
[HCI_SETTING.UPGRADE_CHECKER_ENABLED]: { kind: 'boolean' },
|
[HCI_SETTING.ADDITIONAL_CA]: {
|
||||||
[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]: { kind: 'number' },
|
[HCI_SETTING.SUPPORT_BUNDLE_TIMEOUT]: {},
|
||||||
[HCI_SETTING.SUPPORT_BUNDLE_EXPIRATION]: { kind: 'number' },
|
[HCI_SETTING.SUPPORT_BUNDLE_EXPIRATION]: {},
|
||||||
[HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT]: { kind: 'number', featureFlag: 'supportBundleNodeCollectionTimeoutSetting' },
|
[HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT]: { 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
|
||||||
@@ -84,6 +76,7 @@ export const HCI_ALLOWED_SETTINGS = {
|
|||||||
[HCI_SETTING.SUPPORT_BUNDLE_NAMESPACES]: { from: 'import', canReset: true },
|
[HCI_SETTING.SUPPORT_BUNDLE_NAMESPACES]: { from: 'import', canReset: true },
|
||||||
[HCI_SETTING.AUTO_DISK_PROVISION_PATHS]: { canReset: true },
|
[HCI_SETTING.AUTO_DISK_PROVISION_PATHS]: { canReset: true },
|
||||||
[HCI_SETTING.RELEASE_DOWNLOAD_URL]: { kind: 'url' },
|
[HCI_SETTING.RELEASE_DOWNLOAD_URL]: { kind: 'url' },
|
||||||
|
[HCI_SETTING.UI_PLUGIN_INDEX]: { kind: 'url' },
|
||||||
[HCI_SETTING.CONTAINERD_REGISTRY]: {
|
[HCI_SETTING.CONTAINERD_REGISTRY]: {
|
||||||
kind: 'json', from: 'import', canReset: true
|
kind: 'json', from: 'import', canReset: true
|
||||||
},
|
},
|
||||||
@@ -96,29 +89,13 @@ 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]: { kind: 'number', featureFlag: 'kubeconfigDefaultTokenTTLMinutesSetting' },
|
[HCI_SETTING.KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES]: { 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',
|
|
||||||
},
|
|
||||||
[HCI_SETTING.KUBEVIRT_MIGRATION]: {
|
|
||||||
kind: 'json', from: 'import', canReset: true, featureFlag: 'kubevirtMigration',
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = {
|
export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Harvester
|
* Harvester
|
||||||
*/
|
*/
|
||||||
import { DESCRIPTION } from '@shell/config/table-headers';
|
|
||||||
|
|
||||||
// image
|
// image
|
||||||
export const IMAGE_DOWNLOAD_SIZE = {
|
export const IMAGE_DOWNLOAD_SIZE = {
|
||||||
@@ -97,37 +96,5 @@ export const IMAGE_STORAGE_CLASS = {
|
|||||||
sort: 'imageStorageClass',
|
sort: 'imageStorageClass',
|
||||||
value: 'imageStorageClass',
|
value: 'imageStorageClass',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
width: 150,
|
width: 200,
|
||||||
};
|
|
||||||
|
|
||||||
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',
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,28 +4,11 @@ export const BACKUP_TYPE = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const NETWORK_TYPE = {
|
export const NETWORK_TYPE = {
|
||||||
L2VLAN: 'L2VlanNetwork',
|
L2VLAN: 'L2VlanNetwork',
|
||||||
UNTAGGED: 'UntaggedNetwork',
|
UNTAGGED: 'UntaggedNetwork'
|
||||||
OVERLAY: 'OverlayNetwork',
|
|
||||||
L2TRUNK_VLAN: 'L2VlanTrunkNetwork',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VOLUME_MODE = {
|
export const VOLUME_MODE = {
|
||||||
BLOCK: 'Block',
|
BLOCK: 'Block',
|
||||||
FILE_SYSTEM: 'Filesystem'
|
FILE_SYSTEM: 'Filesystem'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NETWORK_PROTOCOL = {
|
|
||||||
IPv4: 'IPv4',
|
|
||||||
IPv6: 'IPv6',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const INTERNAL_STORAGE_CLASS = {
|
|
||||||
VMSTATE_PERSISTENCE: 'vmstate-persistence',
|
|
||||||
LONGHORN_STATIC: 'longhorn-static',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const L2VLAN_MODE = {
|
|
||||||
ACCESS: 'access',
|
|
||||||
TRUNK: 'trunk',
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,124 +0,0 @@
|
|||||||
<script>
|
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
|
||||||
import ResourceTabs from '@shell/components/form/ResourceTabs';
|
|
||||||
import Tab from '@shell/components/Tabbed/Tab';
|
|
||||||
import SortableTable from '@shell/components/SortableTable';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
|
|
||||||
components: {
|
|
||||||
ResourceTabs,
|
|
||||||
Tab,
|
|
||||||
SortableTable,
|
|
||||||
},
|
|
||||||
|
|
||||||
mixins: [CreateEditView],
|
|
||||||
|
|
||||||
props: {
|
|
||||||
value: {
|
|
||||||
type: Object,
|
|
||||||
default: () => {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
headers() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name: 'profileName',
|
|
||||||
labelKey: 'harvester.migconfiguration.tableHeaders.profileName',
|
|
||||||
value: 'name',
|
|
||||||
width: 75,
|
|
||||||
sort: 'name',
|
|
||||||
dashIfEmpty: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'vGPUID',
|
|
||||||
labelKey: 'harvester.migconfiguration.tableHeaders.vGPUID',
|
|
||||||
value: 'vGPUID',
|
|
||||||
width: 75,
|
|
||||||
sort: 'vGPUID',
|
|
||||||
dashIfEmpty: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'available',
|
|
||||||
labelKey: 'harvester.migconfiguration.tableHeaders.available',
|
|
||||||
value: 'available',
|
|
||||||
width: 75,
|
|
||||||
sort: 'available',
|
|
||||||
align: 'center',
|
|
||||||
dashIfEmpty: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'requested',
|
|
||||||
labelKey: 'harvester.migconfiguration.tableHeaders.requested',
|
|
||||||
value: 'requested',
|
|
||||||
width: 75,
|
|
||||||
sort: 'requested',
|
|
||||||
align: 'center',
|
|
||||||
dashIfEmpty: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'total',
|
|
||||||
labelKey: 'harvester.migconfiguration.tableHeaders.total',
|
|
||||||
value: 'total',
|
|
||||||
width: 75,
|
|
||||||
sort: 'total',
|
|
||||||
align: 'center',
|
|
||||||
dashIfEmpty: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
rows() {
|
|
||||||
let out = (this.value?.status?.profileStatus || []).map((profile) => {
|
|
||||||
const {
|
|
||||||
id, name, total, available
|
|
||||||
} = profile;
|
|
||||||
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
total,
|
|
||||||
available,
|
|
||||||
vGPUID: profile.vGPUID?.join(', ') || '',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
out = out.map((row) => {
|
|
||||||
const requested = this.value?.spec?.profileSpec.find((p) => p.id === row.id)?.requested || 0;
|
|
||||||
|
|
||||||
return { ...row, requested };
|
|
||||||
});
|
|
||||||
|
|
||||||
return out;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<ResourceTabs
|
|
||||||
:value="value"
|
|
||||||
:need-events="false"
|
|
||||||
:need-related="false"
|
|
||||||
:mode="mode"
|
|
||||||
>
|
|
||||||
<Tab
|
|
||||||
name="Profile Status"
|
|
||||||
:label="t('harvester.migconfiguration.profileStatus')"
|
|
||||||
>
|
|
||||||
<SortableTable
|
|
||||||
:headers="headers"
|
|
||||||
:rows="rows"
|
|
||||||
key-field="condition"
|
|
||||||
default-sort-by="condition"
|
|
||||||
:table-actions="false"
|
|
||||||
:row-actions="false"
|
|
||||||
:search="false"
|
|
||||||
/>
|
|
||||||
</Tab>
|
|
||||||
</ResourceTabs>
|
|
||||||
</template>
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
<script>
|
|
||||||
import LabelValue from '@shell/components/LabelValue';
|
|
||||||
import { HCI } from '../../types';
|
|
||||||
import { DOC } from '../../config/doc-links';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'HarvesterHugepages',
|
|
||||||
components: { LabelValue },
|
|
||||||
|
|
||||||
props: {
|
|
||||||
node: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
docsTransparentHugepagesLink() {
|
|
||||||
return DOC.TRANSPARENT_HUGEPAGES;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
const hash = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.HUGEPAGES });
|
|
||||||
|
|
||||||
this.hugepages = hash.find((node) => {
|
|
||||||
return node.id === this.node.id;
|
|
||||||
}) || {};
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return { hugepages: {} };
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<template v-if="hugepages.status">
|
|
||||||
<h2>{{ t('harvester.host.hugepages.meminfo') }}</h2>
|
|
||||||
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabelValue
|
|
||||||
:name="t('harvester.host.hugepages.status.anon')"
|
|
||||||
:value="hugepages.status.meminfo.anonHugePages"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabelValue
|
|
||||||
:name="t('harvester.host.hugepages.status.size')"
|
|
||||||
:value="hugepages.status.meminfo.hugepageSize"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-3">
|
|
||||||
<LabelValue
|
|
||||||
:name="t('harvester.host.hugepages.status.total')"
|
|
||||||
:value="hugepages.status.meminfo.hugePagesTotal"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col span-3">
|
|
||||||
<LabelValue
|
|
||||||
:name="t('harvester.host.hugepages.status.free')"
|
|
||||||
:value="hugepages.status.meminfo.hugePagesFree"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col span-3">
|
|
||||||
<LabelValue
|
|
||||||
:name="t('harvester.host.hugepages.status.rsvd')"
|
|
||||||
:value="hugepages.status.meminfo.hugePagesRsvd"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col span-3">
|
|
||||||
<LabelValue
|
|
||||||
:name="t('harvester.host.hugepages.status.surp')"
|
|
||||||
:value="hugepages.status.meminfo.hugePagesSurp"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<hr class="divider" />
|
|
||||||
<h3>
|
|
||||||
<t
|
|
||||||
k="harvester.host.hugepages.transparent.title"
|
|
||||||
:raw="true"
|
|
||||||
:url="docsTransparentHugepagesLink"
|
|
||||||
/>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-4">
|
|
||||||
<LabelValue
|
|
||||||
:name="t('harvester.host.hugepages.transparent.enabled')"
|
|
||||||
:value="hugepages.spec.transparent.enabled"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col span-4">
|
|
||||||
<LabelValue
|
|
||||||
:name="t('harvester.host.hugepages.transparent.shmemEnabled')"
|
|
||||||
:value="hugepages.spec.transparent.shmemEnabled"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col span-4">
|
|
||||||
<LabelValue
|
|
||||||
:name="t('harvester.host.hugepages.transparent.defrag')"
|
|
||||||
:value="hugepages.spec.transparent.defrag"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -47,26 +47,20 @@ export default {
|
|||||||
STATE,
|
STATE,
|
||||||
NAME,
|
NAME,
|
||||||
{
|
{
|
||||||
name: 'CPU',
|
name: 'vmCPU',
|
||||||
label: 'CPU',
|
labelKey: 'tableHeaders.cpu',
|
||||||
sort: ['displayCPU'],
|
search: false,
|
||||||
value: 'displayCPU',
|
sort: ['spec.template.spec.domain.cpu.cores'],
|
||||||
align: 'center',
|
value: 'spec.template.spec.domain.cpu.cores',
|
||||||
dashIfEmpty: true,
|
width: 120
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Memory',
|
name: 'vmRAM',
|
||||||
value: 'displayMemory',
|
labelKey: 'glance.memory',
|
||||||
sort: ['memorySort'],
|
search: false,
|
||||||
align: 'center',
|
sort: ['memorySort'],
|
||||||
labelKey: 'tableHeaders.memory',
|
value: 'spec.template.spec.domain.resources.limits.memory',
|
||||||
formatter: 'Si',
|
width: 120
|
||||||
formatterOpts: {
|
|
||||||
opts: {
|
|
||||||
increment: 1024, addSuffix: true, maxExponent: 3, minExponent: 3, suffix: 'i',
|
|
||||||
},
|
|
||||||
needParseSi: true
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'ip',
|
name: 'ip',
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import Instance from './VirtualMachineInstance';
|
|||||||
import Disk from './HarvesterHostDisk';
|
import Disk from './HarvesterHostDisk';
|
||||||
import VlanStatus from './VlanStatus';
|
import VlanStatus from './VlanStatus';
|
||||||
import HarvesterKsmtuned from './HarvesterKsmtuned.vue';
|
import HarvesterKsmtuned from './HarvesterKsmtuned.vue';
|
||||||
import HarvesterHugepages from './HarvesterHugepages.vue';
|
|
||||||
import HarvesterSeeder from './HarvesterSeeder';
|
import HarvesterSeeder from './HarvesterSeeder';
|
||||||
|
|
||||||
const LONGHORN_SYSTEM = 'longhorn-system';
|
const LONGHORN_SYSTEM = 'longhorn-system';
|
||||||
@@ -47,7 +46,6 @@ export default {
|
|||||||
VlanStatus,
|
VlanStatus,
|
||||||
LabelValue,
|
LabelValue,
|
||||||
HarvesterKsmtuned,
|
HarvesterKsmtuned,
|
||||||
HarvesterHugepages,
|
|
||||||
Loading,
|
Loading,
|
||||||
SortableTable,
|
SortableTable,
|
||||||
HarvesterSeeder,
|
HarvesterSeeder,
|
||||||
@@ -211,12 +209,6 @@ export default {
|
|||||||
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.KSTUNED);
|
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.KSTUNED);
|
||||||
},
|
},
|
||||||
|
|
||||||
hasHugepagesSchema() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.HUGEPAGES);
|
|
||||||
},
|
|
||||||
|
|
||||||
hasBlockDevicesSchema() {
|
hasBlockDevicesSchema() {
|
||||||
return !!this.$store.getters['harvester/schemaFor'](HCI.BLOCK_DEVICE);
|
return !!this.$store.getters['harvester/schemaFor'](HCI.BLOCK_DEVICE);
|
||||||
},
|
},
|
||||||
@@ -476,16 +468,6 @@ export default {
|
|||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab
|
|
||||||
v-if="hasHugepagesSchema"
|
|
||||||
name="hugepages"
|
|
||||||
:weight="0"
|
|
||||||
:show-header="false"
|
|
||||||
:label="t('harvester.host.tabs.hugepages')"
|
|
||||||
>
|
|
||||||
<HarvesterHugepages :node="value" />
|
|
||||||
</Tab>
|
|
||||||
|
|
||||||
<Tab
|
<Tab
|
||||||
v-if="seederEnabled"
|
v-if="seederEnabled"
|
||||||
name="seeder"
|
name="seeder"
|
||||||
|
|||||||
@@ -125,7 +125,6 @@ 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"
|
||||||
@@ -172,9 +171,6 @@ 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,10 +118,6 @@ 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>
|
||||||
@@ -256,16 +252,6 @@ 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,7 +125,6 @@ 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"
|
||||||
@@ -172,9 +171,6 @@ 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">
|
||||||
|
|||||||
+2
-3
@@ -5,7 +5,6 @@ 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';
|
||||||
|
|
||||||
@@ -92,9 +91,9 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
flavor() {
|
flavor() {
|
||||||
const { cpu, memory } = getVmCPUMemoryValues(this.value);
|
const domain = this.value?.spec?.template?.spec?.domain;
|
||||||
|
|
||||||
return `${ cpu } vCPU , ${ memory } ${ this.t('harvester.virtualMachine.input.memory') }`;
|
return `${ domain.cpu?.cores } vCPU , ${ domain.resources?.limits?.memory } ${ this.t('harvester.virtualMachine.input.memory') }`;
|
||||||
},
|
},
|
||||||
|
|
||||||
kernelRelease() {
|
kernelRelease() {
|
||||||
|
|||||||
-25
@@ -17,13 +17,6 @@ export default {
|
|||||||
default: () => {
|
default: () => {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
},
|
|
||||||
vmimResource: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
default: () => {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -32,12 +25,6 @@ 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;
|
||||||
},
|
},
|
||||||
@@ -71,18 +58,6 @@ 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,8 +27,6 @@ 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',
|
||||||
|
|
||||||
@@ -65,7 +63,6 @@ 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,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -81,7 +78,6 @@ 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 }),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -135,39 +131,6 @@ 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 || {};
|
||||||
@@ -175,6 +138,7 @@ 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;
|
||||||
@@ -193,10 +157,6 @@ export default {
|
|||||||
vm: this.value.name
|
vm: this.value.name
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
liveMigrationProgressEnabled() {
|
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('liveMigrationProgress');
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@@ -213,7 +173,6 @@ export default {
|
|||||||
const diskRows = this.getDiskRows(neu);
|
const diskRows = this.getDiskRows(neu);
|
||||||
|
|
||||||
this['diskRows'] = diskRows;
|
this['diskRows'] = diskRows;
|
||||||
this['networkRows'] = this.getNetworkRows(neu, { fromTemplate: false, init: false });
|
|
||||||
},
|
},
|
||||||
deep: true
|
deep: true
|
||||||
}
|
}
|
||||||
@@ -266,7 +225,6 @@ export default {
|
|||||||
<Network
|
<Network
|
||||||
v-model:value="networkRows"
|
v-model:value="networkRows"
|
||||||
mode="view"
|
mode="view"
|
||||||
:vm="value"
|
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
@@ -388,20 +346,6 @@ 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,13 +1,12 @@
|
|||||||
<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',
|
||||||
@@ -15,7 +14,7 @@ export default {
|
|||||||
emits: ['close'],
|
emits: ['close'],
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
AsyncButton, Banner, Checkbox, Card, LabeledInput, LabeledSelect
|
AsyncButton, Banner, Checkbox, Card, LabeledInput
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
@@ -27,11 +26,9 @@ export default {
|
|||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
name: '',
|
name: '',
|
||||||
cloneData: true,
|
cloneData: true,
|
||||||
errors: [],
|
errors: []
|
||||||
runStrategy: runStrategyOptions[1],
|
|
||||||
runStrategyOptions
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -41,9 +38,6 @@ export default {
|
|||||||
actionResource() {
|
actionResource() {
|
||||||
return this.resources[0];
|
return this.resources[0];
|
||||||
},
|
},
|
||||||
vmCloneRunStrategyEnabled() {
|
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('vmCloneRunStrategy');
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@@ -64,8 +58,7 @@ export default {
|
|||||||
|
|
||||||
// deep clone
|
// deep clone
|
||||||
try {
|
try {
|
||||||
const payload = this.vmCloneRunStrategyEnabled ? { targetVm: this.name, runStrategy: this.runStrategy } : { targetVm: this.name };
|
const res = await this.actionResource.doAction('clone', { targetVm: this.name }, {}, false);
|
||||||
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', {
|
||||||
@@ -113,19 +106,6 @@ 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
|
||||||
@@ -147,6 +127,11 @@ 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 }, true);
|
return this.t('dialog.confirmExecution.andOthers', { count: remaining });
|
||||||
},
|
},
|
||||||
|
|
||||||
type() {
|
type() {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export default {
|
|||||||
resources: {
|
resources: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
@@ -42,25 +42,8 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
...mapState('action-menu', ['modalData']),
|
...mapState('action-menu', ['modalData']),
|
||||||
|
|
||||||
title() {
|
warningMessageKey() {
|
||||||
return this.modalData?.title || 'dialog.promptRemove.title';
|
return this.modalData.warningMessageKey;
|
||||||
},
|
|
||||||
|
|
||||||
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() {
|
||||||
@@ -87,12 +70,6 @@ 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;
|
||||||
|
|
||||||
@@ -120,15 +97,11 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
deleteDisabled() {
|
deleteDisabled() {
|
||||||
if (!this.needConfirmation) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.confirmName !== this.nameToMatch;
|
return this.confirmName !== this.nameToMatch;
|
||||||
},
|
},
|
||||||
|
|
||||||
protip() {
|
protip() {
|
||||||
return this.t('dialog.promptRemove.protip', { alternateLabel });
|
return this.t('promptRemove.protip', { alternateLabel });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -145,7 +118,6 @@ export default {
|
|||||||
try {
|
try {
|
||||||
for (const resource of this.resources) {
|
for (const resource of this.resources) {
|
||||||
await resource.remove();
|
await resource.remove();
|
||||||
if (this.modalData?.extraActionAfterRemove) await this.modalData.extraActionAfterRemove();
|
|
||||||
}
|
}
|
||||||
buttonDone(true);
|
buttonDone(true);
|
||||||
this.close();
|
this.close();
|
||||||
@@ -165,39 +137,31 @@ export default {
|
|||||||
>
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
<h4 class="text-default-text">
|
<h4 class="text-default-text">
|
||||||
{{ t(title, { type }, true) }}
|
{{ t('promptRemove.title') }}
|
||||||
</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="warningMessage"
|
v-clean-html="t(warningMessageKey, { type, names: resourceNames }, true)"
|
||||||
></span>
|
></span>
|
||||||
|
|
||||||
<div
|
<div class="mt-10 mb-10">
|
||||||
v-if="needConfirmation"
|
<span
|
||||||
class="mt-20"
|
v-clean-html="t('promptRemove.confirmName', { nameToMatch: escapeHtml(nameToMatch) }, true)"
|
||||||
>
|
></span>
|
||||||
<div class="mt-10 mb-10">
|
</div>
|
||||||
<span
|
<div class="mb-10">
|
||||||
v-clean-html="t('dialog.promptRemove.confirmName', {
|
<CopyToClipboardText :text="nameToMatch" />
|
||||||
type: formattedType,
|
</div>
|
||||||
nameToMatch: escapeHtml(nameToMatch)
|
<input
|
||||||
}, true)"
|
id="confirm"
|
||||||
></span>
|
v-model="confirmName"
|
||||||
</div>
|
type="text"
|
||||||
<div class="mb-10">
|
/>
|
||||||
<CopyToClipboardText :text="nameToMatch" />
|
<div class="text-info mt-20">
|
||||||
</div>
|
{{ protip }}
|
||||||
<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"
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
<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>
|
||||||
+4
-8
@@ -11,7 +11,7 @@ import { LabeledInput } from '@components/Form/LabeledInput';
|
|||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HotplugVolumeModal',
|
name: 'HotplugModal',
|
||||||
|
|
||||||
emits: ['close'],
|
emits: ['close'],
|
||||||
|
|
||||||
@@ -57,12 +57,8 @@ 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 true;
|
return !pvc.attachVM;
|
||||||
})
|
})
|
||||||
.map((pvc) => {
|
.map((pvc) => {
|
||||||
return {
|
return {
|
||||||
@@ -90,7 +86,7 @@ export default {
|
|||||||
if (res._status === 200 || res._status === 204) {
|
if (res._status === 200 || res._status === 204) {
|
||||||
this.$store.dispatch('growl/success', {
|
this.$store.dispatch('growl/success', {
|
||||||
title: this.t('generic.notification.title.succeed'),
|
title: this.t('generic.notification.title.succeed'),
|
||||||
message: this.t('harvester.modal.hotplugVolume.success', { diskName: this.diskName, vm: this.actionResource.nameDisplay })
|
message: this.t('harvester.modal.hotplug.success', { diskName: this.diskName, vm: this.actionResource.nameDisplay })
|
||||||
}, { root: true });
|
}, { root: true });
|
||||||
|
|
||||||
this.close();
|
this.close();
|
||||||
@@ -122,7 +118,7 @@ export default {
|
|||||||
>
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
<h4
|
<h4
|
||||||
v-clean-html="t('harvester.modal.hotplugVolume.title')"
|
v-clean-html="t('harvester.modal.hotplug.title')"
|
||||||
class="text-default-text"
|
class="text-default-text"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -1,212 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
import { NETWORK_ATTACHMENT } from '@shell/config/types';
|
|
||||||
import { Card } from '@components/Card';
|
|
||||||
import { Banner } from '@components/Banner';
|
|
||||||
import AsyncButton from '@shell/components/AsyncButton';
|
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
||||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
|
||||||
import { NETWORK_TYPE } from '../config/types';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'AddHotplugNic',
|
|
||||||
|
|
||||||
emits: ['close'],
|
|
||||||
|
|
||||||
components: {
|
|
||||||
AsyncButton,
|
|
||||||
Card,
|
|
||||||
LabeledInput,
|
|
||||||
LabeledSelect,
|
|
||||||
Banner
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
|
||||||
resources: {
|
|
||||||
type: Array,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
try {
|
|
||||||
this.allVMNetworks = await this.$store.dispatch('harvester/findAll', { type: NETWORK_ATTACHMENT });
|
|
||||||
} catch (err) {
|
|
||||||
this.errors = exceptionToErrorsArray(err);
|
|
||||||
this.allVMNetworks = [];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
interfaceName: '',
|
|
||||||
networkName: '',
|
|
||||||
macAddress: '',
|
|
||||||
allVMNetworks: [],
|
|
||||||
errors: [],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
...mapGetters({ t: 'i18n/t' }),
|
|
||||||
|
|
||||||
actionResource() {
|
|
||||||
return this.resources?.[0];
|
|
||||||
},
|
|
||||||
|
|
||||||
isFormValid() {
|
|
||||||
return this.interfaceName !== '' && this.networkName !== '';
|
|
||||||
},
|
|
||||||
|
|
||||||
vmNetworksOption() {
|
|
||||||
return this.allVMNetworks
|
|
||||||
.filter((network) => {
|
|
||||||
const labels = network.metadata?.labels || {};
|
|
||||||
const type = labels[HCI_ANNOTATIONS.NETWORK_TYPE];
|
|
||||||
|
|
||||||
const isValidType = [
|
|
||||||
NETWORK_TYPE.L2VLAN,
|
|
||||||
NETWORK_TYPE.UNTAGGED,
|
|
||||||
NETWORK_TYPE.L2TRUNK_VLAN,
|
|
||||||
].includes(type);
|
|
||||||
|
|
||||||
return isValidType && !network.isSystem;
|
|
||||||
})
|
|
||||||
.map((network) => {
|
|
||||||
const label = network.isNotReady ? `${ network.id } (${ this.t('generic.notReady') })` : network.id;
|
|
||||||
|
|
||||||
return ({
|
|
||||||
label,
|
|
||||||
value: network.id || '',
|
|
||||||
disabled: network.isNotReady,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
close() {
|
|
||||||
this.interfaceName = '';
|
|
||||||
this.networkName = '';
|
|
||||||
this.macAddress = '';
|
|
||||||
this.errors = [];
|
|
||||||
this.$emit('close');
|
|
||||||
},
|
|
||||||
|
|
||||||
async save(buttonCb) {
|
|
||||||
if (!this.actionResource) {
|
|
||||||
buttonCb(false);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
interfaceName: this.interfaceName,
|
|
||||||
networkName: this.networkName
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.macAddress) {
|
|
||||||
payload.macAddress = this.macAddress;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await this.actionResource.doAction('addNic', payload);
|
|
||||||
|
|
||||||
if ([200, 204].includes(res?._status)) {
|
|
||||||
this.$store.dispatch('growl/success', {
|
|
||||||
title: this.t('generic.notification.title.succeed'),
|
|
||||||
message: this.t('harvester.modal.hotplugNic.success', {
|
|
||||||
interfaceName: this.interfaceName,
|
|
||||||
vm: this.actionResource.nameDisplay
|
|
||||||
})
|
|
||||||
}, { root: true });
|
|
||||||
|
|
||||||
this.close();
|
|
||||||
buttonCb(true);
|
|
||||||
} else {
|
|
||||||
this.errors = exceptionToErrorsArray(res);
|
|
||||||
buttonCb(false);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
this.errors = exceptionToErrorsArray(err);
|
|
||||||
buttonCb(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Card
|
|
||||||
ref="modal"
|
|
||||||
name="modal"
|
|
||||||
:show-highlight-border="false"
|
|
||||||
>
|
|
||||||
<template #title>
|
|
||||||
<h4
|
|
||||||
v-clean-html="t('harvester.modal.hotplugNic.title')"
|
|
||||||
class="text-default-text"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #body>
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="interfaceName"
|
|
||||||
:label="t('generic.name')"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="networkName"
|
|
||||||
class="mt-20"
|
|
||||||
:label="t('harvester.modal.hotplugNic.vmNetwork')"
|
|
||||||
:options="vmNetworksOption"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="macAddress"
|
|
||||||
class="mt-20"
|
|
||||||
label-key="harvester.modal.hotplugNic.macAddress"
|
|
||||||
:tooltip="t('harvester.modal.hotplugNic.macAddressTooltip', _, true)"
|
|
||||||
/>
|
|
||||||
<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"
|
|
||||||
:disabled="!isFormValid"
|
|
||||||
@click="save"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.actions {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.buttons {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
<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,7 +10,6 @@ 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',
|
||||||
@@ -106,17 +105,11 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const options = storages.filter((s) => !s.parameters?.backingImage).map((s) => {
|
const options = storages.filter((s) => !s.parameters?.backingImage).map((s) => {
|
||||||
let label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
const 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,5 +1,4 @@
|
|||||||
<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';
|
||||||
@@ -7,15 +6,7 @@ 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',
|
||||||
@@ -26,36 +17,14 @@ 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 {
|
||||||
isOpen: false,
|
url: '',
|
||||||
errors: [],
|
description: '',
|
||||||
version: '',
|
errors: [],
|
||||||
clusterName: '',
|
isOpen: false,
|
||||||
url: '',
|
|
||||||
description: '',
|
|
||||||
namespaces: [],
|
|
||||||
defaultNamespaces: [],
|
|
||||||
timeout: '',
|
|
||||||
expiration: '',
|
|
||||||
nodeTimeout: '',
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -70,51 +39,23 @@ 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) {
|
||||||
this.isOpen = show;
|
if (show) {
|
||||||
}
|
this.$nextTick(() => {
|
||||||
},
|
this.isOpen = true;
|
||||||
|
});
|
||||||
isOpen(newVal) {
|
} else {
|
||||||
if (newVal) {
|
this.isOpen = false;
|
||||||
this.loadDefaultSettings();
|
this.url = '';
|
||||||
} else {
|
this.description = '';
|
||||||
this.resetForm();
|
}
|
||||||
}
|
},
|
||||||
|
immediate: true
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -124,93 +65,33 @@ 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-${ this.clusterName }-${ this.version }-${ randomStr(5).toLowerCase() }`;
|
const name = `bundle-${ 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: { name, namespace },
|
metadata: {
|
||||||
spec,
|
name,
|
||||||
|
namespace
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
issueURL: this.url,
|
||||||
|
description: this.description
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
const bundleValue = await this.$store.dispatch(`${ inStore }/create`, bundleCrd);
|
||||||
const bundleValue = await this.$store.dispatch(`${ inStore }/create`, bundleCrd);
|
|
||||||
|
|
||||||
|
try {
|
||||||
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 });
|
||||||
@@ -237,110 +118,69 @@ export default {
|
|||||||
@close="close"
|
@close="close"
|
||||||
>
|
>
|
||||||
<div class="p-20">
|
<div class="p-20">
|
||||||
<h2>{{ t('harvester.modal.bundle.title') }}</h2>
|
<h2>
|
||||||
<div class="content">
|
{{ t('harvester.modal.bundle.title') }}
|
||||||
<div
|
</h2>
|
||||||
v-if="bundlePending"
|
|
||||||
class="circle mb-20"
|
<div
|
||||||
>
|
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="lightgrey"
|
secondary-stroke-color="white"
|
||||||
:stroke-width="6"
|
:stroke-width="6"
|
||||||
:percentage="percentage"
|
:percentage="percentage"
|
||||||
:show-text="true"
|
:show-text="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<template v-else>
|
</div>
|
||||||
<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"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<template v-if="customSupportBundleFeatureEnabled">
|
<div
|
||||||
<LabeledSelect
|
v-for="(err, idx) in errors"
|
||||||
v-model:value="namespaces"
|
:key="idx"
|
||||||
:label="t('harvester.modal.bundle.namespaces.label')"
|
>
|
||||||
:clearable="true"
|
<Banner
|
||||||
:multiple="true"
|
color="error"
|
||||||
:options="namespaceOptions"
|
:label="stringify(err)"
|
||||||
class="mb-10 label-select"
|
/>
|
||||||
:tooltip="t('harvester.modal.bundle.namespaces.tooltip', _, true)"
|
</div>
|
||||||
@update:value="updateNamespaces"
|
|
||||||
/>
|
<div class="footer mt-20">
|
||||||
<UnitInput
|
<button
|
||||||
v-model:value="timeout"
|
class="btn btn-sm role-secondary mr-10"
|
||||||
:label="t('harvester.modal.bundle.timeout.label')"
|
@click="close"
|
||||||
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"
|
|
||||||
>
|
>
|
||||||
<Banner
|
{{ t('generic.close') }}
|
||||||
color="error"
|
</button>
|
||||||
:label="stringify(err)"
|
|
||||||
/>
|
<AsyncButton
|
||||||
</div>
|
type="submit"
|
||||||
<div class="footer mt-20">
|
mode="generate"
|
||||||
<button
|
class="btn btn-sm bg-primary"
|
||||||
class="btn btn-sm role-secondary mr-10"
|
:disabled="bundlePending"
|
||||||
@click="close"
|
@click="save"
|
||||||
>
|
/>
|
||||||
{{ 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>
|
||||||
@@ -354,10 +194,6 @@ 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);
|
||||||
@@ -368,8 +204,10 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
|
height: 218px;
|
||||||
|
|
||||||
.circle {
|
.circle {
|
||||||
padding: 10px 0;
|
padding-top: 20px;
|
||||||
height: 160px;
|
height: 160px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-38
@@ -1,12 +1,13 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapState, mapGetters } from 'vuex';
|
import { mapState, mapGetters } from 'vuex';
|
||||||
|
|
||||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
import { Card } from '@components/Card';
|
import { Card } from '@components/Card';
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import AsyncButton from '@shell/components/AsyncButton';
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterHotUnplug',
|
name: 'HarvesterHotUnplugModal',
|
||||||
|
|
||||||
emits: ['close'],
|
emits: ['close'],
|
||||||
|
|
||||||
@@ -34,25 +35,8 @@ export default {
|
|||||||
actionResource() {
|
actionResource() {
|
||||||
return this.resources[0];
|
return this.resources[0];
|
||||||
},
|
},
|
||||||
|
diskName() {
|
||||||
name() {
|
return this.modalData.diskName;
|
||||||
return this.modalData.name;
|
|
||||||
},
|
|
||||||
|
|
||||||
isVolume() {
|
|
||||||
return this.modalData.type === 'volume';
|
|
||||||
},
|
|
||||||
|
|
||||||
titleKey() {
|
|
||||||
return this.isVolume ? 'harvester.virtualMachine.hotUnplug.detachVolume.title' : 'harvester.virtualMachine.hotUnplug.detachNIC.title';
|
|
||||||
},
|
|
||||||
|
|
||||||
actionLabelKey() {
|
|
||||||
return this.isVolume ? 'harvester.virtualMachine.hotUnplug.detachVolume.actionLabel' : 'harvester.virtualMachine.hotUnplug.detachNIC.actionLabel';
|
|
||||||
},
|
|
||||||
|
|
||||||
successMessageKey() {
|
|
||||||
return this.isVolume ? 'harvester.virtualMachine.hotUnplug.detachVolume.success' : 'harvester.virtualMachine.hotUnplug.detachNIC.success';
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -63,20 +47,14 @@ export default {
|
|||||||
|
|
||||||
async save(buttonCb) {
|
async save(buttonCb) {
|
||||||
try {
|
try {
|
||||||
let res;
|
const res = await this.actionResource.doAction('removeVolume', { diskName: this.diskName });
|
||||||
|
|
||||||
if (this.isVolume) {
|
|
||||||
res = await this.actionResource.doAction('removeVolume', { diskName: this.name });
|
|
||||||
} else {
|
|
||||||
res = await this.actionResource.doAction('removeNic', { interfaceName: this.name });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res._status === 200 || res._status === 204) {
|
if (res._status === 200 || res._status === 204) {
|
||||||
this.$store.dispatch(
|
this.$store.dispatch(
|
||||||
'growl/success',
|
'growl/success',
|
||||||
{
|
{
|
||||||
title: this.t('generic.notification.title.succeed'),
|
title: this.t('generic.notification.title.succeed'),
|
||||||
message: this.t(this.successMessageKey, { name: this.name })
|
message: this.t('harvester.modal.hotunplug.success', { name: this.diskName })
|
||||||
},
|
},
|
||||||
{ root: true }
|
{ root: true }
|
||||||
);
|
);
|
||||||
@@ -86,14 +64,14 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
const error = [res?.data] || exceptionToErrorsArray(res);
|
const error = [res?.data] || exceptionToErrorsArray(res);
|
||||||
|
|
||||||
this.errors = error;
|
this['errors'] = error;
|
||||||
buttonCb(false);
|
buttonCb(false);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = err?.data || err;
|
const error = err?.data || err;
|
||||||
const message = exceptionToErrorsArray(error);
|
const message = exceptionToErrorsArray(error);
|
||||||
|
|
||||||
this.errors = message;
|
this['errors'] = message;
|
||||||
buttonCb(false);
|
buttonCb(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,7 +87,7 @@ export default {
|
|||||||
>
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
<h4
|
<h4
|
||||||
v-clean-html="t(titleKey, { name })"
|
v-clean-html="t('harvester.virtualMachine.unplug.title', { name: diskName })"
|
||||||
class="text-default-text"
|
class="text-default-text"
|
||||||
/>
|
/>
|
||||||
<Banner
|
<Banner
|
||||||
@@ -133,9 +111,9 @@ export default {
|
|||||||
|
|
||||||
<AsyncButton
|
<AsyncButton
|
||||||
mode="apply"
|
mode="apply"
|
||||||
:action-label="t(actionLabelKey)"
|
:action-label="t('harvester.virtualMachine.unplug.actionLabel')"
|
||||||
:waiting-label="t(actionLabelKey)"
|
:waiting-label="t('harvester.virtualMachine.unplug.actionLabel')"
|
||||||
:success-label="t(actionLabelKey)"
|
:success-label="t('harvester.virtualMachine.unplug.actionLabel')"
|
||||||
@click="save"
|
@click="save"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -154,8 +132,4 @@ export default {
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep(.card-title) {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@@ -73,10 +73,8 @@ 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,
|
url: `/v1/harvester/${ HCI.VLAN_CONFIG }s/${ data.id }`,
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,133 +0,0 @@
|
|||||||
<script>
|
|
||||||
import Tabbed from '@shell/components/Tabbed';
|
|
||||||
import Tab from '@shell/components/Tabbed/Tab';
|
|
||||||
import CruResource from '@shell/components/CruResource';
|
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
||||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
|
||||||
import LabelValue from '@shell/components/LabelValue';
|
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'HarvesterEditMIGConfiguration',
|
|
||||||
|
|
||||||
components: {
|
|
||||||
Tab,
|
|
||||||
Tabbed,
|
|
||||||
CruResource,
|
|
||||||
LabeledInput,
|
|
||||||
NameNsDescription,
|
|
||||||
LabelValue
|
|
||||||
},
|
|
||||||
|
|
||||||
mixins: [CreateEditView],
|
|
||||||
|
|
||||||
inheritAttrs: false,
|
|
||||||
|
|
||||||
props: {
|
|
||||||
value: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
const { profileSpec } = this.value.spec;
|
|
||||||
|
|
||||||
return { profileSpec: profileSpec || [] };
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
if (this.registerBeforeHook) {
|
|
||||||
this.registerBeforeHook(this.updateBeforeSave);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
isView() {
|
|
||||||
return this.mode === 'view';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
updateBeforeSave() {
|
|
||||||
// MIGConfiguration CRD don't have any namespace field,
|
|
||||||
// so we need to remove the namespace field before saving
|
|
||||||
delete this.value.metadata.namespace;
|
|
||||||
// enable the MIGConfiguration when saving
|
|
||||||
this.value.spec.enabled = true;
|
|
||||||
},
|
|
||||||
|
|
||||||
labelTitle(profile) {
|
|
||||||
return `${ profile.name } (available : ${ this.available(profile) })`;
|
|
||||||
},
|
|
||||||
|
|
||||||
available(profile) {
|
|
||||||
const count = this.value.status?.profileStatus?.find((p) => p.id === profile.id)?.available;
|
|
||||||
|
|
||||||
return count || 0;
|
|
||||||
},
|
|
||||||
|
|
||||||
updateRequested(neu, profile) {
|
|
||||||
if (neu === null || neu === '') return;
|
|
||||||
const newValue = Number(neu);
|
|
||||||
const availableCount = this.available(profile);
|
|
||||||
|
|
||||||
if (newValue < 0) {
|
|
||||||
profile.requested = 0;
|
|
||||||
} else if ( newValue > availableCount ) {
|
|
||||||
profile.requested = availableCount;
|
|
||||||
} else {
|
|
||||||
profile.requested = newValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CruResource
|
|
||||||
:done-route="doneRoute"
|
|
||||||
:resource="value"
|
|
||||||
:mode="mode"
|
|
||||||
:errors="errors"
|
|
||||||
:apply-hooks="applyHooks"
|
|
||||||
finish-button-mode="enable"
|
|
||||||
@finish="save"
|
|
||||||
@error="e=>errors=e"
|
|
||||||
>
|
|
||||||
<NameNsDescription
|
|
||||||
:value="value"
|
|
||||||
:mode="mode"
|
|
||||||
/>
|
|
||||||
<Tabbed
|
|
||||||
v-bind="$attrs"
|
|
||||||
class="mt-15"
|
|
||||||
:side-tabs="true"
|
|
||||||
>
|
|
||||||
<Tab
|
|
||||||
name="profileSpec"
|
|
||||||
:label="t('harvester.migconfiguration.profileSpec')"
|
|
||||||
:weight="1"
|
|
||||||
class="bordered-table"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-for="(profile, index) in profileSpec"
|
|
||||||
:key="index"
|
|
||||||
>
|
|
||||||
<LabelValue
|
|
||||||
:value="labelTitle(profile)"
|
|
||||||
class="mb-10"
|
|
||||||
/>
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="profile.requested"
|
|
||||||
:min="0"
|
|
||||||
:disabled="isView"
|
|
||||||
type="number"
|
|
||||||
class="mb-20"
|
|
||||||
:label="`${t('harvester.migconfiguration.requested')}`"
|
|
||||||
@update:value="updateRequested($event, profile)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Tab>
|
|
||||||
</Tabbed>
|
|
||||||
</CruResource>
|
|
||||||
</template>
|
|
||||||
@@ -38,7 +38,6 @@ 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,7 +80,6 @@ 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"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import LabelValue from '@shell/components/LabelValue';
|
|||||||
import { BadgeState } from '@components/BadgeState';
|
import { BadgeState } from '@components/BadgeState';
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
import { RadioGroup } from '@components/Form/Radio';
|
import { RadioGroup, RadioButton } from '@components/Form/Radio';
|
||||||
import HarvesterDisk from '../../mixins/harvester-disk';
|
import HarvesterDisk from '../../mixins/harvester-disk';
|
||||||
import Tags from '../../components/DiskTags';
|
import Tags from '../../components/DiskTags';
|
||||||
import { HCI } from '../../types';
|
import { HCI } from '../../types';
|
||||||
@@ -30,6 +30,7 @@ export default {
|
|||||||
BadgeState,
|
BadgeState,
|
||||||
Banner,
|
Banner,
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
|
RadioButton,
|
||||||
ModalWithCard,
|
ModalWithCard,
|
||||||
Tags,
|
Tags,
|
||||||
},
|
},
|
||||||
@@ -183,11 +184,12 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
forceFormattedDisabled() {
|
forceFormattedDisabled() {
|
||||||
|
const lastFormattedAt = this.blockDevice?.status?.deviceStatus?.fileSystem?.LastFormattedAt;
|
||||||
const fileSystem = this.blockDevice?.status?.deviceStatus?.fileSystem.type;
|
const fileSystem = this.blockDevice?.status?.deviceStatus?.fileSystem.type;
|
||||||
|
|
||||||
const systems = ['ext4', 'XFS'];
|
const systems = ['ext4', 'XFS'];
|
||||||
|
|
||||||
if (this.blockDevice?.childParts?.length > 0) {
|
if (lastFormattedAt || this.blockDevice?.childParts?.length > 0) {
|
||||||
return true;
|
return true;
|
||||||
} else if (systems.includes(fileSystem)) {
|
} else if (systems.includes(fileSystem)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -444,7 +446,7 @@ export default {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="(value.isNew && isLonghornV1) || isCorrupted"
|
v-if="(value.isNew && isLonghornV1 && !isFormatted) || isCorrupted"
|
||||||
class="col span-6"
|
class="col span-6"
|
||||||
>
|
>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
@@ -457,6 +459,15 @@ export default {
|
|||||||
:disabled="forceFormattedDisabled"
|
:disabled="forceFormattedDisabled"
|
||||||
tooltip-key="harvester.host.disk.forceFormatted.toolTip"
|
tooltip-key="harvester.host.disk.forceFormatted.toolTip"
|
||||||
>
|
>
|
||||||
|
<template #1="{option, listeners}">
|
||||||
|
<RadioButton
|
||||||
|
:label="option.label"
|
||||||
|
:val="option.value"
|
||||||
|
:value="value.forceFormatted"
|
||||||
|
:disabled="forceFormattedDisabled && !value.forceFormatted"
|
||||||
|
v-on="listeners"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,157 +0,0 @@
|
|||||||
<script>
|
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
|
|
||||||
import { HCI } from '../../types';
|
|
||||||
import { DOC } from '../../config/doc-links';
|
|
||||||
|
|
||||||
export const hugepagesTHPEnabledMode = [{
|
|
||||||
label: 'Always',
|
|
||||||
value: 'always',
|
|
||||||
}, {
|
|
||||||
label: 'Madvise',
|
|
||||||
value: 'madvise',
|
|
||||||
}, {
|
|
||||||
label: 'Never',
|
|
||||||
value: 'never',
|
|
||||||
}];
|
|
||||||
|
|
||||||
export const hugepagesTHPShmemEnabledMode = [{
|
|
||||||
label: 'Always',
|
|
||||||
value: 'always',
|
|
||||||
}, {
|
|
||||||
label: 'Within Size',
|
|
||||||
value: 'within_size',
|
|
||||||
}, {
|
|
||||||
label: 'Advise',
|
|
||||||
value: 'advise',
|
|
||||||
}, {
|
|
||||||
label: 'Never',
|
|
||||||
value: 'never',
|
|
||||||
}, {
|
|
||||||
label: 'Deny',
|
|
||||||
value: 'deny',
|
|
||||||
}, {
|
|
||||||
label: 'Force',
|
|
||||||
value: 'force',
|
|
||||||
}];
|
|
||||||
|
|
||||||
export const hugepagesTHPDefragMode = [{
|
|
||||||
label: 'Always',
|
|
||||||
value: 'always',
|
|
||||||
}, {
|
|
||||||
label: 'Defer',
|
|
||||||
value: 'defer',
|
|
||||||
}, {
|
|
||||||
label: 'Defer+Madvise',
|
|
||||||
value: 'defer+madvise',
|
|
||||||
}, {
|
|
||||||
label: 'Madvise',
|
|
||||||
value: 'madvise',
|
|
||||||
}, {
|
|
||||||
label: 'Never',
|
|
||||||
value: 'never'
|
|
||||||
}];
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'HarvesterHugepages',
|
|
||||||
components: { LabeledSelect },
|
|
||||||
|
|
||||||
props: {
|
|
||||||
node: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
registerBeforeHook: {
|
|
||||||
type: Function,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
docsTransparentHugepagesLink() {
|
|
||||||
return DOC.TRANSPARENT_HUGEPAGES;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
async saveHugepages() {
|
|
||||||
this.hugepages['spec'] = this.spec;
|
|
||||||
|
|
||||||
await this.hugepages.save().catch((reason) => {
|
|
||||||
if (reason?.type === 'error') {
|
|
||||||
this.$store.dispatch('growl/error', {
|
|
||||||
title: this.t('harvester.notification.title.error'),
|
|
||||||
message: reason?.message,
|
|
||||||
}, { root: true });
|
|
||||||
|
|
||||||
return Promise.reject(new Error('saveHugepages error'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
const hash = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.HUGEPAGES });
|
|
||||||
|
|
||||||
this.hugepages = hash.find((node) => {
|
|
||||||
return node.id === this.node.id;
|
|
||||||
});
|
|
||||||
this.spec = this.hugepages.spec;
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
hugepages: {},
|
|
||||||
spec: { transparent: {} },
|
|
||||||
hugepagesTHPEnabledMode,
|
|
||||||
hugepagesTHPShmemEnabledMode,
|
|
||||||
hugepagesTHPDefragMode,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
this.registerBeforeHook(this.saveHugepages, 'saveHugepages');
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<hr class="divider" />
|
|
||||||
<h3>
|
|
||||||
<t
|
|
||||||
k="harvester.host.hugepages.transparent.title"
|
|
||||||
:raw="true"
|
|
||||||
:url="docsTransparentHugepagesLink"
|
|
||||||
/>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div class="row mb-20">
|
|
||||||
<div class="col span-4">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="spec.transparent.enabled"
|
|
||||||
:label="t('harvester.host.hugepages.transparent.enabled')"
|
|
||||||
:options="hugepagesTHPEnabledMode"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col span-4">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="spec.transparent.shmemEnabled"
|
|
||||||
:label="t('harvester.host.hugepages.transparent.shmemEnabled')"
|
|
||||||
:options="hugepagesTHPShmemEnabledMode"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col span-4">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="spec.transparent.defrag"
|
|
||||||
:label="t('harvester.host.hugepages.transparent.defrag')"
|
|
||||||
:options="hugepagesTHPDefragMode"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -7,8 +7,6 @@ import { Checkbox } from '@components/Form/Checkbox';
|
|||||||
import { HCI } from '../../types';
|
import { HCI } from '../../types';
|
||||||
import { DOC } from '../../config/doc-links';
|
import { DOC } from '../../config/doc-links';
|
||||||
import { docLink } from '../../utils/feature-flags';
|
import { docLink } from '../../utils/feature-flags';
|
||||||
import isEqual from 'lodash/isEqual';
|
|
||||||
import { clone } from '@shell/utils/object';
|
|
||||||
|
|
||||||
export const ksmtunedMode = [{
|
export const ksmtunedMode = [{
|
||||||
value: 'standard',
|
value: 'standard',
|
||||||
@@ -66,13 +64,11 @@ export default {
|
|||||||
|
|
||||||
this.enableMergeAcrossNodes = !!this.ksmtuned.spec?.mergeAcrossNodes;
|
this.enableMergeAcrossNodes = !!this.ksmtuned.spec?.mergeAcrossNodes;
|
||||||
this.spec = this.ksmtuned.spec;
|
this.spec = this.ksmtuned.spec;
|
||||||
this.originSpec = clone(this.ksmtuned.spec);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
ksmtuned: {},
|
ksmtuned: {},
|
||||||
originSpec: {},
|
|
||||||
spec: {},
|
spec: {},
|
||||||
thresCoef: 30,
|
thresCoef: 30,
|
||||||
ksmtunedMode,
|
ksmtunedMode,
|
||||||
@@ -103,11 +99,6 @@ export default {
|
|||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async saveKsmtuned() {
|
async saveKsmtuned() {
|
||||||
// no ksmtuned spec change, skip save
|
|
||||||
if (isEqual(this.spec, this.originSpec)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.spec.mergeAcrossNodes = this.enableMergeAcrossNodes ? 1 : 0;
|
this.spec.mergeAcrossNodes = this.enableMergeAcrossNodes ? 1 : 0;
|
||||||
this.ksmtuned['spec'] = this.spec;
|
this.ksmtuned['spec'] = this.spec;
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import { HCI } from '../../types';
|
|||||||
import HarvesterDisk from './HarvesterDisk';
|
import HarvesterDisk from './HarvesterDisk';
|
||||||
import HarvesterSeeder from './HarvesterSeeder';
|
import HarvesterSeeder from './HarvesterSeeder';
|
||||||
import HarvesterKsmtuned from './HarvesterKsmtuned';
|
import HarvesterKsmtuned from './HarvesterKsmtuned';
|
||||||
import HarvesterHugepages from './HarvesterHugepages';
|
|
||||||
import Tags from '../../components/DiskTags';
|
import Tags from '../../components/DiskTags';
|
||||||
import { LVM_DRIVER } from '../../models/harvester/storage.k8s.io.storageclass';
|
import { LVM_DRIVER } from '../../models/harvester/storage.k8s.io.storageclass';
|
||||||
import isEqual from 'lodash/isEqual';
|
import isEqual from 'lodash/isEqual';
|
||||||
@@ -51,7 +50,6 @@ export default {
|
|||||||
ArrayListGrouped,
|
ArrayListGrouped,
|
||||||
HarvesterDisk,
|
HarvesterDisk,
|
||||||
HarvesterKsmtuned,
|
HarvesterKsmtuned,
|
||||||
HarvesterHugepages,
|
|
||||||
ButtonDropdown,
|
ButtonDropdown,
|
||||||
KeyValue,
|
KeyValue,
|
||||||
Banner,
|
Banner,
|
||||||
@@ -227,12 +225,6 @@ export default {
|
|||||||
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.KSTUNED);
|
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.KSTUNED);
|
||||||
},
|
},
|
||||||
|
|
||||||
hasHugepagesSchema() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
return !!this.$store.getters[`${ inStore }/schemaFor`](HCI.HUGEPAGES);
|
|
||||||
},
|
|
||||||
|
|
||||||
hasBlockDevicesSchema() {
|
hasBlockDevicesSchema() {
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
@@ -535,6 +527,7 @@ export default {
|
|||||||
id: this.longhornNode.id,
|
id: this.longhornNode.id,
|
||||||
opt: { force: true },
|
opt: { force: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, '5000'));
|
await new Promise((resolve) => setTimeout(resolve, '5000'));
|
||||||
await retrySave();
|
await retrySave();
|
||||||
} else {
|
} else {
|
||||||
@@ -655,17 +648,6 @@ export default {
|
|||||||
</template>
|
</template>
|
||||||
</ArrayListGrouped>
|
</ArrayListGrouped>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
|
||||||
v-if="hasHugepagesSchema"
|
|
||||||
name="Hugepages"
|
|
||||||
:weight="70"
|
|
||||||
:label="t('harvester.host.tabs.hugepages')"
|
|
||||||
>
|
|
||||||
<HarvesterHugepages
|
|
||||||
:node="value"
|
|
||||||
:register-before-hook="registerBeforeHook"
|
|
||||||
/>
|
|
||||||
</Tab>
|
|
||||||
<Tab
|
<Tab
|
||||||
v-if="hasKsmtunedSchema"
|
v-if="hasKsmtunedSchema"
|
||||||
name="Ksmtuned"
|
name="Ksmtuned"
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ 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,17 +6,14 @@ 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, L2VLAN_MODE } from '../config/types';
|
import { NETWORK_TYPE } from '../config/types';
|
||||||
import { removeObject } from '@shell/utils/array';
|
|
||||||
|
|
||||||
const {
|
const { L2VLAN, UNTAGGED } = NETWORK_TYPE;
|
||||||
L2VLAN, UNTAGGED, OVERLAY, L2TRUNK_VLAN
|
|
||||||
} = NETWORK_TYPE;
|
|
||||||
const { ACCESS, TRUNK } = L2VLAN_MODE;
|
|
||||||
|
|
||||||
const AUTO = 'auto';
|
const AUTO = 'auto';
|
||||||
const MANUAL = 'manual';
|
const MANUAL = 'manual';
|
||||||
@@ -47,17 +44,18 @@ 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 {
|
||||||
config,
|
config,
|
||||||
type,
|
type,
|
||||||
l2VlanMode: this.value.vlanType === L2TRUNK_VLAN ? TRUNK : ACCESS,
|
|
||||||
vlanTrunk: this.parseVlanTrunk(config),
|
|
||||||
layer3Network: {
|
layer3Network: {
|
||||||
mode: layer3Network.mode || AUTO,
|
mode: layer3Network.mode || AUTO,
|
||||||
serverIPAddr: layer3Network.serverIPAddr || '',
|
serverIPAddr: layer3Network.serverIPAddr || '',
|
||||||
@@ -80,30 +78,6 @@ 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'),
|
||||||
@@ -114,18 +88,6 @@ export default {
|
|||||||
}];
|
}];
|
||||||
},
|
},
|
||||||
|
|
||||||
l2VlanTrunkModeFeatureEnabled() {
|
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('l2VlanTrunkMode');
|
|
||||||
},
|
|
||||||
|
|
||||||
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) || [];
|
||||||
@@ -141,56 +103,8 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
l2VlanModeOptions() {
|
networkType() {
|
||||||
return [{
|
return [L2VLAN, UNTAGGED];
|
||||||
label: this.t('harvester.vlanStatus.vlanConfig.l2VlanMode.access'),
|
|
||||||
value: ACCESS,
|
|
||||||
}, {
|
|
||||||
label: this.t('harvester.vlanStatus.vlanConfig.l2VlanMode.trunk'),
|
|
||||||
value: TRUNK,
|
|
||||||
}];
|
|
||||||
},
|
|
||||||
|
|
||||||
networkTypes() {
|
|
||||||
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;
|
|
||||||
},
|
|
||||||
|
|
||||||
isL2VlanTrunkMode() {
|
|
||||||
if (this.isView) {
|
|
||||||
return this.value.vlanType === L2TRUNK_VLAN && this.l2VlanMode === TRUNK;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.type === L2TRUNK_VLAN || (this.l2VlanMode === TRUNK && this.type === L2VLAN);
|
|
||||||
},
|
|
||||||
|
|
||||||
isL2VlanAccessMode() {
|
|
||||||
if (this.isView) {
|
|
||||||
return this.value.vlanType === L2VLAN && this.l2VlanMode === ACCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.type === L2VLAN && this.l2VlanMode === ACCESS;
|
|
||||||
},
|
|
||||||
|
|
||||||
isOverlayNetwork() {
|
|
||||||
if (this.isView) {
|
|
||||||
return this.value.vlanType === OVERLAY;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.type === OVERLAY;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
isUntaggedNetwork() {
|
isUntaggedNetwork() {
|
||||||
@@ -202,102 +116,39 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
|
||||||
type(newType) {
|
|
||||||
if (newType === OVERLAY) { // overlay network configuration
|
|
||||||
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 { // l2vlan or untagged network configuration
|
|
||||||
this.config.type = 'bridge';
|
|
||||||
this.config.promiscMode = true;
|
|
||||||
this.config.ipam = {};
|
|
||||||
this.config.bridge = '';
|
|
||||||
delete this.config.provider;
|
|
||||||
delete this.config.server_socket;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
l2VlanMode(newAccessMode) {
|
|
||||||
if (this.type !== L2VLAN) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newAccessMode === TRUNK) { // trunk mode
|
|
||||||
this.config.vlan = 0;
|
|
||||||
this.config.vlanTrunk = this.vlanTrunk;
|
|
||||||
} else { // access mode
|
|
||||||
delete this.config.vlanTrunk;
|
|
||||||
this.config.vlan = '';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async saveNetwork(buttonCb) {
|
async saveNetwork(buttonCb) {
|
||||||
const errors = [];
|
const errors = [];
|
||||||
|
|
||||||
if (this.isL2VlanNetwork || this.isUntaggedNetwork) {
|
if (!this.config.vlan && !this.isUntaggedNetwork) {
|
||||||
if (this.isL2VlanTrunkMode && this.vlanTrunk.some((trunk) => trunk.minID === '')) {
|
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('tableHeaders.networkVlan') }));
|
||||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.vlanStatus.vlanConfig.vlanTrunk.minId') }));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isL2VlanTrunkMode && this.vlanTrunk.some((trunk) => trunk.maxID === '')) {
|
|
||||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.vlanStatus.vlanConfig.vlanTrunk.maxId') }));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isL2VlanAccessMode && !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 (errors.length > 0) {
|
|
||||||
buttonCb(false);
|
|
||||||
this.errors = errors;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
this.value.setAnnotation(HCI_LABELS_ANNOTATIONS.NETWORK_ROUTE, JSON.stringify(this.layer3Network));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
await this.save(buttonCb);
|
await this.save(buttonCb);
|
||||||
},
|
},
|
||||||
|
|
||||||
parseVlanTrunk(config) {
|
|
||||||
if (config?.vlanTrunk && config?.vlanTrunk?.length > 0) {
|
|
||||||
return config.vlanTrunk;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [{ minID: '', maxID: '' }];
|
|
||||||
},
|
|
||||||
|
|
||||||
removeVlanTrunk(trunk) {
|
|
||||||
removeObject(this.vlanTrunk, trunk);
|
|
||||||
},
|
|
||||||
|
|
||||||
addVlanTrunk() {
|
|
||||||
if (!this.config.vlanTrunk) {
|
|
||||||
this.config.vlanTrunk = [];
|
|
||||||
}
|
|
||||||
this.vlanTrunk.push({ minID: this.minId, maxID: this.maxId });
|
|
||||||
this.config.vlanTrunk = this.vlanTrunk;
|
|
||||||
},
|
|
||||||
|
|
||||||
vlanTrunkChange() {
|
|
||||||
this.config.vlanTrunk = this.vlanTrunk;
|
|
||||||
},
|
|
||||||
|
|
||||||
input(neu) {
|
input(neu) {
|
||||||
if (neu === '') {
|
if (neu === '') {
|
||||||
this.config.vlan = '';
|
this.config.vlan = '';
|
||||||
@@ -318,19 +169,14 @@ 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.config });
|
this.value.spec.config = JSON.stringify({
|
||||||
|
...this.config,
|
||||||
|
bridge: `${ this.config.bridge }-br`,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -344,7 +190,6 @@ 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"
|
||||||
@@ -366,83 +211,14 @@ export default {
|
|||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
v-model:value="type"
|
v-model:value="type"
|
||||||
class="mb-20"
|
class="mb-20"
|
||||||
:options="networkTypes"
|
:options="networkType"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:disabled="isEdit"
|
|
||||||
:label="t('harvester.fields.type')"
|
:label="t('harvester.fields.type')"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LabeledSelect
|
|
||||||
v-if="isL2VlanNetwork && l2VlanTrunkModeFeatureEnabled"
|
|
||||||
v-model:value="l2VlanMode"
|
|
||||||
class="mb-20"
|
|
||||||
:options="l2VlanModeOptions"
|
|
||||||
:mode="mode"
|
|
||||||
:disabled="isEdit"
|
|
||||||
:label="t('harvester.vlanStatus.vlanConfig.l2VlanMode.label')"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<div v-if="isL2VlanTrunkMode && l2VlanTrunkModeFeatureEnabled">
|
|
||||||
<div
|
|
||||||
v-for="(trunk, i) in vlanTrunk"
|
|
||||||
:key="i"
|
|
||||||
>
|
|
||||||
<div class="row mt-10">
|
|
||||||
<div class="col trunk-span">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value.number="trunk.minID"
|
|
||||||
class="mb-20"
|
|
||||||
required
|
|
||||||
placeholder="e.g. 1-4094"
|
|
||||||
:min="1"
|
|
||||||
:max="4094"
|
|
||||||
type="number"
|
|
||||||
:label="t('harvester.vlanStatus.vlanConfig.vlanTrunk.minId')"
|
|
||||||
:mode="mode"
|
|
||||||
@update:value="vlanTrunkChange"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col trunk-span">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value.number="trunk.maxID"
|
|
||||||
class="mb-20"
|
|
||||||
:max="4094"
|
|
||||||
:min="1"
|
|
||||||
placeholder="e.g. 1-4094"
|
|
||||||
required
|
|
||||||
type="number"
|
|
||||||
:label="t('harvester.vlanStatus.vlanConfig.vlanTrunk.maxId')"
|
|
||||||
:mode="mode"
|
|
||||||
@update:value="vlanTrunkChange"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col remove-btn mb-20">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
:disabled="isView || vlanTrunk.length <= 1"
|
|
||||||
:aria-label="t('generic.ariaLabel.remove', {index: i+1})"
|
|
||||||
role="button"
|
|
||||||
class="btn role-link"
|
|
||||||
@click="removeVlanTrunk(trunk)"
|
|
||||||
>
|
|
||||||
{{ t('harvester.fields.remove') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
v-if="isL2VlanTrunkMode"
|
|
||||||
type="button"
|
|
||||||
class="btn btn-sm bg-primary mb-20"
|
|
||||||
:disabled="isView"
|
|
||||||
@click="addVlanTrunk"
|
|
||||||
>
|
|
||||||
{{ t('harvester.vlanStatus.vlanConfig.vlanTrunk.add') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<LabeledInput
|
<LabeledInput
|
||||||
v-if="isL2VlanAccessMode"
|
v-if="!isUntaggedNetwork"
|
||||||
v-model:value.number="config.vlan"
|
v-model:value.number="config.vlan"
|
||||||
class="mb-20"
|
class="mb-20"
|
||||||
required
|
required
|
||||||
@@ -452,20 +228,25 @@ export default {
|
|||||||
:mode="mode"
|
:mode="mode"
|
||||||
@update:value="input"
|
@update:value="input"
|
||||||
/>
|
/>
|
||||||
<LabeledSelect
|
|
||||||
v-if="!isOverlayNetwork"
|
<div class="row">
|
||||||
v-model:value="clusterBridge"
|
<div
|
||||||
class="mb-20"
|
class="col span-12"
|
||||||
:label="t('harvester.network.clusterNetwork.label')"
|
>
|
||||||
required
|
<LabeledSelect
|
||||||
:disabled="isEdit"
|
v-model:value="config.bridge"
|
||||||
:options="clusterNetworkOptions"
|
class="mb-20"
|
||||||
:mode="mode"
|
:label="t('harvester.network.clusterNetwork.label')"
|
||||||
:placeholder="t('harvester.network.clusterNetwork.selectPlaceholder')"
|
required
|
||||||
/>
|
:options="clusterNetworkOptions"
|
||||||
|
:mode="mode"
|
||||||
|
:placeholder="t('harvester.network.clusterNetwork.selectPlaceholder')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
v-if="isL2VlanAccessMode"
|
v-if="!isUntaggedNetwork"
|
||||||
name="layer3Network"
|
name="layer3Network"
|
||||||
:label="t('harvester.network.tabs.layer3Network')"
|
:label="t('harvester.network.tabs.layer3Network')"
|
||||||
:weight="98"
|
:weight="98"
|
||||||
@@ -524,12 +305,3 @@ export default {
|
|||||||
</Tabbed>
|
</Tabbed>
|
||||||
</CruResource>
|
</CruResource>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.remove-btn {
|
|
||||||
align-self: center;
|
|
||||||
}
|
|
||||||
.trunk-span{
|
|
||||||
flex: 5;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import { HCI } from '../types';
|
|||||||
import { sortBy } from '@shell/utils/sort';
|
import { sortBy } from '@shell/utils/sort';
|
||||||
import { BACKUP_TYPE } from '../config/types';
|
import { BACKUP_TYPE } from '../config/types';
|
||||||
import { _EDIT, _CREATE } from '@shell/config/query-params';
|
import { _EDIT, _CREATE } from '@shell/config/query-params';
|
||||||
import { isBackupTargetSettingEmpty, isBackupTargetSettingUnavailable } from '../utils/setting';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CreateVMSchedule',
|
name: 'CreateVMSchedule',
|
||||||
@@ -94,7 +93,7 @@ export default {
|
|||||||
return this.settings.find( (O) => O.id === 'backup-target');
|
return this.settings.find( (O) => O.id === 'backup-target');
|
||||||
},
|
},
|
||||||
isEmptyValue() {
|
isEmptyValue() {
|
||||||
return isBackupTargetSettingEmpty(this.backupTargetResource);
|
return this.getBackupTargetValueIsEmpty(this.backupTargetResource);
|
||||||
},
|
},
|
||||||
canUpdate() {
|
canUpdate() {
|
||||||
return this?.backupTargetResource?.canUpdate;
|
return this?.backupTargetResource?.canUpdate;
|
||||||
@@ -110,7 +109,7 @@ export default {
|
|||||||
!!this.value.spec.maxFailure;
|
!!this.value.spec.maxFailure;
|
||||||
},
|
},
|
||||||
isBackupTargetUnAvailable() {
|
isBackupTargetUnAvailable() {
|
||||||
return this.value.spec.vmbackup.type === BACKUP_TYPE.BACKUP && isBackupTargetSettingUnavailable(this.backupTargetResource);
|
return this.value.spec.vmbackup.type === BACKUP_TYPE.BACKUP && (this.errorMessage || this.isEmptyValue) && this.canUpdate;
|
||||||
},
|
},
|
||||||
vmOptions() {
|
vmOptions() {
|
||||||
const nsVmList = this.$store.getters['harvester/all'](HCI.VM).filter((vm) => vm.metadata.namespace === this.value.metadata.namespace);
|
const nsVmList = this.$store.getters['harvester/all'](HCI.VM).filter((vm) => vm.metadata.namespace === this.value.metadata.namespace);
|
||||||
@@ -167,6 +166,20 @@ export default {
|
|||||||
onTypeChange(newType) {
|
onTypeChange(newType) {
|
||||||
this.value.metadata.name = `svm${ newType }-${ this.value.spec.vmbackup.source.name }`;
|
this.value.metadata.name = `svm${ newType }-${ this.value.spec.vmbackup.source.name }`;
|
||||||
},
|
},
|
||||||
|
getBackupTargetValueIsEmpty(resource) {
|
||||||
|
let out = true;
|
||||||
|
|
||||||
|
if (resource?.value) {
|
||||||
|
try {
|
||||||
|
const valueJson = JSON.parse(resource?.value);
|
||||||
|
|
||||||
|
out = !valueJson.type;
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
validateFailure(count) {
|
validateFailure(count) {
|
||||||
if (this.value.spec.retain && count > this.value.spec.retain) {
|
if (this.value.spec.retain && count > this.value.spec.retain) {
|
||||||
this.value.spec['maxFailure'] = this.value.spec.retain;
|
this.value.spec['maxFailure'] = this.value.spec.retain;
|
||||||
@@ -185,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
|
||||||
@@ -220,7 +233,7 @@ export default {
|
|||||||
:disabled="isEdit || isView"
|
:disabled="isEdit || isView"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
row
|
row
|
||||||
@update:value="onTypeChange"
|
@input="onTypeChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-30">
|
<div class="row mb-30">
|
||||||
|
|||||||
@@ -84,10 +84,6 @@ 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,
|
||||||
@@ -238,7 +234,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,10 +4,8 @@ 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';
|
||||||
|
|
||||||
@@ -17,8 +15,7 @@ export default {
|
|||||||
LabeledInput,
|
LabeledInput,
|
||||||
LabeledSelect,
|
LabeledSelect,
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
TextAreaAutoGrow,
|
TextAreaAutoGrow
|
||||||
UnitInput
|
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [CreateEditView],
|
mixins: [CreateEditView],
|
||||||
@@ -60,13 +57,7 @@ export default {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
setting,
|
setting,
|
||||||
description: isHarvester ? t(
|
description: isHarvester ? t(`advancedSettings.descriptions.harv-${ this.value.id }`) : t(`advancedSettings.descriptions.${ this.value.id }`),
|
||||||
`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,
|
||||||
@@ -137,13 +128,6 @@ 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);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -179,18 +163,6 @@ 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 {};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -205,7 +177,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"
|
||||||
>
|
>
|
||||||
@@ -266,13 +238,6 @@ 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"
|
||||||
@@ -296,9 +261,4 @@ export default {
|
|||||||
:deep() .edit-help code {
|
:deep() .edit-help code {
|
||||||
padding: 1px 5px;
|
padding: 1px 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep(.banner__content.closable) {
|
|
||||||
white-space: normal;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,319 +0,0 @@
|
|||||||
<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,5 +1,4 @@
|
|||||||
<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';
|
||||||
@@ -7,9 +6,11 @@ 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 {
|
||||||
@@ -21,7 +22,6 @@ 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,7 +45,6 @@ export default {
|
|||||||
Tabbed,
|
Tabbed,
|
||||||
Loading,
|
Loading,
|
||||||
Tags,
|
Tags,
|
||||||
CDISettings,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [CreateEditView],
|
mixins: [CreateEditView],
|
||||||
@@ -118,7 +117,6 @@ export default {
|
|||||||
key: '',
|
key: '',
|
||||||
values: [],
|
values: [],
|
||||||
},
|
},
|
||||||
cdiSettings: {},
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -126,10 +124,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) {
|
||||||
@@ -199,14 +197,6 @@ 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: {
|
||||||
@@ -259,10 +249,6 @@ export default {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.formatAllowedTopoloties();
|
this.formatAllowedTopoloties();
|
||||||
|
|
||||||
if (this.shouldShowCDISettingsTab) {
|
|
||||||
this.formatCDISettings();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
formatAllowedTopoloties() {
|
formatAllowedTopoloties() {
|
||||||
@@ -284,42 +270,6 @@ 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -336,7 +286,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"
|
||||||
>
|
>
|
||||||
@@ -454,19 +404,6 @@ 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,7 +17,6 @@ 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';
|
||||||
@@ -111,7 +110,6 @@ export default {
|
|||||||
headers: {},
|
headers: {},
|
||||||
fileUrl: '',
|
fileUrl: '',
|
||||||
file: '',
|
file: '',
|
||||||
HCI_ANNOTATIONS,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -169,18 +167,11 @@ export default {
|
|||||||
|
|
||||||
return filteredStorages
|
return filteredStorages
|
||||||
.map((s) => {
|
.map((s) => {
|
||||||
let label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
const 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
|
|
||||||
};
|
};
|
||||||
}) || [];
|
}) || [];
|
||||||
},
|
},
|
||||||
@@ -604,8 +595,6 @@ 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)"
|
||||||
>
|
>
|
||||||
@@ -622,7 +611,7 @@ export default {
|
|||||||
<input
|
<input
|
||||||
v-else
|
v-else
|
||||||
v-model="row[valueName]"
|
v-model="row[valueName]"
|
||||||
:disabled="isView || row[keyName] === HCI_ANNOTATIONS.IMAGE_DISPLAY_NAME"
|
:disabled="isView"
|
||||||
:type="'text'"
|
:type="'text'"
|
||||||
:placeholder="t('keyValue.valuePlaceholder')"
|
:placeholder="t('keyValue.valuePlaceholder')"
|
||||||
autocorrect="off"
|
autocorrect="off"
|
||||||
|
|||||||
@@ -189,16 +189,6 @@ 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);
|
||||||
@@ -229,7 +219,6 @@ 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"
|
||||||
@@ -257,12 +246,8 @@ 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"
|
||||||
/>
|
/>
|
||||||
@@ -373,26 +358,6 @@ 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')"
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import ResourceTabs from '@shell/components/form/ResourceTabs';
|
|||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||||
import Conditions from '@shell/components/form/Conditions';
|
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import { allHash } from '@shell/utils/promise';
|
import { allHash } from '@shell/utils/promise';
|
||||||
import { get } from '@shell/utils/object';
|
import { get } from '@shell/utils/object';
|
||||||
@@ -24,7 +23,6 @@ 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',
|
||||||
@@ -41,7 +39,6 @@ export default {
|
|||||||
LabeledSelect,
|
LabeledSelect,
|
||||||
LabeledInput,
|
LabeledInput,
|
||||||
NameNsDescription,
|
NameNsDescription,
|
||||||
Conditions
|
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [CreateEditView],
|
mixins: [CreateEditView],
|
||||||
@@ -209,18 +206,11 @@ export default {
|
|||||||
|
|
||||||
storageClassOptions() {
|
storageClassOptions() {
|
||||||
return this.storageClasses.filter((s) => !s.parameters?.backingImage).map((s) => {
|
return this.storageClasses.filter((s) => !s.parameters?.backingImage).map((s) => {
|
||||||
let label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
const 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
|
|
||||||
};
|
};
|
||||||
}) || [];
|
}) || [];
|
||||||
},
|
},
|
||||||
@@ -269,19 +259,7 @@ export default {
|
|||||||
|
|
||||||
rebuildStatus() {
|
rebuildStatus() {
|
||||||
return this.value.longhornEngine?.status?.rebuildStatus;
|
return this.value.longhornEngine?.status?.rebuildStatus;
|
||||||
},
|
}
|
||||||
|
|
||||||
isLHV2VolExpansionFeatureEnabled() {
|
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('lhV2VolExpansion');
|
|
||||||
},
|
|
||||||
|
|
||||||
isResizeDisabled() {
|
|
||||||
return (
|
|
||||||
!this.isLHV2VolExpansionFeatureEnabled &&
|
|
||||||
this.value?.isLonghornV2 &&
|
|
||||||
this.isEdit
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
@@ -326,10 +304,6 @@ 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);
|
||||||
|
|
||||||
@@ -400,12 +374,10 @@ 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)"
|
||||||
/>
|
/>
|
||||||
@@ -477,7 +449,7 @@ export default {
|
|||||||
:output-modifier="true"
|
:output-modifier="true"
|
||||||
:increment="1024"
|
:increment="1024"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:disabled="isResizeDisabled"
|
:disabled="value?.isLonghornV2 && isEdit"
|
||||||
required
|
required
|
||||||
class="mb-20"
|
class="mb-20"
|
||||||
:suffix="GIBIBYTE"
|
:suffix="GIBIBYTE"
|
||||||
@@ -485,7 +457,7 @@ export default {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Banner
|
<Banner
|
||||||
v-if="isResizeDisabled"
|
v-if="value?.isLonghornV2 && isEdit"
|
||||||
color="warning"
|
color="warning"
|
||||||
>
|
>
|
||||||
<span>{{ t('harvester.volume.longhorn.disableResize') }}</span>
|
<span>{{ t('harvester.volume.longhorn.disableResize') }}</span>
|
||||||
@@ -595,15 +567,6 @@ export default {
|
|||||||
:label="t('nameNsDescription.name.label')"
|
:label="t('nameNsDescription.name.label')"
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
|
||||||
v-if="!isCreate"
|
|
||||||
name="conditions"
|
|
||||||
:label="t('harvester.volume.tabs.conditions')"
|
|
||||||
class="bordered-table"
|
|
||||||
:mode="mode"
|
|
||||||
>
|
|
||||||
<Conditions :value="value" />
|
|
||||||
</Tab>
|
|
||||||
</ResourceTabs>
|
</ResourceTabs>
|
||||||
</CruResource>
|
</CruResource>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ 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"
|
||||||
|
|||||||
@@ -1,196 +0,0 @@
|
|||||||
<script>
|
|
||||||
import debounce from 'lodash/debounce';
|
|
||||||
import { _EDIT, _VIEW } from '@shell/config/query-params';
|
|
||||||
import { removeAt } from '@shell/utils/array';
|
|
||||||
import { Banner } from '@components/Banner';
|
|
||||||
import InfoBox from '@shell/components/InfoBox';
|
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'AccessControlList',
|
|
||||||
|
|
||||||
emits: ['update:value'],
|
|
||||||
|
|
||||||
components: {
|
|
||||||
Banner, InfoBox, LabeledSelect, LabeledInput
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
|
||||||
value: {
|
|
||||||
type: Array,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
mode: {
|
|
||||||
type: String,
|
|
||||||
default: _EDIT,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
const rows = (this.value || []).map((row) => {
|
|
||||||
return {
|
|
||||||
action: row.action || '',
|
|
||||||
direction: row.direction || '',
|
|
||||||
priority: row.priority || 0,
|
|
||||||
match: row.match || '',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return { rows };
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
isView() {
|
|
||||||
return this.mode === _VIEW;
|
|
||||||
},
|
|
||||||
|
|
||||||
actionOptions() {
|
|
||||||
return [
|
|
||||||
{ label: 'Allow', value: 'allow' },
|
|
||||||
{ label: 'Drop', value: 'drop' },
|
|
||||||
{ label: 'Pass', value: 'pass' },
|
|
||||||
{ label: 'Reject', value: 'reject' },
|
|
||||||
{ label: 'Allow-related', value: 'allow-related' },
|
|
||||||
{ label: 'Allow-stateless', value: 'allow-stateless' },
|
|
||||||
];
|
|
||||||
},
|
|
||||||
directionOptions() {
|
|
||||||
return [
|
|
||||||
{ label: 'To-lport', value: 'to-lport' },
|
|
||||||
{ label: 'From-lport', value: 'from-lport' },
|
|
||||||
];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
this.queueUpdate = debounce(this.update, 100);
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
add() {
|
|
||||||
this.rows.push({
|
|
||||||
action: '',
|
|
||||||
direction: '',
|
|
||||||
priority: this.rows.length,
|
|
||||||
match: '',
|
|
||||||
});
|
|
||||||
this.queueUpdate();
|
|
||||||
},
|
|
||||||
|
|
||||||
removeRule(idx) {
|
|
||||||
removeAt(this.rows, idx);
|
|
||||||
this.queueUpdate();
|
|
||||||
},
|
|
||||||
|
|
||||||
update() {
|
|
||||||
if (this.isView) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$emit('update:value', this.rows);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<Banner
|
|
||||||
v-if="rows.length > 0"
|
|
||||||
color="info"
|
|
||||||
>
|
|
||||||
<t
|
|
||||||
k="harvester.subnet.acl.banner"
|
|
||||||
:raw="true"
|
|
||||||
/>
|
|
||||||
</Banner>
|
|
||||||
<div
|
|
||||||
v-for="(row, idx) in rows"
|
|
||||||
:key="idx"
|
|
||||||
>
|
|
||||||
<InfoBox class="box">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="role-link btn btn-sm removeBtn"
|
|
||||||
:disabled="isView"
|
|
||||||
@click="removeRule(idx)"
|
|
||||||
>
|
|
||||||
<i class="icon icon-x" />
|
|
||||||
</button>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col span-4">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="row.action"
|
|
||||||
:mode="mode"
|
|
||||||
class="mt-5 ml-5"
|
|
||||||
:options="actionOptions"
|
|
||||||
:required="true"
|
|
||||||
:label="t('harvester.subnet.acl.action.label')"
|
|
||||||
:placeholder="t('harvester.subnet.acl.action.placeholder')"
|
|
||||||
@update:value="update"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col span-4">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="row.direction"
|
|
||||||
:mode="mode"
|
|
||||||
:options="directionOptions"
|
|
||||||
class="mt-5"
|
|
||||||
:required="true"
|
|
||||||
:label="t('harvester.subnet.acl.direction.label')"
|
|
||||||
:placeholder="t('harvester.subnet.acl.direction.placeholder')"
|
|
||||||
@update:value="update"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col span-3">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value.number="row.priority"
|
|
||||||
type="number"
|
|
||||||
class="mb-20 mt-5"
|
|
||||||
:max="32767"
|
|
||||||
:min="0"
|
|
||||||
:mode="mode"
|
|
||||||
required
|
|
||||||
label-key="harvester.subnet.acl.priority.label"
|
|
||||||
@update:value="update"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col span-11">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="row.match"
|
|
||||||
class="mb-5 ml-5"
|
|
||||||
:mode="mode"
|
|
||||||
required
|
|
||||||
:placeholder="t('harvester.subnet.acl.match.placeholder')"
|
|
||||||
label-key="harvester.subnet.acl.match.label"
|
|
||||||
@update:value="update"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</InfoBox>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn role-tertiary add"
|
|
||||||
:disabled="isView"
|
|
||||||
@click="add()"
|
|
||||||
>
|
|
||||||
<t k="harvester.subnet.acl.addRule" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.box {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.removeBtn {
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
right: 10px;
|
|
||||||
padding: 0px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,399 +0,0 @@
|
|||||||
<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';
|
|
||||||
import { Banner } from '@components/Banner';
|
|
||||||
import AccessControlList from './AccessControlList';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'EditSubnet',
|
|
||||||
|
|
||||||
emits: ['update:value'],
|
|
||||||
|
|
||||||
components: {
|
|
||||||
Banner,
|
|
||||||
CruResource,
|
|
||||||
LabeledInput,
|
|
||||||
LabeledSelect,
|
|
||||||
NameNsDescription,
|
|
||||||
Tab,
|
|
||||||
RadioGroup,
|
|
||||||
ArrayList,
|
|
||||||
ResourceTabs,
|
|
||||||
Loading,
|
|
||||||
AccessControlList
|
|
||||||
},
|
|
||||||
|
|
||||||
mixins: [CreateEditView],
|
|
||||||
|
|
||||||
inheritAttrs: false,
|
|
||||||
|
|
||||||
created() {
|
|
||||||
const vpc = this.$route.query.vpc || '';
|
|
||||||
const enableDHCP = this.value?.spec?.enableDHCP || false;
|
|
||||||
|
|
||||||
set(this.value.spec, 'enableDHCP', enableDHCP);
|
|
||||||
set(this.value, 'spec', this.value.spec || {
|
|
||||||
cidrBlock: '',
|
|
||||||
protocol: NETWORK_PROTOCOL.IPv4,
|
|
||||||
provider: '',
|
|
||||||
vpc,
|
|
||||||
gatewayIP: '',
|
|
||||||
excludeIps: [],
|
|
||||||
private: false,
|
|
||||||
enableDHCP,
|
|
||||||
acls: []
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
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,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
'value.spec.enableDHCP': {
|
|
||||||
handler(newValue) {
|
|
||||||
if (newValue === false) {
|
|
||||||
this.value.spec.dhcpV4Options = '';
|
|
||||||
this.value.spec.dhcpV6Options = '';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async saveSubnet(buttonCb) {
|
|
||||||
const errors = [];
|
|
||||||
const name = this.value?.metadata?.name;
|
|
||||||
const hasEmptyAcls = this.value?.spec?.acls?.some((acl) => !acl.match || !acl.action || acl.priority === undefined || acl.priority === null);
|
|
||||||
|
|
||||||
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'));
|
|
||||||
} else if (hasEmptyAcls) {
|
|
||||||
errors.push(this.t('harvester.validation.subnet.aclEmptyError'));
|
|
||||||
}
|
|
||||||
|
|
||||||
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.enableDHCP"
|
|
||||||
name="enabled"
|
|
||||||
:options="[true, false]"
|
|
||||||
:label="t('harvester.subnet.dhcp.label')"
|
|
||||||
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
|
||||||
:mode="mode"
|
|
||||||
:tooltip="t('harvester.subnet.dhcp.tooltip')"
|
|
||||||
/>
|
|
||||||
<LabeledInput
|
|
||||||
v-if="value.spec.enableDHCP && value.spec.protocol === 'IPv4'"
|
|
||||||
v-model:value="value.spec.dhcpV4Options"
|
|
||||||
class="mb-20 mt-20"
|
|
||||||
:placeholder="t('harvester.subnet.dhcp.placeholder')"
|
|
||||||
:label="t('harvester.subnet.dhcp.v4Options')"
|
|
||||||
:mode="mode"
|
|
||||||
/>
|
|
||||||
<LabeledInput
|
|
||||||
v-if="value.spec.enableDHCP && value.spec.protocol === 'IPv6'"
|
|
||||||
v-model:value="value.spec.dhcpV6Options"
|
|
||||||
:placeholder="t('harvester.subnet.dhcp.placeholder')"
|
|
||||||
class="mb-20 mt-20"
|
|
||||||
:label="t('harvester.subnet.dhcp.v6Options')"
|
|
||||||
:mode="mode"
|
|
||||||
/>
|
|
||||||
<Banner
|
|
||||||
v-if="value.spec.enableDHCP"
|
|
||||||
color="info"
|
|
||||||
class="dhcpOption-banner"
|
|
||||||
>
|
|
||||||
<t
|
|
||||||
k="harvester.subnet.dhcp.dhcpOptionBanner"
|
|
||||||
:raw="true"
|
|
||||||
/>
|
|
||||||
</Banner>
|
|
||||||
</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.subnet.excludeIPs.placeholder')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</ArrayList>
|
|
||||||
</Tab>
|
|
||||||
<Tab
|
|
||||||
name="ACL"
|
|
||||||
:label="t('harvester.subnet.acl.label')"
|
|
||||||
:weight="-2"
|
|
||||||
class="bordered-table"
|
|
||||||
>
|
|
||||||
<AccessControlList
|
|
||||||
v-model:value="value.spec.acls"
|
|
||||||
:mode="mode"
|
|
||||||
/>
|
|
||||||
</Tab>
|
|
||||||
</ResourceTabs>
|
|
||||||
</CruResource>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.dhcpOption-banner {
|
|
||||||
width: max-content;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
<script>
|
|
||||||
import debounce from 'lodash/debounce';
|
|
||||||
import { _EDIT, _VIEW } from '@shell/config/query-params';
|
|
||||||
import { removeAt } from '@shell/utils/array';
|
|
||||||
import { Banner } from '@components/Banner';
|
|
||||||
import { DOC } from '../../config/doc-links';
|
|
||||||
import { docLink } from '../../utils/feature-flags';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'StaticRoutes',
|
|
||||||
|
|
||||||
emits: ['update:value'],
|
|
||||||
|
|
||||||
components: { Banner },
|
|
||||||
|
|
||||||
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');
|
|
||||||
},
|
|
||||||
|
|
||||||
vpcPeeringExamplesLink() {
|
|
||||||
const version = this.$store.getters['harvester-common/getServerVersion']();
|
|
||||||
|
|
||||||
return docLink(DOC.VPC_CONFIGURATION_EXAMPLES, version);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
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>
|
|
||||||
<Banner color="info">
|
|
||||||
<t
|
|
||||||
k="harvester.vpc.vpcPeerings.infoBanner"
|
|
||||||
:raw="true"
|
|
||||||
:url="vpcPeeringExamplesLink"
|
|
||||||
/>
|
|
||||||
</Banner>
|
|
||||||
<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>
|
|
||||||
@@ -1,187 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
<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,10 +20,6 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: 'create'
|
default: 'create'
|
||||||
},
|
},
|
||||||
osType: {
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
value: {
|
value: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
@@ -68,12 +64,6 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
osType(neu) {
|
|
||||||
if (neu === 'windows') {
|
|
||||||
this.id = '';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
value(neu) {
|
value(neu) {
|
||||||
this.yamlScript = neu;
|
this.yamlScript = neu;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -28,10 +28,6 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
osType: {
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
userScript: {
|
userScript: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
@@ -207,7 +203,6 @@ 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"
|
||||||
@@ -224,7 +219,6 @@ 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,44 +2,23 @@
|
|||||||
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: {
|
components: { UnitInput, InputOrDisplay },
|
||||||
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',
|
||||||
@@ -50,59 +29,21 @@ 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: {
|
||||||
isView() {
|
cupDisplay() {
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -114,161 +55,71 @@ 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: {
|
||||||
hotPlugChanged(neu) {
|
change() {
|
||||||
// If hot plug is enabled, we need to update the maxCpu and maxMemory values
|
let memory = '';
|
||||||
if (neu) {
|
|
||||||
this.maxLocalCpu = this.localCpu ? this.localCpu * this.maxHotplugRatio : null;
|
if (String(this.localMemory).includes('Gi')) {
|
||||||
this.maxLocalMemory = this.localMemory ? `${ parseInt(this.localMemory, 10) * this.maxHotplugRatio }${ GIBIBYTE }` : null;
|
memory = this.localMemory;
|
||||||
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, this.maxLocalCpu, this.maxLocalMemory, neu);
|
|
||||||
} else {
|
} else {
|
||||||
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, '', null, neu);
|
memory = `${ this.localMemory }${ GIBIBYTE }`;
|
||||||
}
|
}
|
||||||
},
|
if (memory.includes('null')) {
|
||||||
changeMemory() {
|
memory = null;
|
||||||
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);
|
this.$emit('updateCpuMemory', this.localCpu, memory);
|
||||||
},
|
|
||||||
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>
|
<div class="row">
|
||||||
<div class="row">
|
<div class="col span-6 mb-10">
|
||||||
<div class="col span-6 mb-10">
|
<InputOrDisplay
|
||||||
<InputOrDisplay
|
name="CPU"
|
||||||
name="CPU"
|
:value="cupDisplay"
|
||||||
:value="cpuDisplay"
|
:mode="mode"
|
||||||
|
>
|
||||||
|
<UnitInput
|
||||||
|
v-model:value="localCpu"
|
||||||
|
label="CPU"
|
||||||
|
suffix="C"
|
||||||
|
:delay="0"
|
||||||
|
required
|
||||||
|
:disabled="disabled"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
class="mb-10"
|
class="mb-20"
|
||||||
>
|
@update:value="change"
|
||||||
<UnitInput
|
/>
|
||||||
v-model:value="localCpu"
|
</InputOrDisplay>
|
||||||
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
|
|
||||||
v-if="isCPUMemoryHotPlugFeatureEnabled"
|
<div class="col span-6 mb-10">
|
||||||
class="row"
|
<InputOrDisplay
|
||||||
>
|
:name="t('harvester.virtualMachine.input.memory')"
|
||||||
<Checkbox
|
:value="memoryDisplay"
|
||||||
v-model:value="localEnableHotPlug"
|
:mode="mode"
|
||||||
class="check"
|
>
|
||||||
type="checkbox"
|
<UnitInput
|
||||||
:label="t('harvester.virtualMachine.hotplug.title')"
|
v-model:value="localMemory"
|
||||||
:disabled="isView"
|
:label="t('harvester.virtualMachine.input.memory')"
|
||||||
@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"
|
||||||
class="mt-20"
|
:input-exponent="3"
|
||||||
>
|
:delay="0"
|
||||||
<UnitInput
|
:increment="1024"
|
||||||
v-model:value="maxLocalCpu"
|
:output-modifier="true"
|
||||||
:label="t('harvester.virtualMachine.input.maxCpu')"
|
:disabled="disabled"
|
||||||
suffix="C"
|
required
|
||||||
:delay="0"
|
:suffix="GIBIBYTE"
|
||||||
:disabled="disabled"
|
class="mb-20"
|
||||||
:mode="mode"
|
@update:value="change"
|
||||||
class="mt-20"
|
/>
|
||||||
@update:value="changeMaxCPUMemory"
|
</InputOrDisplay>
|
||||||
/>
|
|
||||||
</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>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import InfoBox from '@shell/components/InfoBox';
|
import InfoBox from '@shell/components/InfoBox';
|
||||||
|
|
||||||
import { NETWORK_ATTACHMENT } from '@shell/config/types';
|
import { NETWORK_ATTACHMENT } from '@shell/config/types';
|
||||||
import { sortBy } from '@shell/utils/sort';
|
import { sortBy } from '@shell/utils/sort';
|
||||||
import { clone } from '@shell/utils/object';
|
import { clone } from '@shell/utils/object';
|
||||||
@@ -13,13 +14,6 @@ export default {
|
|||||||
components: { InfoBox, Base },
|
components: { InfoBox, Base },
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
vm: {
|
|
||||||
type: Object,
|
|
||||||
default: () => {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
mode: {
|
mode: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'create'
|
default: 'create'
|
||||||
@@ -38,15 +32,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
await this.fetchHotunplugData();
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
nameIdx: 1,
|
rows: this.addKeyId(clone(this.value)),
|
||||||
rows: this.addKeyId(clone(this.value)),
|
nameIdx: 1
|
||||||
hotunpluggableNics: new Set(),
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -75,57 +64,15 @@ export default {
|
|||||||
|
|
||||||
return out;
|
return out;
|
||||||
},
|
},
|
||||||
|
|
||||||
canCheckHotunplug() {
|
|
||||||
return !!this.vm?.actions?.findHotunpluggableNics;
|
|
||||||
},
|
|
||||||
|
|
||||||
vmState() {
|
|
||||||
return this.vm?.stateDisplay;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
value(neu) {
|
value(neu) {
|
||||||
this.rows = this.mergeHotplugData(clone(neu));
|
this.rows = neu;
|
||||||
},
|
},
|
||||||
|
|
||||||
vmState(newState, oldState) {
|
|
||||||
if (newState !== oldState) {
|
|
||||||
this.fetchHotunplugData();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async fetchHotunplugData() {
|
|
||||||
if (!this.canCheckHotunplug) {
|
|
||||||
this.rows = this.mergeHotplugData(clone(this.value));
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const resp = await this.vm.doAction('findHotunpluggableNics');
|
|
||||||
|
|
||||||
this.hotunpluggableNics = new Set(resp?.interfaces || []);
|
|
||||||
} catch (e) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.error('Failed to fetch hot-unpluggable NICs:', e);
|
|
||||||
this.hotunpluggableNics = new Set();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.rows = this.mergeHotplugData(clone(this.value));
|
|
||||||
},
|
|
||||||
|
|
||||||
mergeHotplugData(networks) {
|
|
||||||
return (networks || []).map((network) => ({
|
|
||||||
...network,
|
|
||||||
isHotunpluggable: this.hotunpluggableNics.has(network.name),
|
|
||||||
rowKeyId: network.rowKeyId || randomStr(10)
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
add(type) {
|
add(type) {
|
||||||
const name = this.generateName();
|
const name = this.generateName();
|
||||||
|
|
||||||
@@ -171,11 +118,7 @@ export default {
|
|||||||
|
|
||||||
update() {
|
update() {
|
||||||
this.$emit('update:value', this.rows);
|
this.$emit('update:value', this.rows);
|
||||||
},
|
}
|
||||||
|
|
||||||
unplugNIC(network) {
|
|
||||||
this.vm.unplugNIC(network.name);
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -186,28 +129,17 @@ export default {
|
|||||||
v-for="(row, i) in rows"
|
v-for="(row, i) in rows"
|
||||||
:key="i"
|
:key="i"
|
||||||
>
|
>
|
||||||
<div class="box-title mb-10">
|
<button
|
||||||
<h3>
|
v-if="!isView"
|
||||||
{{ t('harvester.virtualMachine.network.title') }}
|
type="button"
|
||||||
</h3>
|
class="role-link remove-vol"
|
||||||
<button
|
@click="remove(row)"
|
||||||
v-if="!isView"
|
>
|
||||||
type="button"
|
<i class="icon icon-x" />
|
||||||
class="role-link btn btn-sm remove"
|
</button>
|
||||||
@click="remove(row)"
|
|
||||||
>
|
<h3> {{ t('harvester.virtualMachine.network.title') }} </h3>
|
||||||
<i class="icon icon-x" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="vm.hotplugNicFeatureEnabled && row.isHotunpluggable && isView"
|
|
||||||
type="button"
|
|
||||||
class="role-link btn btn-sm remove"
|
|
||||||
:disabled="!canCheckHotunplug"
|
|
||||||
@click="unplugNIC(row)"
|
|
||||||
>
|
|
||||||
{{ t('harvester.virtualMachine.hotUnplug.detachNIC.actionLabel') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<Base
|
<Base
|
||||||
v-model:value="rows[i]"
|
v-model:value="rows[i]"
|
||||||
:rows="rows"
|
:rows="rows"
|
||||||
@@ -230,13 +162,16 @@ export default {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang='scss' scoped>
|
<style lang='scss' scoped>
|
||||||
.box-title{
|
.infoBox{
|
||||||
display: flex;
|
position: relative;
|
||||||
justify-content: space-between;
|
}
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
h3 {
|
.remove-vol {
|
||||||
margin-bottom: 0;
|
position: absolute;
|
||||||
}
|
top: 10px;
|
||||||
|
right: 16px;
|
||||||
|
padding:0px;
|
||||||
|
max-height: 28px;
|
||||||
|
min-height: 28px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -101,7 +101,9 @@ export default {
|
|||||||
...formatted,
|
...formatted,
|
||||||
];
|
];
|
||||||
|
|
||||||
set(this.value.domain.devices, 'hostDevices', devices);
|
if (devices.length > 0) {
|
||||||
|
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 } from '@shell/config/query-params';
|
import { _VIEW, _EDIT } 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,6 +90,9 @@ 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,7 +84,9 @@ export default {
|
|||||||
...formatted,
|
...formatted,
|
||||||
];
|
];
|
||||||
|
|
||||||
set(this.value.domain.devices, 'hostDevices', devices);
|
if (devices.length > 0) {
|
||||||
|
set(this.value.domain.devices, 'hostDevices', devices);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ 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'],
|
||||||
@@ -136,10 +135,6 @@ export default {
|
|||||||
pvcs() {
|
pvcs() {
|
||||||
return this.$store.getters['harvester/all'](PVC) || [];
|
return this.$store.getters['harvester/all'](PVC) || [];
|
||||||
},
|
},
|
||||||
|
|
||||||
isLHV2VolExpansionFeatureEnabled() {
|
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('lhV2VolExpansion');
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
@@ -271,17 +266,7 @@ 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.isLHV2VolExpansionFeatureEnabled) return false;
|
|
||||||
if (this.isCreate || volume.newCreateId) return false;
|
|
||||||
|
|
||||||
const isStopped = this.vm.stateDisplay === OFF;
|
|
||||||
const isLonghornV2 = this.isLonghornV2(volume);
|
|
||||||
|
|
||||||
return !isStopped || isLonghornV2;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -303,57 +288,55 @@ export default {
|
|||||||
v-for="(volume, i) in rows"
|
v-for="(volume, i) in rows"
|
||||||
:key="volume.id"
|
:key="volume.id"
|
||||||
>
|
>
|
||||||
<InfoBox>
|
<InfoBox class="box">
|
||||||
<div class="box-title mb-10">
|
<button
|
||||||
<h3>
|
v-if="!isView"
|
||||||
<span
|
type="button"
|
||||||
v-if="volume.to && isVirtualType"
|
class="role-link btn btn-sm remove"
|
||||||
class="title"
|
@click="removeVolume(volume)"
|
||||||
|
>
|
||||||
|
<i class="icon icon-x" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="volume.hotpluggable && isView"
|
||||||
|
type="button"
|
||||||
|
class="role-link btn remove"
|
||||||
|
@click="unplugVolume(volume)"
|
||||||
|
>
|
||||||
|
{{ t('harvester.virtualMachine.unplug.detachVolume') }}
|
||||||
|
</button>
|
||||||
|
<h3>
|
||||||
|
<span
|
||||||
|
v-if="volume.to && isVirtualType"
|
||||||
|
class="title"
|
||||||
|
>
|
||||||
|
<router-link :to="volume.to">
|
||||||
|
{{ t('harvester.virtualMachine.volume.edit') }} {{ headerFor(volume.source) }}
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<BadgeStateFormatter
|
||||||
|
v-if="volume.pvc"
|
||||||
|
class="ml-10 state"
|
||||||
|
:arbitrary="true"
|
||||||
|
:row="volume.pvc"
|
||||||
|
:value="volume.pvc.state"
|
||||||
|
/>
|
||||||
|
<a
|
||||||
|
v-if="dev && !!volume.pvc && !!volume.pvc.resourceExternalLink"
|
||||||
|
v-clean-tooltip="t(volume.pvc.resourceExternalLink.tipsKey || 'generic.resourceExternalLinkTips')"
|
||||||
|
class="ml-5 resource-external"
|
||||||
|
rel="nofollow noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
:href="volume.pvc.resourceExternalLink.url"
|
||||||
>
|
>
|
||||||
<router-link :to="volume.to">
|
<i class="icon icon-external-link" />
|
||||||
{{ t('harvester.virtualMachine.volume.edit') }} {{ headerFor(volume.source) }}
|
</a>
|
||||||
</router-link>
|
</span>
|
||||||
|
|
||||||
<BadgeStateFormatter
|
<span v-else>
|
||||||
v-if="volume.pvc"
|
{{ headerFor(volume.source, !!volume?.volumeBackups) }}
|
||||||
class="ml-10 state"
|
</span>
|
||||||
:arbitrary="true"
|
</h3>
|
||||||
:row="volume.pvc"
|
|
||||||
:value="volume.pvc.state"
|
|
||||||
/>
|
|
||||||
<a
|
|
||||||
v-if="dev && !!volume.pvc && !!volume.pvc.resourceExternalLink"
|
|
||||||
v-clean-tooltip="t(volume.pvc.resourceExternalLink.tipsKey || 'generic.resourceExternalLinkTips')"
|
|
||||||
class="ml-5 resource-external"
|
|
||||||
rel="nofollow noopener noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
:href="volume.pvc.resourceExternalLink.url"
|
|
||||||
>
|
|
||||||
<i class="icon icon-external-link" />
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span v-else>
|
|
||||||
{{ headerFor(volume.source, !!volume?.volumeBackups) }}
|
|
||||||
</span>
|
|
||||||
</h3>
|
|
||||||
<button
|
|
||||||
v-if="!isView"
|
|
||||||
type="button"
|
|
||||||
class="role-link btn btn-sm remove"
|
|
||||||
@click="removeVolume(volume)"
|
|
||||||
>
|
|
||||||
<i class="icon icon-x" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="volume.hotpluggable && isView"
|
|
||||||
type="button"
|
|
||||||
class="role-link btn btn-sm remove"
|
|
||||||
@click="unplugVolume(volume)"
|
|
||||||
>
|
|
||||||
{{ t('harvester.virtualMachine.hotUnplug.detachVolume.actionLabel') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<component
|
<component
|
||||||
:is="componentFor(volume.source)"
|
:is="componentFor(volume.source)"
|
||||||
@@ -364,7 +347,6 @@ 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"
|
||||||
@@ -412,7 +394,7 @@ export default {
|
|||||||
:label="ucFirst(value.volumeBackups.error.message)"
|
:label="ucFirst(value.volumeBackups.error.message)"
|
||||||
/>
|
/>
|
||||||
<Banner
|
<Banner
|
||||||
v-if="!isLHV2VolExpansionFeatureEnabled && isLonghornV2(volume) && !isView"
|
v-if="isLonghornV2(volume) && !isView"
|
||||||
color="warning"
|
color="warning"
|
||||||
:label="t('harvester.volume.longhorn.disableResize')"
|
:label="t('harvester.volume.longhorn.disableResize')"
|
||||||
/>
|
/>
|
||||||
@@ -497,24 +479,25 @@ export default {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang='scss' scoped>
|
<style lang='scss' scoped>
|
||||||
.box-title {
|
.box {
|
||||||
display: flex;
|
position: relative;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.state {
|
.state {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.remove {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
.bootOrder {
|
.bootOrder {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
+7
-7
@@ -99,6 +99,7 @@ export default {
|
|||||||
this.allPVCs
|
this.allPVCs
|
||||||
.filter( (pvc) => {
|
.filter( (pvc) => {
|
||||||
let isAvailable = true;
|
let isAvailable = true;
|
||||||
|
let isBeingUsed = false;
|
||||||
|
|
||||||
this.rows.forEach( (O) => {
|
this.rows.forEach( (O) => {
|
||||||
if (O.volumeName === pvc.metadata.name) {
|
if (O.volumeName === pvc.metadata.name) {
|
||||||
@@ -110,16 +111,17 @@ export default {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// already used as image volume
|
if (pvc.metadata?.annotations?.[HCI_ANNOTATIONS.GOLDEN_IMAGE] === 'true') {
|
||||||
if (this.idx > 0 && pvc.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_ID]) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pvc.isGoldenImageVolume) {
|
if (pvc.attachVM && isAvailable && pvc.attachVM?.id === this.vm?.id && this.isEdit) {
|
||||||
return false;
|
isBeingUsed = false;
|
||||||
|
} else if (pvc.attachVM) {
|
||||||
|
isBeingUsed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return isAvailable && pvc.isAvailable;
|
return isAvailable && !isBeingUsed && pvc.isAvailable;
|
||||||
})
|
})
|
||||||
.map((pvc) => {
|
.map((pvc) => {
|
||||||
return {
|
return {
|
||||||
@@ -140,12 +142,10 @@ 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) {
|
||||||
|
|||||||
+12
-26
@@ -8,7 +8,6 @@ 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';
|
||||||
@@ -64,19 +63,13 @@ 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: [],
|
||||||
@@ -97,23 +90,12 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
imagesOption() {
|
imagesOption() {
|
||||||
return this.images
|
return this.images.filter((c) => c.isReady).sort((a, b) => a.creationTimestamp > b.creationTimestamp ? -1 : 1).map( (I) => {
|
||||||
.filter((image) => {
|
return {
|
||||||
if (!image.isReady) return false;
|
label: this.imageOptionLabel(I),
|
||||||
|
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() {
|
||||||
@@ -140,6 +122,10 @@ 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);
|
||||||
},
|
},
|
||||||
@@ -351,7 +337,7 @@ export default {
|
|||||||
:label="t('harvester.fields.size')"
|
:label="t('harvester.fields.size')"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:required="validateRequired"
|
:required="validateRequired"
|
||||||
:disabled="isResizeDisabled"
|
:disable="isLonghornV2"
|
||||||
:suffix="GIBIBYTE"
|
:suffix="GIBIBYTE"
|
||||||
@update:value="update"
|
@update:value="update"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ 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 {
|
||||||
@@ -56,12 +55,7 @@ export default {
|
|||||||
isVirtualType: {
|
isVirtualType: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
},
|
}
|
||||||
|
|
||||||
isResizeDisabled: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
@@ -124,22 +118,19 @@ export default {
|
|||||||
|
|
||||||
storageClassOptions() {
|
storageClassOptions() {
|
||||||
return this.storageClasses.filter((s) => !s.parameters?.backingImage).map((s) => {
|
return this.storageClasses.filter((s) => !s.parameters?.backingImage).map((s) => {
|
||||||
let label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
const 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) || {};
|
||||||
|
|
||||||
@@ -311,7 +302,7 @@ export default {
|
|||||||
:mode="mode"
|
:mode="mode"
|
||||||
:required="validateRequired"
|
:required="validateRequired"
|
||||||
:label="t('harvester.fields.size')"
|
:label="t('harvester.fields.size')"
|
||||||
:disabled="isResizeDisabled"
|
:disabled="isLonghornV2"
|
||||||
:suffix="GIBIBYTE"
|
:suffix="GIBIBYTE"
|
||||||
@update:value="update"
|
@update:value="update"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ 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';
|
||||||
@@ -96,6 +97,7 @@ export default {
|
|||||||
templateVersionId: '',
|
templateVersionId: '',
|
||||||
namePrefix: '',
|
namePrefix: '',
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
|
useTemplate: false,
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
hostname,
|
hostname,
|
||||||
isRestartImmediately,
|
isRestartImmediately,
|
||||||
@@ -113,11 +115,13 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
machineTypeOptions() {
|
machineTypeOptions() {
|
||||||
return this.machineTypes.map((type) => {
|
return [{
|
||||||
if (!type) return { label: 'None', value: '' };
|
label: 'None',
|
||||||
|
value: ''
|
||||||
return { label: type, value: type };
|
}, {
|
||||||
});
|
label: 'q35',
|
||||||
|
value: 'q35'
|
||||||
|
}];
|
||||||
},
|
},
|
||||||
|
|
||||||
templateOptions() {
|
templateOptions() {
|
||||||
@@ -236,9 +240,6 @@ 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;
|
||||||
@@ -246,6 +247,7 @@ 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;
|
||||||
@@ -256,17 +258,6 @@ 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
|
||||||
});
|
});
|
||||||
@@ -337,15 +328,6 @@ 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 {
|
||||||
@@ -353,18 +335,6 @@ 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;
|
||||||
@@ -433,44 +403,29 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_compareDisksIgnoreStorage(disksA = [], disksB = []) {
|
|
||||||
if (disksA.length !== disksB.length) return false;
|
|
||||||
|
|
||||||
const sortedA = [...disksA].sort((a, b) => a.metadata.name.localeCompare(b.metadata.name));
|
|
||||||
const sortedB = [...disksB].sort((a, b) => a.metadata.name.localeCompare(b.metadata.name));
|
|
||||||
|
|
||||||
for (let i = 0; i < sortedA.length; i++) {
|
|
||||||
const cloneA = clone(sortedA[i]);
|
|
||||||
const cloneB = clone(sortedB[i]);
|
|
||||||
|
|
||||||
// remove the storage field before comparison
|
|
||||||
delete cloneA?.spec?.resources?.requests?.storage;
|
|
||||||
delete cloneB?.spec?.resources?.requests?.storage;
|
|
||||||
|
|
||||||
if (!isEqual(cloneA, cloneB)) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
_shouldPromptRestart(oldVM, newVM) {
|
|
||||||
const specChanged = !isEqual(oldVM?.spec, newVM?.spec);
|
|
||||||
|
|
||||||
// check if the disk defined in annotations has changed, ignoring storage size
|
|
||||||
const oldDisks = parseVolumeClaimTemplates(oldVM);
|
|
||||||
const newDisks = parseVolumeClaimTemplates(newVM);
|
|
||||||
const diskChanged = !this._compareDisksIgnoreStorage(oldDisks, newDisks);
|
|
||||||
|
|
||||||
return specChanged || diskChanged;
|
|
||||||
},
|
|
||||||
|
|
||||||
restartVM() {
|
restartVM() {
|
||||||
if (this.mode !== 'edit' || !this.value.isRunning) return;
|
if (this.mode !== 'edit') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.value.isRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cloneDeepNewVM = clone(this.value);
|
||||||
|
|
||||||
const needsRestart = this._shouldPromptRestart(this.cloneVM, this.value);
|
delete cloneDeepNewVM?.metadata;
|
||||||
|
delete cloneDeepNewVM?.__clone;
|
||||||
|
|
||||||
// if no restart is needed (either no changes or only size changed)
|
delete this.cloneVM?.metadata;
|
||||||
if (!needsRestart) return;
|
delete this.cloneVM?.__clone;
|
||||||
|
|
||||||
|
const oldVM = JSON.parse(JSON.stringify(this.cloneVM));
|
||||||
|
const newVM = JSON.parse(JSON.stringify(cloneDeepNewVM));
|
||||||
|
|
||||||
|
// we won't show restart dialog in yaml page as we don't have a way to detect change in yaml editor.
|
||||||
|
// only check VM is changed in form page.
|
||||||
|
if (isEqual(oldVM, newVM)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
this.isOpen = true;
|
this.isOpen = true;
|
||||||
@@ -489,7 +444,6 @@ export default {
|
|||||||
if (this.isSingle) {
|
if (this.isSingle) {
|
||||||
if (!this.value.spec.template.spec.hostname) {
|
if (!this.value.spec.template.spec.hostname) {
|
||||||
this.value.spec.template.spec['hostname'] = this.value.metadata.name;
|
this.value.spec.template.spec['hostname'] = this.value.metadata.name;
|
||||||
this.spec.template.spec['hostname'] = this.value.metadata.name;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -531,7 +485,6 @@ 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);
|
||||||
|
|
||||||
@@ -545,7 +498,6 @@ 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"
|
||||||
@@ -557,7 +509,6 @@ 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"
|
||||||
@@ -630,13 +581,9 @@ 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"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -804,31 +751,10 @@ 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="-12"
|
:weight="-11"
|
||||||
>
|
>
|
||||||
<div class="row mb-20">
|
<div class="row mb-20">
|
||||||
<div class="col span-6">
|
<div class="col span-6">
|
||||||
@@ -898,10 +824,7 @@ export default {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="col span-6">
|
||||||
v-if="!value.vmMachineTypeAutoFeatureEnabled"
|
|
||||||
class="col span-6"
|
|
||||||
>
|
|
||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
v-model:value="machineType"
|
v-model:value="machineType"
|
||||||
label-key="harvester.virtualMachine.input.MachineType"
|
label-key="harvester.virtualMachine.input.MachineType"
|
||||||
@@ -929,7 +852,6 @@ 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"
|
||||||
|
|||||||
@@ -39,10 +39,6 @@ export default {
|
|||||||
return this.mode === _VIEW;
|
return this.mode === _VIEW;
|
||||||
},
|
},
|
||||||
|
|
||||||
disableAdd() {
|
|
||||||
return this.isStandaloneHarvester && this.rows.some((row) => row.namespace === '*');
|
|
||||||
},
|
|
||||||
|
|
||||||
showAdd() {
|
showAdd() {
|
||||||
return !this.isView;
|
return !this.isView;
|
||||||
},
|
},
|
||||||
@@ -145,7 +141,6 @@ export default {
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
:disabled="disableAdd"
|
|
||||||
class="btn role-tertiary add"
|
class="btn role-tertiary add"
|
||||||
@click="add()"
|
@click="add()"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -66,26 +66,17 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
namespaceOptions() {
|
namespaceOptions() {
|
||||||
const namespaces = this.filteredNamespaces;
|
const out = (this.filteredNamespaces || []).map((namespace) => {
|
||||||
const selected = this.rows.map((r) => r?.namespace);
|
return {
|
||||||
|
label: namespace.metadata.name,
|
||||||
if (this.isStandaloneHarvester) {
|
value: namespace.id,
|
||||||
return [
|
};
|
||||||
{ label: this.t('generic.all'), value: '*' },
|
});
|
||||||
...namespaces.map((ns) => ({
|
|
||||||
label: ns.metadata.name,
|
|
||||||
value: ns.id,
|
|
||||||
disabled: selected.includes(ns.id) && ns.id !== this.row.namespace
|
|
||||||
}))
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [{
|
return [{
|
||||||
label: this.t('generic.all'),
|
label: this.t('generic.all'),
|
||||||
value: '*',
|
value: '*',
|
||||||
},
|
}, ...out];
|
||||||
...namespaces.map((ns) => ({ label: ns.metadata.name, value: ns.id }))
|
|
||||||
];
|
|
||||||
},
|
},
|
||||||
|
|
||||||
guestClusterOptions() {
|
guestClusterOptions() {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user