mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-03 20:56:06 +08:00
Compare commits
70
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d13ea96ed1 | ||
|
|
cd43e61163 | ||
|
|
ca83f9e685 | ||
|
|
cdfdd44346 | ||
|
|
7fe4154ad3 | ||
|
|
fb738dca78 | ||
|
|
4ef7e14033 | ||
|
|
e277e32629 | ||
|
|
692b97e2a7 | ||
|
|
6cfc93280f | ||
|
|
ab4d43cc27 | ||
|
|
7fcf2bbd84 | ||
|
|
ff442b2d2e | ||
|
|
314b75e790 | ||
|
|
71379c563d | ||
|
|
97d09d0f6c | ||
|
|
54c0011fb5 | ||
|
|
0e90637815 | ||
|
|
21bd6405cf | ||
|
|
b8b4ee4c41 | ||
|
|
021d6f673f | ||
|
|
a849671034 | ||
|
|
b98f878c5d | ||
|
|
78482285b3 | ||
|
|
4656976b91 | ||
|
|
aeb3588b83 | ||
|
|
d14b0cb95c | ||
|
|
2f0884d7bd | ||
|
|
037ce6132b | ||
|
|
47b54b4f06 | ||
|
|
9dd8ae479a | ||
|
|
5682bf4c3a | ||
|
|
960df31667 | ||
|
|
63451e7d2b | ||
|
|
55aaa8440d | ||
|
|
da4121da69 | ||
|
|
30e49a6f2c | ||
|
|
2255bd4f69 | ||
|
|
7e5ee04637 | ||
|
|
8863de8874 | ||
|
|
73952a6b4e | ||
|
|
0496fae623 | ||
|
|
d5cd97aafd | ||
|
|
31c6a794be | ||
|
|
9d9383ba90 | ||
|
|
1e02dbffe3 | ||
|
|
b0609020cc | ||
|
|
feca476e36 | ||
|
|
e6e36e94f1 | ||
|
|
54d48ce396 | ||
|
|
404395c7d9 | ||
|
|
dc2437db99 | ||
|
|
e8c8a54b9f | ||
|
|
f3e4a8afe8 | ||
|
|
e73c62739f | ||
|
|
696af4e04f | ||
|
|
009f384a73 | ||
|
|
37766454bd | ||
|
|
7943913ef7 | ||
|
|
56a0fefd40 | ||
|
|
ff1d5925a8 | ||
|
|
63b5b980b7 | ||
|
|
1f7fd1246f | ||
|
|
34a25e9fc8 | ||
|
|
fb8d81d953 | ||
|
|
ba810a52b2 | ||
|
|
3f4f30ba0b | ||
|
|
b428aaecf8 | ||
|
|
6b07a03451 | ||
|
|
2c756582e3 |
@@ -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
|
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ description: Setup node and install dependencies
|
|||||||
runs:
|
runs:
|
||||||
using: 'composite'
|
using: 'composite'
|
||||||
steps:
|
steps:
|
||||||
- name: Setup Nodejs with yarn caching
|
- uses: actions/setup-node@v4
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
|
|||||||
+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,49 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
|
||||||
"extends": [
|
|
||||||
"config:recommended",
|
|
||||||
":semanticCommitTypeAll(deps)",
|
|
||||||
":semanticCommitScopeDisabled"
|
|
||||||
],
|
|
||||||
"baseBranches": [
|
|
||||||
"main",
|
|
||||||
"release-harvester-v1.7",
|
|
||||||
"release-harvester-v1.8"
|
|
||||||
],
|
|
||||||
"automergeMajor": false,
|
|
||||||
"semanticCommits": "enabled",
|
|
||||||
"semanticCommitType": "deps",
|
|
||||||
"prHourlyLimit": 12,
|
|
||||||
"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",
|
|
||||||
"minimumReleaseAge": "7 days",
|
|
||||||
"labels": ["minor-update"],
|
|
||||||
"reviewers": ["a110605", "houhoucoop"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"matchUpdateTypes": ["patch", "digest"],
|
|
||||||
"automerge": true,
|
|
||||||
"minimumReleaseAge": "7 days",
|
|
||||||
"groupName": "patch digest dependencies",
|
|
||||||
"labels": ["patch-update", "automerge"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
name: "PR Management Add PR Label Collect Data"
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [opened, reopened, edited]
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- 'release-harvester-v*'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
collect:
|
|
||||||
if: github.event.pull_request.draft == false
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
actions: write
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
|
||||||
with:
|
|
||||||
ref: ${{ github.base_ref }}
|
|
||||||
|
|
||||||
- name: Setup Nodejs and yarn install
|
|
||||||
uses: ./.github/actions/setup
|
|
||||||
|
|
||||||
- name: Extract PR label
|
|
||||||
run: |
|
|
||||||
PR_LABEL=$(node ./scripts/extract-release-label.mjs "${{ github.event.pull_request.title }}")
|
|
||||||
echo "PR_LABEL=$PR_LABEL"
|
|
||||||
{
|
|
||||||
echo "PR_NUMBER=${{ github.event.pull_request.number }}"
|
|
||||||
echo "PR_LABEL=$PR_LABEL"
|
|
||||||
} > pr-add-label-data.env
|
|
||||||
|
|
||||||
- name: Upload PR data artifact
|
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
||||||
with:
|
|
||||||
name: pr-add-label-data
|
|
||||||
path: pr-add-label-data.env
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
name: "PR Management Add PR Label"
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_run:
|
|
||||||
workflows:
|
|
||||||
- "PR Management Add PR Label Collect Data"
|
|
||||||
types: [completed]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
auto-assign-pr-label:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
pull-requests: write
|
|
||||||
steps:
|
|
||||||
- name: Download PR data artifact
|
|
||||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
|
||||||
with:
|
|
||||||
name: pr-add-label-data
|
|
||||||
run-id: ${{ github.event.workflow_run.id }}
|
|
||||||
github-token: ${{ github.token }}
|
|
||||||
|
|
||||||
- name: Load PR data
|
|
||||||
run: |
|
|
||||||
cat pr-add-label-data.env >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Set PR label
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
run: |
|
|
||||||
echo "PR_LABEL = $PR_LABEL"
|
|
||||||
gh pr edit "$PR_NUMBER" --repo "${{ github.repository }}" --add-label "$PR_LABEL"
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
name: "PR Management Auto Assign Collect Data"
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [opened, ready_for_review]
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- 'release-harvester-v*'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
collect:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
actions: write
|
|
||||||
steps:
|
|
||||||
- name: Save PR data to artifact
|
|
||||||
run: |
|
|
||||||
{
|
|
||||||
echo "PR_NUMBER=${{ github.event.pull_request.number }}"
|
|
||||||
echo "PR_AUTHOR=${{ github.event.pull_request.user.login }}"
|
|
||||||
} > pr-auto-assign-data.env
|
|
||||||
|
|
||||||
- name: Upload PR data artifact
|
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
||||||
with:
|
|
||||||
name: pr-auto-assign-data
|
|
||||||
path: pr-auto-assign-data.env
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
name: "PR Management Auto Assign"
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_run:
|
|
||||||
workflows:
|
|
||||||
- "PR Management Auto Assign Collect Data"
|
|
||||||
types: [completed]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
auto-assign:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
|
||||||
env:
|
|
||||||
REVIEWERS: "a110605,houhoucoop"
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
pull-requests: write
|
|
||||||
steps:
|
|
||||||
- name: Download PR data artifact
|
|
||||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
|
||||||
with:
|
|
||||||
name: pr-auto-assign-data
|
|
||||||
run-id: ${{ github.event.workflow_run.id }}
|
|
||||||
github-token: ${{ github.token }}
|
|
||||||
|
|
||||||
- name: Load PR data
|
|
||||||
run: |
|
|
||||||
cat pr-auto-assign-data.env >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Auto assign PR author
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
run: |
|
|
||||||
echo "Assigning PR author: $PR_AUTHOR"
|
|
||||||
gh pr edit "$PR_NUMBER" --repo "${{ github.repository }}" --add-assignee "$PR_AUTHOR" --add-reviewer "$REVIEWERS"
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
name: "PR Management Add Labels Collect Data"
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [opened, reopened]
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- 'release-harvester-v*'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
collect:
|
|
||||||
if: github.event.pull_request.draft == false
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
actions: write
|
|
||||||
steps:
|
|
||||||
- name: Save PR data to artifact
|
|
||||||
run: |
|
|
||||||
{
|
|
||||||
echo "PR_NUMBER=${{ github.event.pull_request.number }}"
|
|
||||||
echo "PR_BASE_REF=${{ github.event.pull_request.base.ref }}"
|
|
||||||
echo "PR_USER_LOGIN=${{ github.event.pull_request.user.login }}"
|
|
||||||
} > pr-backport-label-data.env
|
|
||||||
|
|
||||||
- name: Upload PR data artifact
|
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
||||||
with:
|
|
||||||
name: pr-backport-label-data
|
|
||||||
path: pr-backport-label-data.env
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
name: "PR Management Add Backport Labels"
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_run:
|
|
||||||
workflows:
|
|
||||||
- "PR Management Add Labels Collect Data"
|
|
||||||
types: [completed]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
add-require-backport-label:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
pull-requests: write
|
|
||||||
steps:
|
|
||||||
- name: Download PR data artifact
|
|
||||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
|
||||||
with:
|
|
||||||
name: pr-backport-label-data
|
|
||||||
run-id: ${{ github.event.workflow_run.id }}
|
|
||||||
github-token: ${{ github.token }}
|
|
||||||
|
|
||||||
- name: Load PR data
|
|
||||||
run: |
|
|
||||||
cat pr-backport-label-data.env >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Add require-backport label (main branch PRs)
|
|
||||||
if: env.PR_BASE_REF == 'main'
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
repo="${{ github.repository }}"
|
|
||||||
pr_number="$PR_NUMBER"
|
|
||||||
|
|
||||||
release_branches=$(gh api "repos/${repo}/branches" --paginate --jq '.[].name' | grep -E '^release-harvester-v[0-9]+\.[0-9]+$' || true)
|
|
||||||
|
|
||||||
if [[ -z "$release_branches" ]]; then
|
|
||||||
echo "No release branches found, skipping."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
latest_branch=$(echo "$release_branches" | sort -Vr | head -n1)
|
|
||||||
version="${latest_branch#release-harvester-v}"
|
|
||||||
release_tag="v${version}.0"
|
|
||||||
|
|
||||||
tags=$(gh api "repos/${repo}/releases" --paginate --jq '.[].tag_name')
|
|
||||||
if echo "$tags" | grep -Fxq "$release_tag"; then
|
|
||||||
echo "Release $release_tag already published, skipping."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
label="require backport/v${version}"
|
|
||||||
pr_labels=$(gh pr view "$pr_number" --repo "$repo" --json labels --jq '.labels[].name' || echo "")
|
|
||||||
|
|
||||||
if echo "$pr_labels" | grep -Fxq "$label"; then
|
|
||||||
echo "Label '$label' already present, skipping."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Adding label: $label"
|
|
||||||
gh pr edit "$pr_number" --repo "$repo" --add-label "$label"
|
|
||||||
|
|
||||||
- name: Add backport label (release branch PRs opened by Mergify)
|
|
||||||
if: startsWith(env.PR_BASE_REF, 'release-harvester-v')
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
IS_MERGIFY=$(echo "$PR_USER_LOGIN" | grep -iq 'mergify' && echo true || echo false)
|
|
||||||
|
|
||||||
if [[ "$IS_MERGIFY" != "true" ]]; then
|
|
||||||
echo "PR author is not Mergify, skipping."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
version="${PR_BASE_REF#release-harvester-v}"
|
|
||||||
label="backport/v${version}"
|
|
||||||
echo "Adding label: $label"
|
|
||||||
gh pr edit "$PR_NUMBER" --repo "${{ github.repository }}" --add-label "$label"
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
name: "PR Management Request Backport via Mergify Collect Data"
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [closed]
|
|
||||||
branches: [main]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
collect:
|
|
||||||
if: github.event.pull_request.merged == true
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
actions: write
|
|
||||||
steps:
|
|
||||||
- name: Save PR data to artifact
|
|
||||||
run: |
|
|
||||||
labels_json='${{ toJson(github.event.pull_request.labels.*.name) }}'
|
|
||||||
{
|
|
||||||
echo "PR_NUMBER=${{ github.event.pull_request.number }}"
|
|
||||||
echo "PR_LABELS=$(echo "$labels_json" | jq -r '[.[]] | join(",")')"
|
|
||||||
} > pr-backport-request-data.env
|
|
||||||
|
|
||||||
- name: Upload PR data artifact
|
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
||||||
with:
|
|
||||||
name: pr-backport-request-data
|
|
||||||
path: pr-backport-request-data.env
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
name: "PR Management Request Backport via Mergify"
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_run:
|
|
||||||
workflows:
|
|
||||||
- "PR Management Request Backport via Mergify Collect Data"
|
|
||||||
types: [completed]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
comment-backport:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
pull-requests: write
|
|
||||||
steps:
|
|
||||||
- name: Download PR data artifact
|
|
||||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
|
||||||
with:
|
|
||||||
name: pr-backport-request-data
|
|
||||||
run-id: ${{ github.event.workflow_run.id }}
|
|
||||||
github-token: ${{ github.token }}
|
|
||||||
|
|
||||||
- name: Load PR data
|
|
||||||
run: |
|
|
||||||
cat pr-backport-request-data.env >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Post Mergify backport command
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
pr_number="$PR_NUMBER"
|
|
||||||
repo="${{ github.repository }}"
|
|
||||||
|
|
||||||
labels=$(echo "$PR_LABELS" | tr ',' '\n')
|
|
||||||
|
|
||||||
echo "Labels on PR: $labels"
|
|
||||||
|
|
||||||
matches=$(echo "$labels" | grep -oE '^require backport/v[0-9]+\.[0-9]+$' || true)
|
|
||||||
|
|
||||||
if [[ -z "$matches" ]]; then
|
|
||||||
echo "No back-port labels found — skipping."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
branches=$(echo "$matches" \
|
|
||||||
| sed -E 's/^require backport\/v/release-harvester-v/' \
|
|
||||||
| sort -u | tr '\n' ' ')
|
|
||||||
branches=${branches%% }
|
|
||||||
|
|
||||||
cmd="@Mergifyio backport $branches"
|
|
||||||
echo "Posting Mergify command: $cmd"
|
|
||||||
gh pr comment "$pr_number" --repo "$repo" --body "$cmd"
|
|
||||||
@@ -11,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@34e114876b0b11c390a56381ad16ebd13914f8d5 # 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
|
||||||
|
|||||||
@@ -25,12 +25,12 @@ jobs:
|
|||||||
name: Build & Upload Hosted
|
name: Build & Upload Hosted
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
# Note - Cannot use the setup action here as it uses a different yarn install arg
|
# Note - Cannot use the setup action here as it uses a different yarn install arg
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
@@ -45,20 +45,19 @@ jobs:
|
|||||||
run: ./scripts/build-upload-gate
|
run: ./scripts/build-upload-gate
|
||||||
|
|
||||||
- name: Get gcs auth
|
- name: Get gcs auth
|
||||||
uses: rancher-eio/read-vault-secrets@d266f55186f80a893839f6e15662e67388e443e6 # v3
|
uses: rancher-eio/read-vault-secrets@main
|
||||||
with:
|
with:
|
||||||
secrets: |
|
secrets: |
|
||||||
secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ;
|
secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ;
|
||||||
|
|
||||||
- name: Apply gcs auth
|
- name: Apply gcs auth
|
||||||
# https://github.com/google-github-actions/auth
|
# https://github.com/google-github-actions/auth
|
||||||
uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2
|
uses: 'google-github-actions/auth@v2'
|
||||||
|
|
||||||
with:
|
with:
|
||||||
credentials_json: "${{ env.GOOGLE_AUTH }}"
|
credentials_json: "${{ env.GOOGLE_AUTH }}"
|
||||||
|
|
||||||
- name: Upload build
|
- name: Upload build
|
||||||
uses: google-github-actions/upload-cloud-storage@c0f6160ff80057923ff50e5e567695cea181ec23 # v2
|
uses: 'google-github-actions/upload-cloud-storage@v2'
|
||||||
# https://github.com/google-github-actions/upload-cloud-storage
|
# https://github.com/google-github-actions/upload-cloud-storage
|
||||||
with:
|
with:
|
||||||
path: ${{steps.build-hosted.outputs.BUILD_HOSTED_DIR}}
|
path: ${{steps.build-hosted.outputs.BUILD_HOSTED_DIR}}
|
||||||
@@ -72,12 +71,12 @@ jobs:
|
|||||||
name: Build & Upload Embedded
|
name: Build & Upload Embedded
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
# Note - Cannot use the setup action here as it uses a different yarn install arg
|
# Note - Cannot use the setup action here as it uses a different yarn install arg
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
@@ -90,19 +89,19 @@ jobs:
|
|||||||
DISABLED_EMBED_PKG: https://releases.rancher.com/harvester-ui/plugin/harvester-1.0.3.tar.gz
|
DISABLED_EMBED_PKG: https://releases.rancher.com/harvester-ui/plugin/harvester-1.0.3.tar.gz
|
||||||
|
|
||||||
- name: Get gcs auth
|
- name: Get gcs auth
|
||||||
uses: rancher-eio/read-vault-secrets@d266f55186f80a893839f6e15662e67388e443e6 # v3
|
uses: rancher-eio/read-vault-secrets@main
|
||||||
with:
|
with:
|
||||||
secrets: |
|
secrets: |
|
||||||
secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ;
|
secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ;
|
||||||
|
|
||||||
- name: Apply gcs auth
|
- name: Apply gcs auth
|
||||||
# https://github.com/google-github-actions/auth
|
# https://github.com/google-github-actions/auth
|
||||||
uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2
|
uses: 'google-github-actions/auth@v2'
|
||||||
with:
|
with:
|
||||||
credentials_json: "${{ env.GOOGLE_AUTH }}"
|
credentials_json: "${{ env.GOOGLE_AUTH }}"
|
||||||
|
|
||||||
- name: Upload tar
|
- name: Upload tar
|
||||||
uses: google-github-actions/upload-cloud-storage@c0f6160ff80057923ff50e5e567695cea181ec23 # v2
|
uses: 'google-github-actions/upload-cloud-storage@v2'
|
||||||
with:
|
with:
|
||||||
path: ${{steps.build-embedded.outputs.BUILD_EMBEDED_TGZ}}
|
path: ${{steps.build-embedded.outputs.BUILD_EMBEDED_TGZ}}
|
||||||
destination: releases.rancher.com/harvester-ui/dashboard
|
destination: releases.rancher.com/harvester-ui/dashboard
|
||||||
@@ -115,12 +114,12 @@ jobs:
|
|||||||
name: Build & Upload Plugin
|
name: Build & Upload Plugin
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
# Note - Cannot use the setup action here as it uses a different yarn install arg
|
# Note - Cannot use the setup action here as it uses a different yarn install arg
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
@@ -134,19 +133,19 @@ jobs:
|
|||||||
run: ./scripts/build-upload-gate
|
run: ./scripts/build-upload-gate
|
||||||
|
|
||||||
- name: Get gcs auth
|
- name: Get gcs auth
|
||||||
uses: rancher-eio/read-vault-secrets@d266f55186f80a893839f6e15662e67388e443e6 # v3
|
uses: rancher-eio/read-vault-secrets@main
|
||||||
with:
|
with:
|
||||||
secrets: |
|
secrets: |
|
||||||
secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ;
|
secret/data/github/repo/${{ github.repository }}/google-auth/harvester/credentials token | GOOGLE_AUTH ;
|
||||||
|
|
||||||
- name: Apply gcs auth
|
- name: Apply gcs auth
|
||||||
# https://github.com/google-github-actions/auth
|
# https://github.com/google-github-actions/auth
|
||||||
uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2
|
uses: 'google-github-actions/auth@v2'
|
||||||
with:
|
with:
|
||||||
credentials_json: "${{ env.GOOGLE_AUTH }}"
|
credentials_json: "${{ env.GOOGLE_AUTH }}"
|
||||||
|
|
||||||
- name: Upload plugin tar
|
- name: Upload plugin tar
|
||||||
uses: google-github-actions/upload-cloud-storage@c0f6160ff80057923ff50e5e567695cea181ec23 # v2
|
uses: 'google-github-actions/upload-cloud-storage@v2'
|
||||||
with:
|
with:
|
||||||
path: dist-pkg/${{steps.ci-build-pkg.outputs.PKG_TARBALL}}
|
path: dist-pkg/${{steps.ci-build-pkg.outputs.PKG_TARBALL}}
|
||||||
destination: releases.rancher.com/harvester-ui/plugin
|
destination: releases.rancher.com/harvester-ui/plugin
|
||||||
@@ -156,7 +155,7 @@ jobs:
|
|||||||
process_gcloudignore: false
|
process_gcloudignore: false
|
||||||
|
|
||||||
- name: Upload plugin directory
|
- name: Upload plugin directory
|
||||||
uses: google-github-actions/upload-cloud-storage@c0f6160ff80057923ff50e5e567695cea181ec23 # v2
|
uses: 'google-github-actions/upload-cloud-storage@v2'
|
||||||
with:
|
with:
|
||||||
path: dist-pkg/${{steps.ci-build-pkg.outputs.PKG_NAME}}
|
path: dist-pkg/${{steps.ci-build-pkg.outputs.PKG_NAME}}
|
||||||
destination: releases.rancher.com/harvester-ui/plugin/${{steps.ci-build-pkg.outputs.PKG_NAME}}
|
destination: releases.rancher.com/harvester-ui/plugin/${{steps.ci-build-pkg.outputs.PKG_NAME}}
|
||||||
|
|||||||
@@ -27,14 +27,14 @@ jobs:
|
|||||||
build-status: ${{ job.status }}
|
build-status: ${{ job.status }}
|
||||||
steps:
|
steps:
|
||||||
- name: Read Secrets
|
- name: Read Secrets
|
||||||
uses: rancher-eio/read-vault-secrets@d266f55186f80a893839f6e15662e67388e443e6 # v3
|
uses: rancher-eio/read-vault-secrets@main
|
||||||
with:
|
with:
|
||||||
secrets: |
|
secrets: |
|
||||||
secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials username | DOCKER_USERNAME ;
|
secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials username | DOCKER_USERNAME ;
|
||||||
secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials password | DOCKER_PASSWORD ;
|
secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials password | DOCKER_PASSWORD ;
|
||||||
|
|
||||||
- name: Checkout repository (normal flow)
|
- name: Checkout repository (normal flow)
|
||||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Enable Corepack
|
- name: Enable Corepack
|
||||||
run: corepack enable
|
run: corepack enable
|
||||||
@@ -45,20 +45,26 @@ jobs:
|
|||||||
git config user.email 'github-actions[bot]@users.noreply.github.com'
|
git config user.email 'github-actions[bot]@users.noreply.github.com'
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ env.DOCKER_USERNAME }}
|
username: ${{ env.DOCKER_USERNAME }}
|
||||||
password: ${{ env.DOCKER_PASSWORD }}
|
password: ${{ env.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
- name: Setup Helm
|
- name: Setup Helm
|
||||||
uses: azure/setup-helm@5119fcb9089d432beecbf79bb2c7915207344b78 # v3
|
uses: azure/setup-helm@v3
|
||||||
with:
|
with:
|
||||||
version: v3.8.0
|
version: v3.8.0
|
||||||
|
|
||||||
- name: Setup Nodejs with yarn install
|
- name: Setup Nodejs with yarn caching
|
||||||
uses: ./.github/actions/setup
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
cache: yarn
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn
|
||||||
|
|
||||||
- name: Build and push UI image
|
- name: Build and push UI image
|
||||||
run: |
|
run: |
|
||||||
publish="yarn publish-pkgs -cp -r ${{ inputs.registry_target }} -o ${{ inputs.registry_user }}"
|
publish="yarn publish-pkgs -cp -r ${{ inputs.registry_target }} -o ${{ inputs.registry_user }}"
|
||||||
$publish
|
$publish
|
||||||
@@ -4,7 +4,6 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- 'release-harvester-v*'
|
- 'release-harvester-v*'
|
||||||
- 'main'
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
setup-target-branch:
|
setup-target-branch:
|
||||||
@@ -13,22 +12,19 @@ jobs:
|
|||||||
target_branch: ${{ steps.get-version.outputs.target_branch }}
|
target_branch: ${{ steps.get-version.outputs.target_branch }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- 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
|
||||||
@@ -44,7 +40,7 @@ jobs:
|
|||||||
version: ${{ steps.get_version.outputs.version }}
|
version: ${{ steps.get_version.outputs.version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Extract version from package.json
|
- name: Extract version from package.json
|
||||||
id: get_version
|
id: get_version
|
||||||
@@ -62,13 +58,7 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Node from .nvmrc
|
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
||||||
with:
|
|
||||||
node-version-file: '.nvmrc'
|
|
||||||
cache: 'yarn'
|
|
||||||
|
|
||||||
- name: Setup environment
|
- name: Setup environment
|
||||||
run: |
|
run: |
|
||||||
@@ -76,7 +66,7 @@ jobs:
|
|||||||
yarn install --frozen-lockfile
|
yarn install --frozen-lockfile
|
||||||
|
|
||||||
- name: Setup Helm
|
- name: Setup Helm
|
||||||
uses: azure/setup-helm@5119fcb9089d432beecbf79bb2c7915207344b78 # v3
|
uses: azure/setup-helm@v3
|
||||||
with:
|
with:
|
||||||
version: v3.8.0
|
version: v3.8.0
|
||||||
|
|
||||||
@@ -85,7 +75,7 @@ jobs:
|
|||||||
yarn publish-pkgs -s ${{ github.repository }} -b ${{ needs.setup-target-branch.outputs.target_branch }} -t harvester-${{ needs.extract-version.outputs.version }}
|
yarn publish-pkgs -s ${{ github.repository }} -b ${{ needs.setup-target-branch.outputs.target_branch }} -t harvester-${{ needs.extract-version.outputs.version }}
|
||||||
|
|
||||||
- name: Upload charts artifact
|
- name: Upload charts artifact
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: charts
|
name: charts
|
||||||
path: tmp
|
path: tmp
|
||||||
@@ -100,7 +90,7 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout release branch
|
- name: Checkout release branch
|
||||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: '${{ github.ref_name }}'
|
ref: '${{ github.ref_name }}'
|
||||||
|
|
||||||
@@ -111,7 +101,7 @@ jobs:
|
|||||||
echo "LAST_COMMIT=${LAST_COMMIT}" >> $GITHUB_ENV
|
echo "LAST_COMMIT=${LAST_COMMIT}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Checkout target branch
|
- name: Checkout target branch
|
||||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: '${{ needs.setup-target-branch.outputs.target_branch }}'
|
ref: '${{ needs.setup-target-branch.outputs.target_branch }}'
|
||||||
|
|
||||||
@@ -127,7 +117,7 @@ jobs:
|
|||||||
git config user.email 'github-actions[bot]@users.noreply.github.com'
|
git config user.email 'github-actions[bot]@users.noreply.github.com'
|
||||||
|
|
||||||
- name: Download build artifacts
|
- name: Download build artifacts
|
||||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: charts
|
name: charts
|
||||||
|
|
||||||
@@ -138,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@cae68fefc6b5f367a0275617c9f83181ba54714f # v1.7.0
|
uses: helm/chart-releaser-action@v1.7.0
|
||||||
with:
|
with:
|
||||||
charts_dir: ./charts
|
charts_dir: ./charts
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ jobs:
|
|||||||
release_tag: ${{ steps.determine_tag.outputs.release_tag }}
|
release_tag: ${{ steps.determine_tag.outputs.release_tag }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Determine release tag
|
- name: Determine release tag
|
||||||
id: determine_tag
|
id: determine_tag
|
||||||
@@ -29,24 +29,25 @@ jobs:
|
|||||||
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@34e114876b0b11c390a56381ad16ebd13914f8d5 # 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@9bf23dff6650116673346447bcf0b088e0edb27f # creators-pkg-v3.0.12
|
uses: rancher/dashboard/.github/workflows/build-extension-charts.yml@master
|
||||||
permissions:
|
permissions:
|
||||||
|
actions: write
|
||||||
contents: write
|
contents: write
|
||||||
pull-requests: write
|
deployments: write
|
||||||
|
pages: write
|
||||||
with:
|
with:
|
||||||
target_branch: gh-pages
|
target_branch: gh-pages
|
||||||
tagged_release: '${{ needs.setup-release-tag.outputs.release_tag }}'
|
tagged_release: '${{ needs.setup-release-tag.outputs.release_tag }}'
|
||||||
|
|||||||
@@ -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@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
|
||||||
- name: Check package version
|
|
||||||
env:
|
|
||||||
TAG_VERSION: ${{github.ref_name}}
|
|
||||||
run: ./scripts/check-version.sh $TAG_VERSION
|
|
||||||
|
|
||||||
build:
|
build:
|
||||||
name: Build and Upload Package
|
name: Build and Upload Package
|
||||||
uses: ./.github/workflows/build-and-publish-standalone.yaml
|
uses: ./.github/workflows/build-and-publish-standalone.yaml
|
||||||
needs: check-version
|
needs:
|
||||||
|
- build-validation
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
name: FOSSA Scanning
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: ["main", "release-harvester-v*"]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
id-token: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
fossa-scanning:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 30
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
||||||
|
|
||||||
# The FOSSA token is shared between all repos in Harvester's GH org. It can
|
|
||||||
# be used directly and there is no need to request specific access to EIO.
|
|
||||||
- name: Read FOSSA token
|
|
||||||
uses: rancher-eio/read-vault-secrets@d266f55186f80a893839f6e15662e67388e443e6 # v3
|
|
||||||
with:
|
|
||||||
secrets: |
|
|
||||||
secret/data/github/org/harvester/fossa/credentials token | FOSSA_API_KEY_PUSH_ONLY
|
|
||||||
|
|
||||||
- name: FOSSA scan
|
|
||||||
uses: fossas/fossa-action@ff70fe9fe17cbd2040648f1c45e8ec4e4884dcf3 # v1.9.0
|
|
||||||
with:
|
|
||||||
api-key: ${{ env.FOSSA_API_KEY_PUSH_ONLY }}
|
|
||||||
# Only runs the scan and do not provide/returns any results back to the
|
|
||||||
# pipeline.
|
|
||||||
run-tests: false
|
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
name: Tests
|
name: Tests
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_call: # This tells GH that the workflow is reusable
|
workflow_call: # This tells GH that the workflow is reusable
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- 'release-*'
|
- 'release-*'
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, reopened, edited, synchronize]
|
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- 'release-*'
|
- 'release-*'
|
||||||
@@ -16,58 +15,9 @@ jobs:
|
|||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # 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,261 +0,0 @@
|
|||||||
> This `./AGENTS.md` file is generated by running `yarn agents:generate`.
|
|
||||||
|
|
||||||
|
|
||||||
# Project Overview
|
|
||||||
|
|
||||||
The Harvester UI Extension is a Rancher extension that provides the user interface for Harvester within the Rancher Dashboard.
|
|
||||||
|
|
||||||
```
|
|
||||||
Note: This extension is available starting from Rancher 2.10.0. Ensure your Rancher version is 2.10.0 or later to access Harvester integration.
|
|
||||||
```
|
|
||||||
|
|
||||||
# Personas
|
|
||||||
|
|
||||||
## Software Developer
|
|
||||||
|
|
||||||
You are an expert Senior Software Engineer specializing in Vue.js and TypeScript. You have deep knowledge of Kubernetes and the Rancher ecosystem.
|
|
||||||
- **Focus**: Writing clean, maintainable, and performant code.
|
|
||||||
- **Priorities**: Adhering to the project's code style, ensuring type safety, and following best practices for component design.
|
|
||||||
|
|
||||||
|
|
||||||
# Agents
|
|
||||||
|
|
||||||
## Boundaries
|
|
||||||
|
|
||||||
- ✅ **Always:**
|
|
||||||
- Write to `.github`, `docs/`, `pkg/harvester`, `scripts/`.
|
|
||||||
- Make commits in a new branch (for a PR).
|
|
||||||
- Run `yarn lint:fix` before commits.
|
|
||||||
- Use conventional commit format:
|
|
||||||
```
|
|
||||||
<type>:
|
|
||||||
|
|
||||||
<description>
|
|
||||||
```
|
|
||||||
- Follow existing naming conventions (PascalCase for components, camelCase for functions).
|
|
||||||
- After changing a Vue, JS, or TS file, make sure it's automatically formatted with ESLint.
|
|
||||||
- After updating files in `docs/agents.md/`, run `yarn agents:generate` to update root `AGENTS.md`.
|
|
||||||
- ⚠️ **Ask first:**
|
|
||||||
- Adding dependencies
|
|
||||||
- Upgrading dependencies
|
|
||||||
- 🚫 **Never:**
|
|
||||||
- Commit or log secrets, `.env`, `kubeconfig` or any API keys.
|
|
||||||
- Edit `node_modules/`.
|
|
||||||
- Commit directly to `main` (use PRs).
|
|
||||||
- Skip git hooks with `--no-verify` flag.
|
|
||||||
|
|
||||||
## Tools
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- **Node.js**: `>= 24.0.0` (see `.nvmrc` for the pinned version: `24`)
|
|
||||||
- **Package manager**: `yarn` (v1 classic)
|
|
||||||
|
|
||||||
### Common Commands
|
|
||||||
|
|
||||||
| Command | Description |
|
|
||||||
|---------|-------------|
|
|
||||||
| `yarn install --frozen-lockfile` | Install dependencies (CI-safe, no lockfile changes) |
|
|
||||||
| `RANCHER_ENV=harvester API=https://<harvester-ip> yarn dev` | Start development server at `https://127.0.0.1:8005` |
|
|
||||||
| `yarn build-pkg harvester` | Build the Harvester extension package |
|
|
||||||
| `yarn serve-pkgs` | Serve the locally built extension for testing |
|
|
||||||
| `yarn lint` | Run ESLint (zero warnings allowed) |
|
|
||||||
| `yarn lint:fix` | Run ESLint with auto-fix |
|
|
||||||
| `yarn clean` | Clean build artifacts |
|
|
||||||
| `yarn agents:generate` | Regenerate `AGENTS.md` from `docs/agents.md/` sources |
|
|
||||||
|
|
||||||
### Development
|
|
||||||
|
|
||||||
- **Start dev server**:
|
|
||||||
```bash
|
|
||||||
RANCHER_ENV=harvester API=https://your-harvester-ip yarn dev
|
|
||||||
```
|
|
||||||
- `API` should point to a running Harvester cluster (e.g., `https://x.x.x.x`).
|
|
||||||
- The dashboard will be available at `https://127.0.0.1:8005`.
|
|
||||||
|
|
||||||
### Building
|
|
||||||
|
|
||||||
- **Build extension package**:
|
|
||||||
```bash
|
|
||||||
yarn build-pkg harvester
|
|
||||||
```
|
|
||||||
- **Serve locally built extension** (for integration testing with a Rancher instance):
|
|
||||||
```bash
|
|
||||||
yarn serve-pkgs
|
|
||||||
```
|
|
||||||
|
|
||||||
### Linting
|
|
||||||
|
|
||||||
- **Check** (must pass with zero warnings):
|
|
||||||
```bash
|
|
||||||
yarn lint
|
|
||||||
```
|
|
||||||
- **Auto-fix**:
|
|
||||||
```bash
|
|
||||||
yarn lint:fix
|
|
||||||
```
|
|
||||||
- ESLint covers `.js`, `.ts`, and `.vue` files.
|
|
||||||
- Always run `yarn lint:fix` before committing.
|
|
||||||
|
|
||||||
### Commit Conventions
|
|
||||||
|
|
||||||
- Commits are validated by [commitlint](https://commitlint.js.org/) via a Husky `commit-msg` hook.
|
|
||||||
- Follow [Conventional Commits](https://www.conventionalcommits.org/) format (configured in `commitlint.config.js`).
|
|
||||||
|
|
||||||
### Agent Documentation
|
|
||||||
|
|
||||||
- Source files live in `docs/agents.md/` (agents, contributors, personas subdirectories).
|
|
||||||
- After editing any source file, regenerate the root `AGENTS.md`:
|
|
||||||
```bash
|
|
||||||
yarn agents:generate
|
|
||||||
```
|
|
||||||
|
|
||||||
# Contributors Guide
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
Please see the [Harvester UI Extension README](https://github.com/harvester/harvester-ui-extension).
|
|
||||||
|
|
||||||
To get started, follow the `Development Setup` section.
|
|
||||||
|
|
||||||
## Project Information
|
|
||||||
|
|
||||||
- **Tech Stack:**
|
|
||||||
- `Vue.js`: Framework
|
|
||||||
- `Linting`: ESLint
|
|
||||||
- `CSS`: SCSS should be used
|
|
||||||
- `TypeScript`: Primary language for logic.
|
|
||||||
- **Code Style and Standards:**
|
|
||||||
- `Language`: TypeScript is preferred for new code.
|
|
||||||
- `Vue.js`:
|
|
||||||
- Composition API components are preferred over Options API.
|
|
||||||
- Large pages with lots of code and styles should be avoided by breaking the page up into smaller Vue components.
|
|
||||||
- Place source tag above template above style.
|
|
||||||
- style tag should contain `lang='scss' scoped`.
|
|
||||||
- `Linting`: Follow the ESLint configuration in the root.
|
|
||||||
|
|
||||||
- **File Structure:**
|
|
||||||
- `.github/`: CI/CD workflows and Renovate config.
|
|
||||||
- `docs/`: Documentation source for `AGENTS.md` generation.
|
|
||||||
- `scripts/`: Bash scripts for build, CI and doc generation.
|
|
||||||
- `pkg/harvester/`: Main extension source. Files are named after K8s resource types (e.g., `kubevirt.io.virtualmachine`, `harvesterhci.io.volume`).
|
|
||||||
- `index.ts`: Plugin entry point — registers the product and auto-imports models/detail/edit views.
|
|
||||||
- `types.ts`: `HCI` constant mapping 60+ K8s resource types to string identifiers.
|
|
||||||
- `components/`: Reusable Vue components (VNC/serial console, settings panels, upgrade banners, filters).
|
|
||||||
- `config/`: Constants — settings keys, table column definitions, feature flags, type mappings, doc links.
|
|
||||||
- `detail/`: Read-only detail views per resource type. Complex resources use subdirectories with tabs.
|
|
||||||
- `dialog/`: Modal dialogs for operations (VM clone/migrate/restart, backup/restore, device passthrough, etc.).
|
|
||||||
- `edit/`: Create/edit forms. Complex resources (e.g., VM) split into subcomponents (CpuMemory, Network, Volume, CloudConfig, etc.).
|
|
||||||
- `formatters/`: Table cell formatters — state badges, usage bars, resource references.
|
|
||||||
- `l10n/`: Localization (`en-us.yaml`).
|
|
||||||
- `list/`: List (table) views per resource type, mirroring `detail/` and `edit/` naming.
|
|
||||||
- `mixins/`: Shared Vue mixins — VM helpers (`harvester-vm/`) and disk helpers (`harvester-disk.js`).
|
|
||||||
- `models/`: Model classes extending `SteveModel` with computed properties and actions. Base class: `harvester.js`.
|
|
||||||
- `pages/`: Route-level pages — dashboard, support, console, members, brand, alertmanager.
|
|
||||||
- `promptRemove/`: Custom delete-confirmation dialogs (VM, backup).
|
|
||||||
- `routing/`: Vue Router config — all product routes (CRUD, console, support, upgrade, etc.).
|
|
||||||
- `store/`: Vuex modules — `harvester-common.js` for shared state; `harvester-store/` for VM/resource creation actions with Steve integration.
|
|
||||||
- `styles/`: Global SCSS files.
|
|
||||||
- `utils/`: Helpers — VM volume templates, CPU/memory calc, cron parsing, regex validators, feature flags.
|
|
||||||
- `validators/`: Form validation functions per resource type, pushing i18n error messages.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Harvester UI Extension Development Guide
|
|
||||||
|
|
||||||
1. Backward Compatibility
|
|
||||||
The Harvester UI Extension supports earlier cluster versions (e.g., UI Ext v1.8.0 works with clusters v1.7.0 and v1.6.0). It uses Feature Flags defined in pkg/config/feature-flags to ensure the UI matches the cluster's specific version.
|
|
||||||
|
|
||||||
2. Implementation Steps for New Features
|
|
||||||
To add a feature in a new release, follow these steps:
|
|
||||||
|
|
||||||
Register: Add a unique [Feature Name] to the corresponding release array in the configuration.
|
|
||||||
|
|
||||||
Check: Use the following getter to verify if the feature is enabled for the current version:
|
|
||||||
|
|
||||||
```
|
|
||||||
computed: {
|
|
||||||
newFeatureEnabled() {
|
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('[Feature Name]');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
```
|
|
||||||
Render: Use the result of the check to conditionally render the UI components.
|
|
||||||
|
|
||||||
|
|
||||||
## E2E Tests (Cypress)
|
|
||||||
|
|
||||||
See https://github.com/harvester/harvester-ui-tests
|
|
||||||
|
|
||||||
## Node Dependencies
|
|
||||||
|
|
||||||
Dependencies are managed via `package.json` and `yarn`
|
|
||||||
|
|
||||||
- To install dependencies use `yarn install`. This will fail if dependencies and versions listed in `package.json` are out of step with the `yarn.lock` file
|
|
||||||
- To add a dependency use `yarn run add:no-lock ...` instead of `yarn add`
|
|
||||||
- To upgrade a dependency use `yarn run upgrade:no-lock ...` instead of `yarn upgrade`
|
|
||||||
|
|
||||||
|
|
||||||
Renovate automatically updates dependencies and opens upgrade PRs after 10:00 AM on Sundays (`Asia/Taipei`).
|
|
||||||
For the Renovate config, see `.github/renovate.json`.
|
|
||||||
|
|
||||||
|
|
||||||
## Milestone guidance
|
|
||||||
|
|
||||||
- All issues must first be resolved in the `main` branch
|
|
||||||
- If backports are needed they can be made via the backport bot
|
|
||||||
- pull requests
|
|
||||||
- comment `@Mergifyio backport <target branch>` e.g. `@Mergifyio backport release-harvester-v1.8`
|
|
||||||
- All backported pull requests must link to a backported issue
|
|
||||||
|
|
||||||
|
|
||||||
## Creating a branch
|
|
||||||
|
|
||||||
### To resolve an issue
|
|
||||||
- Checkout the branch matching the milestone of the issue `git checkout ${targetMilestoneBranch}`. Replace `${targetMilestoneBranch}` with the target milestone of the issue. For example
|
|
||||||
- `main` for the latest unreleased minor version
|
|
||||||
- `release-harvester-v.X` for release minor versions
|
|
||||||
- `release-harvester-v1.6`
|
|
||||||
- `release-harvester-v1.7`
|
|
||||||
- `release-harvester-v1.8`
|
|
||||||
- Ensure you have the latest of that branch `git pull --rebase`
|
|
||||||
- Checkout the branch to commit the changes to `git checkout issue-${issueNumber}`. Replace `${issueNumber}` with the issue number.
|
|
||||||
|
|
||||||
## Creating a commit
|
|
||||||
|
|
||||||
- This project uses commit-lint with [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) to ensure consistent and meaningful commit messages.
|
|
||||||
|
|
||||||
### Commit Message Format
|
|
||||||
All commit messages must follow the conventional commit format:
|
|
||||||
```
|
|
||||||
<type>[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
|
|
||||||
|
|
||||||
## Creating a Pull Request
|
|
||||||
|
|
||||||
- Pull requests must come from forks
|
|
||||||
- Description should always have commit supported type prefix. E.g `fix: XXX`, `feat: OOO`
|
|
||||||
- A Pull Request will only be merged once
|
|
||||||
- ALL CI gates have passed
|
|
||||||
- At least one harvester/harvester-ui-extension code owners reviews and approves the PR
|
|
||||||
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
@a110605
|
|
||||||
@houhoucoop
|
|
||||||
@@ -1,169 +1,76 @@
|
|||||||
# 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 Harvester UI extension installation instructions, please refer to the page **Rancher Integration** -> **Harvester UI Extension** in [official Harvester documentation](https://docs.harvesterhci.io).
|
To install the Harvester UI Extension in Rancher 2.10.0 and newer:
|
||||||
|
|
||||||
|
- Go to 'Extensions' in the Rancher UI (click on the jigsaw icon in the side bar)
|
||||||
|
- From the three-dots menu in the upper right of the page, open the menu and select 'Manage Repositories'
|
||||||
|
- From the Repositories page, click 'Create'
|
||||||
|
- For name, enter 'harvester-ui-extension'
|
||||||
|
- For Target, choose 'Git repository'
|
||||||
|
- For Git Repo URL, enter `https://github.com/harvester/harvester-ui-extension`
|
||||||
|
- For Git Branch, enter `gh-pages`
|
||||||
|
- Click 'Create`
|
||||||
|
- From the Repositories list, wait until the new Repository added updates to the 'Active' state
|
||||||
|
- Go back to 'Extensions'
|
||||||
|
- Click on the 'Available' tab if not already selected
|
||||||
|
- Click on 'Install' for the Harvester UI Extension
|
||||||
|
|
||||||
## Development Setup
|
## Running for Development
|
||||||
|
|
||||||
|
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-2026 [SUSE, LLC.](https://www.suse.com/)
|
Copyright (c) 2014-2023 [Rancher Labs, Inc.](http://rancher.com)
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -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'),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
# Agents.md
|
|
||||||
|
|
||||||
This folder contains files used to construct a single root `AGENTS.md`
|
|
||||||
|
|
||||||
We do this because
|
|
||||||
- The single commonality between almost all AI tools is that they look for a root `AGENTS.md`
|
|
||||||
- Having the information in a single place for all AI tools is better than duplicating and maintaining the information in multiple files for each AI tool
|
|
||||||
|
|
||||||
## Process
|
|
||||||
1. Add files to or update files in `./docs/agents.md/agents`, `./docs/agents.md/personas`, `./docs/agents.md/contributors`.
|
|
||||||
2. Each file must
|
|
||||||
- Start markdown headers at the `##` level
|
|
||||||
- End in a new line
|
|
||||||
- Order contents in AGENTS.md by giving an optional number prefix
|
|
||||||
3. Run `yarn agents:generate`.
|
|
||||||
- Take the contents of `./docs/agents.md/template_agents.md`
|
|
||||||
- Insert the contents of the agents, personas and contributors files
|
|
||||||
- Output to `./AGENTS.md`
|
|
||||||
4. Commit all changes, including root `./AGENTS.md` and any files that link to it
|
|
||||||
|
|
||||||
## Guidance
|
|
||||||
|
|
||||||
### Examples
|
|
||||||
|
|
||||||
When referencing code or configuration it's good to include an example
|
|
||||||
|
|
||||||
## Future Tasks
|
|
||||||
- Add real world examples when referencing code
|
|
||||||
- Create a GH workflow to monitor `agents.md` folder files in a PR, if changed run generate script and add new AGENTS.md to PR
|
|
||||||
- Discuss
|
|
||||||
- Three multi AI tool related questions
|
|
||||||
- Questions
|
|
||||||
- how AI specific patterns, like co-pilot instructions, can be expressed in a way to be adopted by or reduce duplication with the root agents.md
|
|
||||||
- how AI tool specific metadata code can be included
|
|
||||||
- multiple agents vs personas approach works
|
|
||||||
- Proposal
|
|
||||||
- Generic information applicable to all AI tools is added to the root `agents.md`
|
|
||||||
- Agent, instruction or other AI specific files or patterns can be used in files specific to that AI tool. They must not contain duplicate information to the root `agents.md`
|
|
||||||
- The root `agents.md` continues to be AI tool agnostic
|
|
||||||
- How can folder specific agents be used
|
|
||||||
- Proposal
|
|
||||||
- `agents.md` are added to folders as long as most LLMs can make use of them and they do not duplicate information in the root `agents.md`
|
|
||||||
|
|
||||||
## Test Prompts
|
|
||||||
|
|
||||||
To validate LLMs are picking up information consider running the following prompts.
|
|
||||||
|
|
||||||
### Information
|
|
||||||
|
|
||||||
Can they provide correct information?
|
|
||||||
|
|
||||||
- `what's this project about?`
|
|
||||||
- `can you tell me the process for fixing a github issue in this repo?`
|
|
||||||
- `what makes a good software developer?`
|
|
||||||
- `what should never be done in this project?`
|
|
||||||
- `what are the folders in this project for?`
|
|
||||||
- `what are the env vars i could use when running e2e tests?`
|
|
||||||
|
|
||||||
### Actions
|
|
||||||
|
|
||||||
Can they use the information in their actions?
|
|
||||||
|
|
||||||
Pending
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
## Boundaries
|
|
||||||
|
|
||||||
- ✅ **Always:**
|
|
||||||
- Write to `.github`, `docs/`, `pkg/harvester`, `scripts/`.
|
|
||||||
- Make commits in a new branch (for a PR).
|
|
||||||
- Run `yarn lint:fix` before commits.
|
|
||||||
- Use conventional commit format:
|
|
||||||
```
|
|
||||||
<type>:
|
|
||||||
|
|
||||||
<description>
|
|
||||||
```
|
|
||||||
- Follow existing naming conventions (PascalCase for components, camelCase for functions).
|
|
||||||
- After changing a Vue, JS, or TS file, make sure it's automatically formatted with ESLint.
|
|
||||||
- After updating files in `docs/agents.md/`, run `yarn agents:generate` to update root `AGENTS.md`.
|
|
||||||
- ⚠️ **Ask first:**
|
|
||||||
- Adding dependencies
|
|
||||||
- Upgrading dependencies
|
|
||||||
- 🚫 **Never:**
|
|
||||||
- Commit or log secrets, `.env`, `kubeconfig` or any API keys.
|
|
||||||
- Edit `node_modules/`.
|
|
||||||
- Commit directly to `main` (use PRs).
|
|
||||||
- Skip git hooks with `--no-verify` flag.
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
## Tools
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- **Node.js**: `>= 24.0.0` (see `.nvmrc` for the pinned version: `24`)
|
|
||||||
- **Package manager**: `yarn` (v1 classic)
|
|
||||||
|
|
||||||
### Common Commands
|
|
||||||
|
|
||||||
| Command | Description |
|
|
||||||
|---------|-------------|
|
|
||||||
| `yarn install --frozen-lockfile` | Install dependencies (CI-safe, no lockfile changes) |
|
|
||||||
| `RANCHER_ENV=harvester API=https://<harvester-ip> yarn dev` | Start development server at `https://127.0.0.1:8005` |
|
|
||||||
| `yarn build-pkg harvester` | Build the Harvester extension package |
|
|
||||||
| `yarn serve-pkgs` | Serve the locally built extension for testing |
|
|
||||||
| `yarn lint` | Run ESLint (zero warnings allowed) |
|
|
||||||
| `yarn lint:fix` | Run ESLint with auto-fix |
|
|
||||||
| `yarn clean` | Clean build artifacts |
|
|
||||||
| `yarn agents:generate` | Regenerate `AGENTS.md` from `docs/agents.md/` sources |
|
|
||||||
|
|
||||||
### Development
|
|
||||||
|
|
||||||
- **Start dev server**:
|
|
||||||
```bash
|
|
||||||
RANCHER_ENV=harvester API=https://your-harvester-ip yarn dev
|
|
||||||
```
|
|
||||||
- `API` should point to a running Harvester cluster (e.g., `https://x.x.x.x`).
|
|
||||||
- The dashboard will be available at `https://127.0.0.1:8005`.
|
|
||||||
|
|
||||||
### Building
|
|
||||||
|
|
||||||
- **Build extension package**:
|
|
||||||
```bash
|
|
||||||
yarn build-pkg harvester
|
|
||||||
```
|
|
||||||
- **Serve locally built extension** (for integration testing with a Rancher instance):
|
|
||||||
```bash
|
|
||||||
yarn serve-pkgs
|
|
||||||
```
|
|
||||||
|
|
||||||
### Linting
|
|
||||||
|
|
||||||
- **Check** (must pass with zero warnings):
|
|
||||||
```bash
|
|
||||||
yarn lint
|
|
||||||
```
|
|
||||||
- **Auto-fix**:
|
|
||||||
```bash
|
|
||||||
yarn lint:fix
|
|
||||||
```
|
|
||||||
- ESLint covers `.js`, `.ts`, and `.vue` files.
|
|
||||||
- Always run `yarn lint:fix` before committing.
|
|
||||||
|
|
||||||
### Commit Conventions
|
|
||||||
|
|
||||||
- Commits are validated by [commitlint](https://commitlint.js.org/) via a Husky `commit-msg` hook.
|
|
||||||
- Follow [Conventional Commits](https://www.conventionalcommits.org/) format (configured in `commitlint.config.js`).
|
|
||||||
|
|
||||||
### Agent Documentation
|
|
||||||
|
|
||||||
- Source files live in `docs/agents.md/` (agents, contributors, personas subdirectories).
|
|
||||||
- After editing any source file, regenerate the root `AGENTS.md`:
|
|
||||||
```bash
|
|
||||||
yarn agents:generate
|
|
||||||
```
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
## Getting Started
|
|
||||||
|
|
||||||
Please see the [Harvester UI Extension README](https://github.com/harvester/harvester-ui-extension).
|
|
||||||
|
|
||||||
To get started, follow the `Development Setup` section.
|
|
||||||
|
|
||||||
## Project Information
|
|
||||||
|
|
||||||
- **Tech Stack:**
|
|
||||||
- `Vue.js`: Framework
|
|
||||||
- `Linting`: ESLint
|
|
||||||
- `CSS`: SCSS should be used
|
|
||||||
- `TypeScript`: Primary language for logic.
|
|
||||||
- **Code Style and Standards:**
|
|
||||||
- `Language`: TypeScript is preferred for new code.
|
|
||||||
- `Vue.js`:
|
|
||||||
- Composition API components are preferred over Options API.
|
|
||||||
- Large pages with lots of code and styles should be avoided by breaking the page up into smaller Vue components.
|
|
||||||
- Place source tag above template above style.
|
|
||||||
- style tag should contain `lang='scss' scoped`.
|
|
||||||
- `Linting`: Follow the ESLint configuration in the root.
|
|
||||||
|
|
||||||
- **File Structure:**
|
|
||||||
- `.github/`: CI/CD workflows and Renovate config.
|
|
||||||
- `docs/`: Documentation source for `AGENTS.md` generation.
|
|
||||||
- `scripts/`: Bash scripts for build, CI and doc generation.
|
|
||||||
- `pkg/harvester/`: Main extension source. Files are named after K8s resource types (e.g., `kubevirt.io.virtualmachine`, `harvesterhci.io.volume`).
|
|
||||||
- `index.ts`: Plugin entry point — registers the product and auto-imports models/detail/edit views.
|
|
||||||
- `types.ts`: `HCI` constant mapping 60+ K8s resource types to string identifiers.
|
|
||||||
- `components/`: Reusable Vue components (VNC/serial console, settings panels, upgrade banners, filters).
|
|
||||||
- `config/`: Constants — settings keys, table column definitions, feature flags, type mappings, doc links.
|
|
||||||
- `detail/`: Read-only detail views per resource type. Complex resources use subdirectories with tabs.
|
|
||||||
- `dialog/`: Modal dialogs for operations (VM clone/migrate/restart, backup/restore, device passthrough, etc.).
|
|
||||||
- `edit/`: Create/edit forms. Complex resources (e.g., VM) split into subcomponents (CpuMemory, Network, Volume, CloudConfig, etc.).
|
|
||||||
- `formatters/`: Table cell formatters — state badges, usage bars, resource references.
|
|
||||||
- `l10n/`: Localization (`en-us.yaml`).
|
|
||||||
- `list/`: List (table) views per resource type, mirroring `detail/` and `edit/` naming.
|
|
||||||
- `mixins/`: Shared Vue mixins — VM helpers (`harvester-vm/`) and disk helpers (`harvester-disk.js`).
|
|
||||||
- `models/`: Model classes extending `SteveModel` with computed properties and actions. Base class: `harvester.js`.
|
|
||||||
- `pages/`: Route-level pages — dashboard, support, console, members, brand, alertmanager.
|
|
||||||
- `promptRemove/`: Custom delete-confirmation dialogs (VM, backup).
|
|
||||||
- `routing/`: Vue Router config — all product routes (CRUD, console, support, upgrade, etc.).
|
|
||||||
- `store/`: Vuex modules — `harvester-common.js` for shared state; `harvester-store/` for VM/resource creation actions with Steve integration.
|
|
||||||
- `styles/`: Global SCSS files.
|
|
||||||
- `utils/`: Helpers — VM volume templates, CPU/memory calc, cron parsing, regex validators, feature flags.
|
|
||||||
- `validators/`: Form validation functions per resource type, pushing i18n error messages.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Harvester UI Extension Development Guide
|
|
||||||
|
|
||||||
1. Backward Compatibility
|
|
||||||
The Harvester UI Extension supports earlier cluster versions (e.g., UI Ext v1.8.0 works with clusters v1.7.0 and v1.6.0). It uses Feature Flags defined in pkg/config/feature-flags to ensure the UI matches the cluster's specific version.
|
|
||||||
|
|
||||||
2. Implementation Steps for New Features
|
|
||||||
To add a feature in a new release, follow these steps:
|
|
||||||
|
|
||||||
Register: Add a unique [Feature Name] to the corresponding release array in the configuration.
|
|
||||||
|
|
||||||
Check: Use the following getter to verify if the feature is enabled for the current version:
|
|
||||||
|
|
||||||
```
|
|
||||||
computed: {
|
|
||||||
newFeatureEnabled() {
|
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('[Feature Name]');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
```
|
|
||||||
Render: Use the result of the check to conditionally render the UI components.
|
|
||||||
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
## E2E Tests (Cypress)
|
|
||||||
|
|
||||||
See https://github.com/harvester/harvester-ui-tests
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
## Node Dependencies
|
|
||||||
|
|
||||||
Dependencies are managed via `package.json` and `yarn`
|
|
||||||
|
|
||||||
- To install dependencies use `yarn install`. This will fail if dependencies and versions listed in `package.json` are out of step with the `yarn.lock` file
|
|
||||||
- To add a dependency use `yarn run add:no-lock ...` instead of `yarn add`
|
|
||||||
- To upgrade a dependency use `yarn run upgrade:no-lock ...` instead of `yarn upgrade`
|
|
||||||
|
|
||||||
|
|
||||||
Renovate automatically updates dependencies and opens upgrade PRs after 10:00 AM on Sundays (`Asia/Taipei`).
|
|
||||||
For the Renovate config, see `.github/renovate.json`.
|
|
||||||
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
## Milestone guidance
|
|
||||||
|
|
||||||
- All issues must first be resolved in the `main` branch
|
|
||||||
- If backports are needed they can be made via the backport bot
|
|
||||||
- pull requests
|
|
||||||
- comment `@Mergifyio backport <target branch>` e.g. `@Mergifyio backport release-harvester-v1.8`
|
|
||||||
- All backported pull requests must link to a backported issue
|
|
||||||
|
|
||||||
|
|
||||||
## Creating a branch
|
|
||||||
|
|
||||||
### To resolve an issue
|
|
||||||
- Checkout the branch matching the milestone of the issue `git checkout ${targetMilestoneBranch}`. Replace `${targetMilestoneBranch}` with the target milestone of the issue. For example
|
|
||||||
- `main` for the latest unreleased minor version
|
|
||||||
- `release-harvester-v.X` for release minor versions
|
|
||||||
- `release-harvester-v1.6`
|
|
||||||
- `release-harvester-v1.7`
|
|
||||||
- `release-harvester-v1.8`
|
|
||||||
- Ensure you have the latest of that branch `git pull --rebase`
|
|
||||||
- Checkout the branch to commit the changes to `git checkout issue-${issueNumber}`. Replace `${issueNumber}` with the issue number.
|
|
||||||
|
|
||||||
## Creating a commit
|
|
||||||
|
|
||||||
- This project uses commit-lint with [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) to ensure consistent and meaningful commit messages.
|
|
||||||
|
|
||||||
### Commit Message Format
|
|
||||||
All commit messages must follow the conventional commit format:
|
|
||||||
```
|
|
||||||
<type>[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
|
|
||||||
|
|
||||||
## Creating a Pull Request
|
|
||||||
|
|
||||||
- Pull requests must come from forks
|
|
||||||
- Description should always have commit supported type prefix. E.g `fix: XXX`, `feat: OOO`
|
|
||||||
- A Pull Request will only be merged once
|
|
||||||
- ALL CI gates have passed
|
|
||||||
- At least one harvester/harvester-ui-extension code owners reviews and approves the PR
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
## Software Developer
|
|
||||||
|
|
||||||
You are an expert Senior Software Engineer specializing in Vue.js and TypeScript. You have deep knowledge of Kubernetes and the Rancher ecosystem.
|
|
||||||
- **Focus**: Writing clean, maintainable, and performant code.
|
|
||||||
- **Priorities**: Adhering to the project's code style, ensuring type safety, and following best practices for component design.
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
> This `./AGENTS.md` file is generated by running `yarn agents:generate`.
|
|
||||||
|
|
||||||
|
|
||||||
# Project Overview
|
|
||||||
|
|
||||||
The Harvester UI Extension is a Rancher extension that provides the user interface for Harvester within the Rancher Dashboard.
|
|
||||||
|
|
||||||
```
|
|
||||||
Note: This extension is available starting from Rancher 2.10.0. Ensure your Rancher version is 2.10.0 or later to access Harvester integration.
|
|
||||||
```
|
|
||||||
|
|
||||||
# Personas
|
|
||||||
|
|
||||||
`<personas>`
|
|
||||||
|
|
||||||
# Agents
|
|
||||||
|
|
||||||
`<agents>`
|
|
||||||
|
|
||||||
# Contributors Guide
|
|
||||||
|
|
||||||
`<contributors>`
|
|
||||||
+15
-28
@@ -1,41 +1,36 @@
|
|||||||
{
|
{
|
||||||
"name": "harvester-ui-extension",
|
"name": "harvester-ui-extension",
|
||||||
"version": "1.8.2",
|
"version": "1.0.6",
|
||||||
"private": false,
|
"private": false,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=24.0.0"
|
"node": ">=20.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/plugin-transform-class-static-block": "7.29.7",
|
"@rancher/shell": "3.0.1-rc.4",
|
||||||
"@rancher/shell": "3.0.12-rc.1",
|
|
||||||
"@vue-flow/background": "^1.3.0",
|
|
||||||
"@vue-flow/controls": "^1.1.1",
|
|
||||||
"@vue-flow/core": "^1.33.5",
|
|
||||||
"@vue-flow/minimap": "^1.4.0",
|
|
||||||
"cache-loader": "^4.1.0",
|
"cache-loader": "^4.1.0",
|
||||||
"color": "4.2.3",
|
"color": "4.2.3",
|
||||||
"ip": "2.0.1",
|
"ip": "2.0.1",
|
||||||
"node-polyfill-webpack-plugin": "^3.0.0",
|
"node-polyfill-webpack-plugin": "^3.0.0",
|
||||||
"elkjs": "^0.11.0",
|
|
||||||
"vue-draggable-next": "^2.2.1",
|
"vue-draggable-next": "^2.2.1",
|
||||||
"yaml": "^2.5.1"
|
"yaml": "^2.5.1",
|
||||||
|
"@babel/plugin-transform-class-static-block": "7.28.3"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@types/node": "25.9.5",
|
"@types/node": "~20.19.0",
|
||||||
"cronstrue": "2.59.0",
|
"cronstrue": "2.59.0",
|
||||||
"d3-color": "3.1.0",
|
"d3-color": "3.1.0",
|
||||||
"ejs": "3.1.10",
|
"ejs": "3.1.10",
|
||||||
"follow-redirects": "1.16.0",
|
"follow-redirects": "1.15.11",
|
||||||
"glob": "7.2.3",
|
"glob": "7.2.3",
|
||||||
"glob-parent": "6.0.2",
|
"glob-parent": "6.0.2",
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
"@types/lodash": "4.17.24",
|
"@types/lodash": "4.17.21",
|
||||||
"merge": "2.1.1",
|
"merge": "2.1.1",
|
||||||
"node-forge": "1.4.0",
|
"node-forge": "1.3.3",
|
||||||
"nth-check": "2.1.1",
|
"nth-check": "2.1.1",
|
||||||
"qs": "6.15.3",
|
"qs": "6.14.1",
|
||||||
"roarr": "7.21.7",
|
"roarr": "7.21.2",
|
||||||
"semver": "7.8.5",
|
"semver": "7.7.3",
|
||||||
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
|
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -43,21 +38,13 @@
|
|||||||
"build": "./node_modules/.bin/vue-cli-service build",
|
"build": "./node_modules/.bin/vue-cli-service build",
|
||||||
"clean": "./node_modules/@rancher/shell/scripts/clean",
|
"clean": "./node_modules/@rancher/shell/scripts/clean",
|
||||||
"lint": "./node_modules/.bin/eslint --max-warnings 0 --ext .js,.ts,.vue .",
|
"lint": "./node_modules/.bin/eslint --max-warnings 0 --ext .js,.ts,.vue .",
|
||||||
"lint:fix": "./node_modules/.bin/eslint --fix --max-warnings 0 --ext .js,.ts,.vue .",
|
|
||||||
"build-pkg": "./node_modules/@rancher/shell/scripts/build-pkg.sh",
|
"build-pkg": "./node_modules/@rancher/shell/scripts/build-pkg.sh",
|
||||||
"serve-pkgs": "./node_modules/@rancher/shell/scripts/serve-pkgs",
|
"serve-pkgs": "./node_modules/@rancher/shell/scripts/serve-pkgs",
|
||||||
"publish-pkgs": "./node_modules/@rancher/shell/scripts/extension/publish",
|
"publish-pkgs": "./node_modules/@rancher/shell/scripts/extension/publish",
|
||||||
"parse-tag-name": "./node_modules/@rancher/shell/scripts/extension/parse-tag-name",
|
"parse-tag-name": "./node_modules/@rancher/shell/scripts/extension/parse-tag-name"
|
||||||
"commitlint": "commitlint --edit",
|
|
||||||
"prepare": "husky",
|
|
||||||
"agents:generate": "./scripts/generate-agent-and-persona-mds.sh"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/load": "^19.8.1",
|
|
||||||
"@commitlint/cli": "^19.8.1",
|
|
||||||
"@commitlint/config-conventional": "^19.8.1",
|
|
||||||
"eslint-plugin-node": "^11.1.0",
|
"eslint-plugin-node": "^11.1.0",
|
||||||
"eslint-plugin-promise": "^7.1.0",
|
"eslint-plugin-promise": "^7.1.0"
|
||||||
"husky": "^9.1.7"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
# harvester-ui-extension
|
|
||||||
|
|
||||||
The Harvester UI Extension is a Rancher extension that provides the user interface for [Harvester](https://harvesterhci.io) within the [Rancher Dashboard](https://github.com/rancher/dashboard).
|
|
||||||
|
|
||||||
> **Note:**
|
|
||||||
> This extension is available starting from **Rancher 2.10.0**. Ensure your Rancher version is **2.10.0 or later** to access Harvester integration.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
For Harvester UI extension installation instructions, please refer to the page **Rancher Integration** -> **Harvester UI Extension** in [official Harvester documentation](https://docs.harvesterhci.io).
|
|
||||||
|
|
||||||
## Development Setup
|
|
||||||
|
|
||||||
Ensure **Node.js v20 or later** is installed for development and debugging.
|
|
||||||
|
|
||||||
### Standalone Mode
|
|
||||||
|
|
||||||
Run the extension standalone with hot reload at `https://localhost:8005`.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install dependencies
|
|
||||||
yarn install
|
|
||||||
|
|
||||||
# Start the development server
|
|
||||||
RANCHER_ENV=harvester API=https://your-harvester-ip yarn dev
|
|
||||||
|
|
||||||
# Example with specific server version
|
|
||||||
RANCHER_ENV=harvester VUE_APP_SERVER_VERSION=v1.5.0 API=https://192.168.1.123 yarn dev
|
|
||||||
```
|
|
||||||
|
|
||||||
You may also define environment variables in a `.env` file:
|
|
||||||
|
|
||||||
```env
|
|
||||||
RANCHER_ENV=harvester
|
|
||||||
VUE_APP_SERVER_VERSION=v1.5.0
|
|
||||||
API=https://192.168.1.123
|
|
||||||
```
|
|
||||||
|
|
||||||
### Rancher Integration Mode
|
|
||||||
|
|
||||||
To run as a Rancher extension, follow the [Rancher UI Extension Guide](https://extensions.rancher.io/extensions/next/extensions-getting-started#running-the-app).
|
|
||||||
|
|
||||||
```bash
|
|
||||||
API=https://your-rancher-ip yarn dev
|
|
||||||
```
|
|
||||||
|
|
||||||
## Commit Message Guidelines
|
|
||||||
|
|
||||||
This project uses [commit-lint](https://commitlint.js.org/) with [Conventional Commits](https://www.conventionalcommits.org/) to ensure consistent and meaningful commit messages.
|
|
||||||
|
|
||||||
### Commit Message Format
|
|
||||||
|
|
||||||
All commit messages must follow the conventional commit format:
|
|
||||||
|
|
||||||
```
|
|
||||||
<type>[optional scope]: <description>
|
|
||||||
|
|
||||||
[optional body]
|
|
||||||
|
|
||||||
[optional footer(s)]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Supported Types
|
|
||||||
|
|
||||||
- **feat**: New features
|
|
||||||
- **fix**: Bug fixes
|
|
||||||
- **docs**: Documentation changes
|
|
||||||
- **style**: Code style changes (formatting, missing semicolons, etc.)
|
|
||||||
- **refactor**: Code refactoring
|
|
||||||
- **perf**: Performance improvements
|
|
||||||
- **test**: Adding or updating tests
|
|
||||||
- **build**: Build system or external dependencies
|
|
||||||
- **ci**: CI/CD changes
|
|
||||||
- **chore**: Other changes that don't modify src or test files
|
|
||||||
- **revert**: Reverts a previous commit
|
|
||||||
- **wip**: Work in progress
|
|
||||||
- **deps**: Dependency updates
|
|
||||||
- **security**: Security fixes
|
|
||||||
|
|
||||||
### Examples
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Feature
|
|
||||||
git commit -m "feat: add new virtual machine creation wizard"
|
|
||||||
|
|
||||||
# Bug fix
|
|
||||||
git commit -m "fix: resolve memory leak in VM console"
|
|
||||||
|
|
||||||
# Documentation
|
|
||||||
git commit -m "docs: update installation instructions"
|
|
||||||
|
|
||||||
# Breaking change
|
|
||||||
git commit -m "feat!: change API endpoint structure
|
|
||||||
|
|
||||||
BREAKING CHANGE: The /api/v1/vms endpoint has been replaced with /api/v2/vms"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Git Hooks
|
|
||||||
|
|
||||||
The project uses [Husky](https://typicode.github.io/husky/) to automatically validate commit messages and run linting before commits:
|
|
||||||
|
|
||||||
- **pre-commit**: Runs ESLint to ensure code quality
|
|
||||||
- **commit-msg**: Validates commit message format using commit-lint
|
|
||||||
|
|
||||||
These hooks are automatically installed when you run `yarn install`.
|
|
||||||
|
|
||||||
### Manual Validation
|
|
||||||
|
|
||||||
You can manually validate commit messages:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Validate the last commit
|
|
||||||
yarn commitlint
|
|
||||||
|
|
||||||
# Validate a specific commit
|
|
||||||
npx commitlint --from <commit-hash>
|
|
||||||
|
|
||||||
# Validate a range of commits
|
|
||||||
npx commitlint --from <start-hash> --to <end-hash>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Branch Structure
|
|
||||||
|
|
||||||
- **`main`** – Main development branch
|
|
||||||
- **`release-harvester-vX.Y`** – Stable release branches per version series
|
|
||||||
- **`vX.Y-head`** – Testing branches for ongoing changes to extension builds in each release series
|
|
||||||
|
|
||||||
> **Note:**
|
|
||||||
> The `vX.Y-head` branches are auto-generated and kept in sync with release branches. Use these for testing the latest changes in each version series.
|
|
||||||
|
|
||||||
## Testing Guidelines
|
|
||||||
|
|
||||||
### UI Extension Testing
|
|
||||||
|
|
||||||
To validate changes in a release series, switch to the appropriate `vX.Y-head` branch. For main branch testing, use `main-head`.
|
|
||||||
|
|
||||||
- Examples:
|
|
||||||
- Test `1.0.x` series → `v1.0-head`
|
|
||||||
- Test `1.5.x` series → `v1.5-head`
|
|
||||||
|
|
||||||
**Steps:**
|
|
||||||
1. Navigate to **Rancher UI** → **Local** → **App** → **Repositories**
|
|
||||||
2. Refresh the Harvester repository using the target `vX.Y-head` branch
|
|
||||||
3. Go to the **Extensions** page and install the desired version
|
|
||||||
|
|
||||||
### Standalone Mode Testing
|
|
||||||
|
|
||||||
To test the standalone UI, configure Harvester to load the UI from an external source.
|
|
||||||
|
|
||||||
- Examples of `ui-index`:
|
|
||||||
- Main branch → `https://releases.rancher.com/harvester-ui/dashboard/latest/index.html`
|
|
||||||
- Release series `1.5.x` → `https://releases.rancher.com/harvester-ui/dashboard/release-harvester-v1.5/index.html`
|
|
||||||
|
|
||||||
**Steps:**
|
|
||||||
1. Go to **Harvester UI** → **Advanced** → **Settings** → **UI**
|
|
||||||
2. Set **ui-source** to `External`
|
|
||||||
3. Set **ui-index** to the desired URL
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
If you want to contribute, start by reading this document, then visit our [Getting Started guide](https://extensions.rancher.io/extensions/next/extensions-getting-started) to learn how to develop and submit changes.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
Copyright (c) 2014-2026 [SUSE, LLC.](https://www.suse.com/)
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
@@ -5,10 +5,8 @@ import { Checkbox } from '@components/Form/Checkbox';
|
|||||||
import ModalWithCard from '@shell/components/ModalWithCard';
|
import ModalWithCard from '@shell/components/ModalWithCard';
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
|
||||||
import { HCI } from '../types';
|
import { HCI } from '../types';
|
||||||
import UpgradeInfo from './UpgradeInfo';
|
import UpgradeInfo from './UpgradeInfo';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterUpgrade',
|
name: 'HarvesterUpgrade',
|
||||||
|
|
||||||
@@ -30,15 +28,14 @@ export default {
|
|||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
upgrade: [],
|
upgrade: [],
|
||||||
upgradeMessage: [],
|
upgradeMessage: [],
|
||||||
errors: '',
|
errors: '',
|
||||||
selectMode: true,
|
selectMode: true,
|
||||||
version: '',
|
version: '',
|
||||||
enableLogging: true,
|
enableLogging: true,
|
||||||
skipSingleReplicaDetachedVol: false,
|
readyReleaseNote: false,
|
||||||
readyReleaseNote: false,
|
isOpen: false
|
||||||
isOpen: false
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -71,10 +68,6 @@ export default {
|
|||||||
return this.$store.getters['harvester/schemaFor'](HCI.UPGRADE_LOG);
|
return this.$store.getters['harvester/schemaFor'](HCI.UPGRADE_LOG);
|
||||||
},
|
},
|
||||||
|
|
||||||
skipSingleReplicaDetachedVolFeatureEnabled() {
|
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('skipSingleReplicaDetachedVol');
|
|
||||||
},
|
|
||||||
|
|
||||||
releaseLink() {
|
releaseLink() {
|
||||||
return `https://github.com/harvester/harvester/releases/tag/${ this.version }`;
|
return `https://github.com/harvester/harvester/releases/tag/${ this.version }`;
|
||||||
}
|
}
|
||||||
@@ -111,11 +104,6 @@ export default {
|
|||||||
spec: { version: this.version }
|
spec: { version: this.version }
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.skipSingleReplicaDetachedVolFeatureEnabled && this.skipSingleReplicaDetachedVol) {
|
|
||||||
upgradeValue.metadata.annotations =
|
|
||||||
{ [HCI_ANNOTATIONS.SKIP_SINGLE_REPLICA_DETACHED_VOL]: JSON.stringify(this.skipSingleReplicaDetachedVol) };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.canEnableLogging) {
|
if (this.canEnableLogging) {
|
||||||
upgradeValue.spec.logEnabled = this.enableLogging;
|
upgradeValue.spec.logEnabled = this.enableLogging;
|
||||||
}
|
}
|
||||||
@@ -202,21 +190,6 @@ export default {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="skipSingleReplicaDetachedVolFeatureEnabled"
|
|
||||||
class="mb-5"
|
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
v-model:value="skipSingleReplicaDetachedVol"
|
|
||||||
class="check"
|
|
||||||
type="checkbox"
|
|
||||||
:label="t('harvester.upgradePage.skipSingleReplicaDetachedVol')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<hr
|
|
||||||
v-if="version"
|
|
||||||
class="divider"
|
|
||||||
/>
|
|
||||||
<div v-if="version">
|
<div v-if="version">
|
||||||
<p
|
<p
|
||||||
v-clean-html="t('harvester.upgradePage.releaseTip', {url: releaseLink}, true)"
|
v-clean-html="t('harvester.upgradePage.releaseTip', {url: releaseLink}, true)"
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import Collapse from '@shell/components/Collapse';
|
import Collapse from '@shell/components/Collapse';
|
||||||
import PercentageBar from '@shell/components/PercentageBar';
|
import PercentageBar from '@shell/components/PercentageBar';
|
||||||
import { HCI } from '../types';
|
|
||||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterUpgradeProgressList',
|
name: 'HarvesterUpgradeProgressList',
|
||||||
@@ -27,45 +25,13 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
await this.$store.dispatch('harvester/findAll', { type: HCI.UPGRADE });
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return { open: true };
|
return { open: true };
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
|
||||||
showResumeButton() {
|
|
||||||
return this.title === 'Upgrading Node';
|
|
||||||
},
|
|
||||||
latestUpgradeCR() {
|
|
||||||
return this.$store.getters['harvester/all'](HCI.UPGRADE).find( (U) => U.isLatestUpgrade);
|
|
||||||
},
|
|
||||||
resumeUpgradePausedNodeEnabled() {
|
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('resumeUpgradePausedNode');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
handleSwitch() {
|
handleSwitch() {
|
||||||
this.open = !this.open;
|
this.open = !this.open;
|
||||||
},
|
|
||||||
async resumeNodeUpgrade(nodeName) {
|
|
||||||
if (!this.latestUpgradeCR || !nodeName) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const upgradePauseMapString = this.latestUpgradeCR.metadata.annotations[HCI_ANNOTATIONS.NODE_UPGRADE_PAUSE_MAP] || '{}';
|
|
||||||
const upgradePauseMap = JSON.parse(upgradePauseMapString);
|
|
||||||
|
|
||||||
// update the upgrade CR annotation harvesterhci.io/node-upgrade-pause-map to unpause the node upgrade process
|
|
||||||
upgradePauseMap[`${ nodeName }`] = 'unpause';
|
|
||||||
this.latestUpgradeCR.setAnnotation(HCI_ANNOTATIONS.NODE_UPGRADE_PAUSE_MAP, JSON.stringify(upgradePauseMap));
|
|
||||||
await this.latestUpgradeCR.save();
|
|
||||||
} catch (e) {
|
|
||||||
console.error(`unable to update harvester upgrade CR annotations: ${ this.latestUpgradeCR.id }.`, e); // eslint-disable-line no-console
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -97,28 +63,12 @@ export default {
|
|||||||
v-for="(item, i) in list"
|
v-for="(item, i) in list"
|
||||||
:key="i"
|
:key="i"
|
||||||
>
|
>
|
||||||
<div class="upgrade-node-header">
|
<p>
|
||||||
<div class="upgrade-node-title">
|
{{ item.name }} <span
|
||||||
<p>
|
class="status"
|
||||||
{{ item.name }}
|
:class="{ [item.state]: true }"
|
||||||
</p>
|
>{{ item.state }}</span>
|
||||||
<span
|
</p>
|
||||||
class="status"
|
|
||||||
:class="{ [item.state]: true }"
|
|
||||||
>
|
|
||||||
{{ item.state }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
v-if="showResumeButton && resumeUpgradePausedNodeEnabled && item.state === 'Node-upgrade paused'"
|
|
||||||
type="button"
|
|
||||||
class="btn bg-info btn-sm"
|
|
||||||
data-testid="add-item"
|
|
||||||
@click="resumeNodeUpgrade(item.name)"
|
|
||||||
>
|
|
||||||
{{ t('action.resume') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<PercentageBar
|
<PercentageBar
|
||||||
:model-value="item.percent"
|
:model-value="item.percent"
|
||||||
preferred-direction="MORE"
|
preferred-direction="MORE"
|
||||||
@@ -152,21 +102,10 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.custom-content {
|
.custom-content {
|
||||||
.upgrade-node-title {
|
margin-bottom: 14px;
|
||||||
flex: 1 0 80%;
|
p {
|
||||||
margin-right: 10px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
.upgrade-node-header {
|
|
||||||
display:flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
margin-bottom: 14px;
|
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
@@ -178,8 +117,6 @@ export default {
|
|||||||
}
|
}
|
||||||
.warning {
|
.warning {
|
||||||
color: var(--error);
|
color: var(--error);
|
||||||
margin-bottom: 8px;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,123 +0,0 @@
|
|||||||
<script>
|
|
||||||
import MessageLink from '@shell/components/MessageLink';
|
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
||||||
import { HCI_SETTING } from '../../config/settings';
|
|
||||||
import { Checkbox } from '@components/Form/Checkbox';
|
|
||||||
import { Banner } from '@components/Banner';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'HarvesterEditClusterRegistrationURL',
|
|
||||||
|
|
||||||
components: {
|
|
||||||
LabeledInput, MessageLink, Checkbox, Banner
|
|
||||||
},
|
|
||||||
|
|
||||||
mixins: [CreateEditView],
|
|
||||||
|
|
||||||
data() {
|
|
||||||
let parseDefaultValue = {};
|
|
||||||
|
|
||||||
try {
|
|
||||||
parseDefaultValue = JSON.parse(this.value.value);
|
|
||||||
} catch (error) {
|
|
||||||
parseDefaultValue.url = this.value.value;
|
|
||||||
parseDefaultValue.insecureSkipTLSVerify = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
parseDefaultValue,
|
|
||||||
errors: []
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
toCA() {
|
|
||||||
return `${ HCI_SETTING.ADDITIONAL_CA }?mode=edit`;
|
|
||||||
},
|
|
||||||
clusterRegistrationTLSVerifyEnabled() {
|
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('clusterRegistrationTLSVerify');
|
|
||||||
},
|
|
||||||
registrationURL: {
|
|
||||||
get() {
|
|
||||||
return this.clusterRegistrationTLSVerifyEnabled ? this.parseDefaultValue.url : this.parseDefaultValue;
|
|
||||||
},
|
|
||||||
|
|
||||||
set(value) {
|
|
||||||
if (this.clusterRegistrationTLSVerifyEnabled) {
|
|
||||||
this.parseDefaultValue.url = value;
|
|
||||||
} else {
|
|
||||||
this.parseDefaultValue = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
getDefaultValue() {
|
|
||||||
if (this.clusterRegistrationTLSVerifyEnabled) {
|
|
||||||
return { url: '', insecureSkipTLSVerify: false };
|
|
||||||
} else {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
updateUrl() {
|
|
||||||
this.update();
|
|
||||||
},
|
|
||||||
|
|
||||||
update() {
|
|
||||||
if (this.clusterRegistrationTLSVerifyEnabled) {
|
|
||||||
this.value['value'] = JSON.stringify(this.parseDefaultValue);
|
|
||||||
} else {
|
|
||||||
this.value['value'] = this.parseDefaultValue;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
useDefault() {
|
|
||||||
this.parseDefaultValue = this.getDefaultValue();
|
|
||||||
},
|
|
||||||
|
|
||||||
updateInsecureSkipTLSVerify(newValue) {
|
|
||||||
const { url = '' } = this.parseDefaultValue;
|
|
||||||
|
|
||||||
this.parseDefaultValue = { url, insecureSkipTLSVerify: newValue };
|
|
||||||
this.update();
|
|
||||||
},
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
class="row"
|
|
||||||
>
|
|
||||||
<div class="col span-12">
|
|
||||||
<Banner color="info">
|
|
||||||
<MessageLink
|
|
||||||
:to="toCA"
|
|
||||||
target="_blank"
|
|
||||||
prefix-label="harvester.setting.clusterRegistrationUrl.tip.prefix"
|
|
||||||
middle-label="harvester.setting.clusterRegistrationUrl.tip.middle"
|
|
||||||
suffix-label="harvester.setting.clusterRegistrationUrl.tip.suffix"
|
|
||||||
/>
|
|
||||||
</Banner>
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="registrationURL"
|
|
||||||
class="mb-20"
|
|
||||||
:mode="mode"
|
|
||||||
:label="t('harvester.setting.clusterRegistrationUrl.url')"
|
|
||||||
@update:value="updateUrl"
|
|
||||||
/>
|
|
||||||
<div v-if="clusterRegistrationTLSVerifyEnabled">
|
|
||||||
<Checkbox
|
|
||||||
v-model:value="parseDefaultValue.insecureSkipTLSVerify"
|
|
||||||
class="check mb-5"
|
|
||||||
type="checkbox"
|
|
||||||
:label="t('harvester.setting.clusterRegistrationUrl.insecureSkipTLSVerify')"
|
|
||||||
@update:value="updateInsecureSkipTLSVerify"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -174,6 +174,10 @@ export default {
|
|||||||
return driver === LONGHORN_DRIVER || this.allowEmptySnapshotClassNameFeatureEnabled;
|
return driver === LONGHORN_DRIVER || this.allowEmptySnapshotClassNameFeatureEnabled;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isBackupVolumeSnapshotClassNameSelectable(driver) {
|
||||||
|
return driver === LONGHORN_DRIVER;
|
||||||
|
},
|
||||||
|
|
||||||
add() {
|
add() {
|
||||||
this.configArr.push({
|
this.configArr.push({
|
||||||
key: '',
|
key: '',
|
||||||
@@ -243,6 +247,7 @@ export default {
|
|||||||
:options="getVolumeSnapshotOptions(driver.key)"
|
:options="getVolumeSnapshotOptions(driver.key)"
|
||||||
:required="isBackupVolumeSnapshotRequired(driver.key)"
|
:required="isBackupVolumeSnapshotRequired(driver.key)"
|
||||||
:label="t('harvester.setting.csiDriverConfig.backupVolumeSnapshotClassName')"
|
:label="t('harvester.setting.csiDriverConfig.backupVolumeSnapshotClassName')"
|
||||||
|
@selectable="isBackupVolumeSnapshotClassNameSelectable(driver.key)"
|
||||||
@keydown.native.enter.prevent="()=>{}"
|
@keydown.native.enter.prevent="()=>{}"
|
||||||
@update:value="update"
|
@update:value="update"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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,104 +0,0 @@
|
|||||||
<script>
|
|
||||||
import UnitInput from '@shell/components/form/UnitInput';
|
|
||||||
import { Banner } from '@components/Banner';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'HarvesterInstanceManagerResources',
|
|
||||||
|
|
||||||
components: {
|
|
||||||
UnitInput,
|
|
||||||
Banner,
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
|
||||||
value: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({
|
|
||||||
value: '',
|
|
||||||
default: '{}'
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
const resources = this.parseJSON(this.value?.value) || this.parseJSON(this.value?.default) || {};
|
|
||||||
|
|
||||||
return {
|
|
||||||
resources,
|
|
||||||
parseError: null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
parseJSON(string) {
|
|
||||||
try {
|
|
||||||
return JSON.parse(string);
|
|
||||||
} catch (e) {
|
|
||||||
this.parseError = this.t('harvester.setting.instanceManagerResources.parseError', { error: e.message });
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
update() {
|
|
||||||
if (!this.value) return;
|
|
||||||
|
|
||||||
const cpu = { ...this.resources?.cpu };
|
|
||||||
|
|
||||||
if (cpu.v1 !== null && cpu.v1 !== undefined) cpu.v1 = String(cpu.v1);
|
|
||||||
if (cpu.v2 !== null && cpu.v2 !== undefined) cpu.v2 = String(cpu.v2);
|
|
||||||
|
|
||||||
this.value.value = JSON.stringify({ ...this.resources, cpu });
|
|
||||||
},
|
|
||||||
|
|
||||||
useDefault() {
|
|
||||||
if (this.value?.default) {
|
|
||||||
this.resources = this.parseJSON(this.value.default) || {};
|
|
||||||
this.update();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<Banner
|
|
||||||
v-if="parseError"
|
|
||||||
color="error"
|
|
||||||
>
|
|
||||||
{{ parseError }}
|
|
||||||
</Banner>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col span-12">
|
|
||||||
<UnitInput
|
|
||||||
v-model:value="resources.cpu.v1"
|
|
||||||
:label="t('harvester.setting.instanceManagerResources.v1')"
|
|
||||||
suffix="%"
|
|
||||||
:delay="0"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
max="100"
|
|
||||||
required
|
|
||||||
:mode="mode"
|
|
||||||
class="mb-20"
|
|
||||||
@update:value="update"
|
|
||||||
/>
|
|
||||||
<UnitInput
|
|
||||||
v-model:value="resources.cpu.v2"
|
|
||||||
:label="t('harvester.setting.instanceManagerResources.v2')"
|
|
||||||
suffix="%"
|
|
||||||
:delay="0"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
max="100"
|
|
||||||
required
|
|
||||||
:mode="mode"
|
|
||||||
class="mb-20"
|
|
||||||
@update:value="update"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -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>
|
|
||||||
@@ -1,371 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
|
|
||||||
import { RadioGroup } from '@components/Form/Radio';
|
|
||||||
import ArrayList from '@shell/components/form/ArrayList';
|
|
||||||
import { isValidCIDR } from '@shell/utils/validators/cidr';
|
|
||||||
import { _EDIT } from '@shell/config/query-params';
|
|
||||||
import { Banner } from '@components/Banner';
|
|
||||||
import { allHash } from '@shell/utils/promise';
|
|
||||||
import { HCI } from '../../types';
|
|
||||||
import { NETWORK_TYPE } from '../../config/types';
|
|
||||||
|
|
||||||
const { L2VLAN, UNTAGGED } = NETWORK_TYPE;
|
|
||||||
const SHARE_STORAGE_NETWORK = 'share-storage-network';
|
|
||||||
const NETWORK = 'network';
|
|
||||||
|
|
||||||
const DEFAULT_DEDICATED_NETWORK = {
|
|
||||||
vlan: '',
|
|
||||||
clusterNetwork: '',
|
|
||||||
range: '',
|
|
||||||
exclude: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'RwxNetworkSetting',
|
|
||||||
|
|
||||||
components: {
|
|
||||||
RadioGroup,
|
|
||||||
Banner,
|
|
||||||
ArrayList,
|
|
||||||
LabeledInput,
|
|
||||||
LabeledSelect,
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
|
||||||
registerBeforeHook: {
|
|
||||||
type: Function,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
mode: {
|
|
||||||
type: String,
|
|
||||||
default: _EDIT,
|
|
||||||
},
|
|
||||||
|
|
||||||
value: {
|
|
||||||
type: Object,
|
|
||||||
default: () => {
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
await allHash({
|
|
||||||
clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }),
|
|
||||||
vlanStatus: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VLAN_STATUS }),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
let enabled = false; // enabled / disabled options
|
|
||||||
let shareStorageNetwork = false; // shareStorageNetwork / dedicatedRwxNetwork options
|
|
||||||
let dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
|
|
||||||
let networkType = L2VLAN;
|
|
||||||
let exclude = [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
const parsedValue = JSON.parse(this.value.value || this.value.default || '{}');
|
|
||||||
const parsedNetwork = parsedValue?.[NETWORK] || parsedValue || {};
|
|
||||||
|
|
||||||
if (parsedValue && typeof parsedValue === 'object') {
|
|
||||||
shareStorageNetwork = !!parsedValue[SHARE_STORAGE_NETWORK];
|
|
||||||
networkType = 'vlan' in parsedNetwork ? L2VLAN : UNTAGGED;
|
|
||||||
dedicatedNetwork = {
|
|
||||||
vlan: parsedNetwork.vlan || '',
|
|
||||||
clusterNetwork: parsedNetwork.clusterNetwork || '',
|
|
||||||
range: parsedNetwork.range || '',
|
|
||||||
};
|
|
||||||
exclude = parsedNetwork?.exclude?.toString().split(',') || [];
|
|
||||||
enabled = shareStorageNetwork || !!(parsedNetwork.vlan || parsedNetwork.clusterNetwork || parsedNetwork.range);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
enabled = false;
|
|
||||||
shareStorageNetwork = false;
|
|
||||||
dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
enabled,
|
|
||||||
shareStorageNetwork,
|
|
||||||
dedicatedNetwork,
|
|
||||||
networkType,
|
|
||||||
exclude,
|
|
||||||
defaultAddValue: '',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
if (this.registerBeforeHook) {
|
|
||||||
this.registerBeforeHook(this.willSave, 'willSave');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
showDedicatedNetworkConfig() {
|
|
||||||
return this.enabled && !this.shareStorageNetwork;
|
|
||||||
},
|
|
||||||
|
|
||||||
showVlan() {
|
|
||||||
return this.networkType === L2VLAN;
|
|
||||||
},
|
|
||||||
|
|
||||||
networkTypes() {
|
|
||||||
return [L2VLAN, UNTAGGED];
|
|
||||||
},
|
|
||||||
|
|
||||||
clusterNetworkOptions() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
|
|
||||||
const clusterNetworksOptions = this.networkType === UNTAGGED ? clusterNetworks.filter((network) => network.id !== 'mgmt') : clusterNetworks;
|
|
||||||
|
|
||||||
return clusterNetworksOptions.map((network) => {
|
|
||||||
const disabled = !network.isReadyForStorageNetwork;
|
|
||||||
|
|
||||||
return {
|
|
||||||
label: disabled ? `${ network.id } (${ this.t('generic.notReady') })` : network.id,
|
|
||||||
value: network.id,
|
|
||||||
disabled,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
onUpdateEnabled() {
|
|
||||||
if (!this.enabled) {
|
|
||||||
this.shareStorageNetwork = false;
|
|
||||||
this.dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
|
|
||||||
}
|
|
||||||
|
|
||||||
this.update();
|
|
||||||
},
|
|
||||||
|
|
||||||
onUpdateNetworkType() {
|
|
||||||
if (this.shareStorageNetwork) {
|
|
||||||
this.dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
|
|
||||||
}
|
|
||||||
|
|
||||||
this.update();
|
|
||||||
},
|
|
||||||
|
|
||||||
onUpdateDedicatedType(neu) {
|
|
||||||
this.dedicatedNetwork.clusterNetwork = '';
|
|
||||||
|
|
||||||
if (neu === L2VLAN) {
|
|
||||||
this.dedicatedNetwork.vlan = '';
|
|
||||||
} else {
|
|
||||||
delete this.dedicatedNetwork.vlan;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.update();
|
|
||||||
},
|
|
||||||
|
|
||||||
inputVlan(neu) {
|
|
||||||
if (neu === '') {
|
|
||||||
this.dedicatedNetwork.vlan = '';
|
|
||||||
this.update();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newValue = Number(neu);
|
|
||||||
|
|
||||||
if (newValue > 4094) {
|
|
||||||
this.dedicatedNetwork.vlan = 4094;
|
|
||||||
} else if (newValue < 1) {
|
|
||||||
this.dedicatedNetwork.vlan = 1;
|
|
||||||
} else {
|
|
||||||
this.dedicatedNetwork.vlan = newValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.update();
|
|
||||||
},
|
|
||||||
|
|
||||||
useDefault() {
|
|
||||||
this.enabled = false;
|
|
||||||
this.shareStorageNetwork = false;
|
|
||||||
this.dedicatedNetwork = { ...DEFAULT_DEDICATED_NETWORK };
|
|
||||||
this.update();
|
|
||||||
},
|
|
||||||
|
|
||||||
update() {
|
|
||||||
const value = { [SHARE_STORAGE_NETWORK]: false };
|
|
||||||
|
|
||||||
if (this.enabled && this.shareStorageNetwork) {
|
|
||||||
value[SHARE_STORAGE_NETWORK] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.showDedicatedNetworkConfig) {
|
|
||||||
value[NETWORK] = {};
|
|
||||||
|
|
||||||
if (this.showVlan) {
|
|
||||||
value[NETWORK].vlan = this.dedicatedNetwork.vlan;
|
|
||||||
}
|
|
||||||
|
|
||||||
value[NETWORK].clusterNetwork = this.dedicatedNetwork.clusterNetwork;
|
|
||||||
value[NETWORK].range = this.dedicatedNetwork.range;
|
|
||||||
|
|
||||||
const excludeList = this.exclude.filter((ip) => ip);
|
|
||||||
|
|
||||||
if (Array.isArray(excludeList) && excludeList.length > 0) {
|
|
||||||
value[NETWORK].exclude = excludeList;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.value.value = JSON.stringify(value);
|
|
||||||
},
|
|
||||||
|
|
||||||
willSave() {
|
|
||||||
this.update();
|
|
||||||
|
|
||||||
if (!this.showDedicatedNetworkConfig) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
if (this.showVlan && !this.dedicatedNetwork.vlan) {
|
|
||||||
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.vlan') }, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.dedicatedNetwork.clusterNetwork) {
|
|
||||||
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.clusterNetwork') }, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.dedicatedNetwork.range) {
|
|
||||||
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.range.label') }, true));
|
|
||||||
} else if (!isValidCIDR(this.dedicatedNetwork.range)) {
|
|
||||||
errors.push(this.t('harvester.setting.storageNetwork.range.invalid', null, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.exclude) {
|
|
||||||
const hasInvalidCIDR = this.exclude.find((cidr) => {
|
|
||||||
return cidr && !isValidCIDR(cidr);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (hasInvalidCIDR) {
|
|
||||||
errors.push(this.t('harvester.setting.storageNetwork.exclude.invalid', null, true));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errors.length > 0) {
|
|
||||||
return Promise.reject(errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div :class="mode">
|
|
||||||
<Banner color="warning">
|
|
||||||
<t
|
|
||||||
k="harvester.setting.rwxNetwork.warning"
|
|
||||||
:raw="true"
|
|
||||||
/>
|
|
||||||
</Banner>
|
|
||||||
<RadioGroup
|
|
||||||
v-model:value="enabled"
|
|
||||||
class="mb-20"
|
|
||||||
name="rwx-network-enable"
|
|
||||||
:options="[true,false]"
|
|
||||||
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
|
||||||
@update:value="onUpdateEnabled"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<RadioGroup
|
|
||||||
v-if="enabled"
|
|
||||||
v-model:value="shareStorageNetwork"
|
|
||||||
class="mb-20"
|
|
||||||
name="rwx-network-type"
|
|
||||||
:options="[true,false]"
|
|
||||||
:labels="[t('harvester.setting.rwxNetwork.shareStorageNetwork'), t('harvester.setting.rwxNetwork.dedicatedRwxNetwork')]"
|
|
||||||
@update:value="onUpdateNetworkType"
|
|
||||||
/>
|
|
||||||
<Banner
|
|
||||||
v-if="shareStorageNetwork"
|
|
||||||
class="mb-20"
|
|
||||||
color="warning"
|
|
||||||
>
|
|
||||||
<t
|
|
||||||
k="harvester.setting.rwxNetwork.shareStorageNetworkWarning"
|
|
||||||
:raw="true"
|
|
||||||
/>
|
|
||||||
</Banner>
|
|
||||||
<template v-if="showDedicatedNetworkConfig">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="networkType"
|
|
||||||
class="mb-20"
|
|
||||||
:options="networkTypes"
|
|
||||||
:mode="mode"
|
|
||||||
:label="t('harvester.fields.type')"
|
|
||||||
required
|
|
||||||
@update:value="onUpdateDedicatedType"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LabeledInput
|
|
||||||
v-if="showVlan"
|
|
||||||
v-model:value.number="dedicatedNetwork.vlan"
|
|
||||||
type="number"
|
|
||||||
class="mb-20"
|
|
||||||
:mode="mode"
|
|
||||||
required
|
|
||||||
placeholder="e.g. 1 - 4094"
|
|
||||||
label-key="harvester.setting.storageNetwork.vlan"
|
|
||||||
@update:value="inputVlan"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="dedicatedNetwork.clusterNetwork"
|
|
||||||
label-key="harvester.setting.storageNetwork.clusterNetwork"
|
|
||||||
class="mb-20"
|
|
||||||
required
|
|
||||||
:options="clusterNetworkOptions"
|
|
||||||
@update:value="update"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="dedicatedNetwork.range"
|
|
||||||
class="mb-5"
|
|
||||||
:mode="mode"
|
|
||||||
required
|
|
||||||
:placeholder="t('harvester.setting.storageNetwork.range.placeholder')"
|
|
||||||
label-key="harvester.setting.storageNetwork.range.label"
|
|
||||||
@update:value="update"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ArrayList
|
|
||||||
v-model:value="exclude"
|
|
||||||
:show-header="true"
|
|
||||||
:default-add-value="defaultAddValue"
|
|
||||||
:mode="mode"
|
|
||||||
:add-label="t('harvester.setting.storageNetwork.exclude.addIp')"
|
|
||||||
class="mt-20"
|
|
||||||
@update:value="update"
|
|
||||||
>
|
|
||||||
<template #column-headers>
|
|
||||||
<div class="box mb-10">
|
|
||||||
<div class="key">
|
|
||||||
{{ t('harvester.setting.storageNetwork.exclude.label') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #columns="scope">
|
|
||||||
<div class="key">
|
|
||||||
<input
|
|
||||||
v-model="scope.row.value"
|
|
||||||
:placeholder="t('harvester.setting.storageNetwork.exclude.placeholder')"
|
|
||||||
@update:value="update"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</ArrayList>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -12,9 +12,6 @@ import { NODE } from '@shell/config/types';
|
|||||||
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 { NETWORK_TYPE } from '../../config/types';
|
|
||||||
|
|
||||||
const { L2VLAN, UNTAGGED } = NETWORK_TYPE;
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterEditStorageNetwork',
|
name: 'HarvesterEditStorageNetwork',
|
||||||
@@ -60,14 +57,11 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
let parsedDefaultValue = {};
|
let parsedDefaultValue = {};
|
||||||
let openVlan = false;
|
let openVlan = false;
|
||||||
let networkType = L2VLAN;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
parsedDefaultValue = JSON.parse(this.value.value);
|
parsedDefaultValue = JSON.parse(this.value.value);
|
||||||
networkType = 'vlan' in parsedDefaultValue ? L2VLAN : UNTAGGED; // backend doesn't provide networkType, so we check if vlan is provided instead
|
|
||||||
openVlan = true;
|
openVlan = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
networkType = L2VLAN;
|
|
||||||
parsedDefaultValue = {
|
parsedDefaultValue = {
|
||||||
vlan: '',
|
vlan: '',
|
||||||
clusterNetwork: '',
|
clusterNetwork: '',
|
||||||
@@ -79,7 +73,6 @@ export default {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
openVlan,
|
openVlan,
|
||||||
networkType,
|
|
||||||
errors: [],
|
errors: [],
|
||||||
exclude,
|
exclude,
|
||||||
parsedDefaultValue,
|
parsedDefaultValue,
|
||||||
@@ -94,35 +87,16 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
showVlan() {
|
|
||||||
return this.networkType === L2VLAN;
|
|
||||||
},
|
|
||||||
networkTypes() {
|
|
||||||
const types = [L2VLAN];
|
|
||||||
|
|
||||||
if (this.untaggedNetworkSettingEnabled) {
|
|
||||||
types.push(UNTAGGED);
|
|
||||||
}
|
|
||||||
|
|
||||||
return types;
|
|
||||||
},
|
|
||||||
storageNetworkExampleLink() {
|
storageNetworkExampleLink() {
|
||||||
const version = this.$store.getters['harvester-common/getServerVersion']();
|
const version = this.$store.getters['harvester-common/getServerVersion']();
|
||||||
|
|
||||||
return docLink(DOC.STORAGE_NETWORK_EXAMPLE, version);
|
return docLink(DOC.STORAGE_NETWORK_EXAMPLE, version);
|
||||||
},
|
},
|
||||||
|
|
||||||
untaggedNetworkSettingEnabled() {
|
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('untaggedNetworkSetting');
|
|
||||||
},
|
|
||||||
|
|
||||||
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) || [];
|
||||||
// untaggedNetwork filter out mgmt cluster network
|
|
||||||
const clusterNetworksOptions = this.networkType === UNTAGGED ? clusterNetworks.filter((net) => net.id !== 'mgmt') : clusterNetworks;
|
|
||||||
|
|
||||||
return clusterNetworksOptions.map((n) => {
|
return clusterNetworks.map((n) => {
|
||||||
const disabled = !n.isReadyForStorageNetwork;
|
const disabled = !n.isReadyForStorageNetwork;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -134,50 +108,7 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
|
||||||
networkType: {
|
|
||||||
handler(neu) {
|
|
||||||
this.parsedDefaultValue.clusterNetwork = '';
|
|
||||||
|
|
||||||
if (neu === L2VLAN) {
|
|
||||||
this.parsedDefaultValue.vlan = '';
|
|
||||||
} else {
|
|
||||||
delete this.parsedDefaultValue.vlan;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
deep: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
inputVlan(neu) {
|
|
||||||
if (neu === '') {
|
|
||||||
this.parsedDefaultValue.vlan = '';
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const newValue = Number(neu);
|
|
||||||
|
|
||||||
if (newValue > 4094) {
|
|
||||||
this.parsedDefaultValue.vlan = 4094;
|
|
||||||
} else if (newValue < 1) {
|
|
||||||
this.parsedDefaultValue.vlan = 1;
|
|
||||||
} else {
|
|
||||||
this.parsedDefaultValue.vlan = newValue;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
useDefault() {
|
|
||||||
this.openVlan = false;
|
|
||||||
this.networkType = L2VLAN;
|
|
||||||
this.parsedDefaultValue = {
|
|
||||||
vlan: '',
|
|
||||||
clusterNetwork: '',
|
|
||||||
range: '',
|
|
||||||
exclude: []
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
const exclude = this.exclude.filter((ip) => ip);
|
const exclude = this.exclude.filter((ip) => ip);
|
||||||
|
|
||||||
@@ -207,7 +138,7 @@ export default {
|
|||||||
errors.push(this.t('harvester.setting.storageNetwork.range.invalid', null, true));
|
errors.push(this.t('harvester.setting.storageNetwork.range.invalid', null, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.networkType === L2VLAN && !this.parsedDefaultValue.vlan) {
|
if (!this.parsedDefaultValue.vlan) {
|
||||||
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.vlan') }, true));
|
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.vlan') }, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,25 +191,13 @@ export default {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div v-if="openVlan">
|
<div v-if="openVlan">
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="networkType"
|
|
||||||
class="mb-20"
|
|
||||||
:options="networkTypes"
|
|
||||||
:mode="mode"
|
|
||||||
:label="t('harvester.fields.type')"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LabeledInput
|
<LabeledInput
|
||||||
v-if="showVlan"
|
|
||||||
v-model:value.number="parsedDefaultValue.vlan"
|
v-model:value.number="parsedDefaultValue.vlan"
|
||||||
type="number"
|
type="number"
|
||||||
class="mb-20"
|
class="mb-20"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
required
|
required
|
||||||
placeholder="e.g. 1 - 4094"
|
|
||||||
label-key="harvester.setting.storageNetwork.vlan"
|
label-key="harvester.setting.storageNetwork.vlan"
|
||||||
@update:value="inputVlan"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
|
|||||||
@@ -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,224 +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';
|
|
||||||
import { allHash } from '@shell/utils/promise';
|
|
||||||
import { NODE } from '@shell/config/types';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'HarvesterUpgradeConfig',
|
|
||||||
|
|
||||||
components: {
|
|
||||||
LabeledInput,
|
|
||||||
LabeledSelect,
|
|
||||||
RadioGroup
|
|
||||||
},
|
|
||||||
mixins: [CreateEditView],
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
const hash = { nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }) };
|
|
||||||
|
|
||||||
await allHash(hash);
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
let parseDefaultValue = {};
|
|
||||||
|
|
||||||
try {
|
|
||||||
parseDefaultValue = this.value.value ? JSON.parse(this.value.value) : JSON.parse(this.value.default);
|
|
||||||
} catch (error) {
|
|
||||||
parseDefaultValue = JSON.parse(this.value.default);
|
|
||||||
}
|
|
||||||
parseDefaultValue = this.normalizeValue(parseDefaultValue);
|
|
||||||
|
|
||||||
return {
|
|
||||||
parseDefaultValue,
|
|
||||||
errors: []
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
...mapGetters({ t: 'i18n/t' }),
|
|
||||||
strategyOptions() {
|
|
||||||
return [
|
|
||||||
{ value: 'sequential', label: 'sequential' },
|
|
||||||
{ value: 'skip', label: 'skip' },
|
|
||||||
{ value: 'parallel', label: 'parallel' }
|
|
||||||
];
|
|
||||||
},
|
|
||||||
nodeUpgradeOptions() {
|
|
||||||
return [
|
|
||||||
{ value: 'auto', label: 'auto' },
|
|
||||||
{ value: 'manual', label: 'manual' }
|
|
||||||
];
|
|
||||||
},
|
|
||||||
nodesOptions() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
const nodes = this.$store.getters[`${ inStore }/all`](NODE);
|
|
||||||
|
|
||||||
return nodes.map((node) => ({ value: node.id, label: node.name }));
|
|
||||||
},
|
|
||||||
showPauseNodes() {
|
|
||||||
return this.parseDefaultValue.nodeUpgradeOption?.strategy?.mode === 'manual';
|
|
||||||
},
|
|
||||||
resumeUpgradePausedNodeEnabled() {
|
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('resumeUpgradePausedNode');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
this.update();
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
normalizeValue(obj) {
|
|
||||||
// handle nodeUpgradeOption.strategy
|
|
||||||
if (obj?.nodeUpgradeOption?.strategy?.mode === 'auto') {
|
|
||||||
delete obj.nodeUpgradeOption.strategy.pauseNodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (obj?.nodeUpgradeOption?.strategy?.mode === 'manual') {
|
|
||||||
if (!Array.isArray(obj.nodeUpgradeOption.strategy.pauseNodes)) {
|
|
||||||
obj.nodeUpgradeOption.strategy.pauseNodes = this.nodesOptions.map((node) => node.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle imagePreloadOption.strategy
|
|
||||||
if (!obj.imagePreloadOption) {
|
|
||||||
obj.imagePreloadOption = { strategy: { type: 'sequential' } };
|
|
||||||
}
|
|
||||||
if (!obj.imagePreloadOption.strategy) {
|
|
||||||
obj.imagePreloadOption.strategy = { type: 'sequential' };
|
|
||||||
}
|
|
||||||
if (!obj.imagePreloadOption.strategy.type) {
|
|
||||||
obj.imagePreloadOption.strategy.type = 'sequential';
|
|
||||||
}
|
|
||||||
// Only set concurrency if type is 'parallel'
|
|
||||||
if (obj.imagePreloadOption.strategy.type === 'parallel') {
|
|
||||||
if (typeof obj.imagePreloadOption.strategy.concurrency !== 'number') {
|
|
||||||
obj.imagePreloadOption.strategy.concurrency = 0;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
delete obj.imagePreloadOption.strategy.concurrency;
|
|
||||||
}
|
|
||||||
if (typeof obj.restoreVM !== 'boolean') {
|
|
||||||
obj.restoreVM = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return obj;
|
|
||||||
},
|
|
||||||
update() {
|
|
||||||
try {
|
|
||||||
// Clone to avoid mutating the form state
|
|
||||||
const valueToSave = JSON.parse(JSON.stringify(this.parseDefaultValue));
|
|
||||||
|
|
||||||
if (valueToSave.imagePreloadOption && valueToSave.imagePreloadOption.strategy) {
|
|
||||||
if (valueToSave.imagePreloadOption.strategy.type !== 'parallel') {
|
|
||||||
delete valueToSave.imagePreloadOption.strategy.concurrency;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.value['value'] = JSON.stringify(valueToSave, null, 2);
|
|
||||||
this.errors = [];
|
|
||||||
} catch (e) {
|
|
||||||
this.errors = ['Invalid JSON'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
value: {
|
|
||||||
handler(neu) {
|
|
||||||
let parseDefaultValue;
|
|
||||||
|
|
||||||
try {
|
|
||||||
parseDefaultValue = JSON.parse(neu.value);
|
|
||||||
} catch (err) {
|
|
||||||
parseDefaultValue = JSON.parse(this.value.default);
|
|
||||||
}
|
|
||||||
parseDefaultValue = this.normalizeValue(parseDefaultValue);
|
|
||||||
this['parseDefaultValue'] = parseDefaultValue;
|
|
||||||
this.update();
|
|
||||||
},
|
|
||||||
deep: true
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="row"
|
|
||||||
@input="update"
|
|
||||||
>
|
|
||||||
<div class="col span-12">
|
|
||||||
<label class="mb-5"><b>{{ t('harvester.setting.upgrade.imagePreloadStrategy') }}</b></label>
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="parseDefaultValue.imagePreloadOption.strategy.type"
|
|
||||||
class="mb-20"
|
|
||||||
:mode="mode"
|
|
||||||
:label="t('harvester.setting.upgrade.strategyType')"
|
|
||||||
:options="strategyOptions"
|
|
||||||
@update:value="update"
|
|
||||||
/>
|
|
||||||
<LabeledInput
|
|
||||||
v-if="parseDefaultValue.imagePreloadOption.strategy.type === 'parallel'"
|
|
||||||
v-model:value.number="parseDefaultValue.imagePreloadOption.strategy.concurrency"
|
|
||||||
class="mb-20"
|
|
||||||
:mode="mode"
|
|
||||||
:label="t('harvester.setting.upgrade.concurrency')"
|
|
||||||
min="0"
|
|
||||||
type="number"
|
|
||||||
/>
|
|
||||||
<label class="mb-5"><b>{{ t('harvester.setting.upgrade.restoreVM') }}</b></label>
|
|
||||||
<RadioGroup
|
|
||||||
v-model:value="parseDefaultValue.restoreVM"
|
|
||||||
class="mb-20"
|
|
||||||
name="restoreVM"
|
|
||||||
:options="[true, false]"
|
|
||||||
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
|
||||||
@update:value="update"
|
|
||||||
/>
|
|
||||||
<div v-if="resumeUpgradePausedNodeEnabled">
|
|
||||||
<label class="mb-5"><b>{{ t('harvester.setting.upgrade.nodeUpgradeOption') }}</b></label>
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="parseDefaultValue.nodeUpgradeOption.strategy.mode"
|
|
||||||
class="mb-20 label-select"
|
|
||||||
:mode="mode"
|
|
||||||
:label="t('harvester.setting.upgrade.strategy')"
|
|
||||||
:options="nodeUpgradeOptions"
|
|
||||||
@update:value="update"
|
|
||||||
/>
|
|
||||||
<LabeledSelect
|
|
||||||
v-if="showPauseNodes"
|
|
||||||
v-model:value="parseDefaultValue.nodeUpgradeOption.strategy.pauseNodes"
|
|
||||||
class="mb-20 label-select"
|
|
||||||
:clearable="true"
|
|
||||||
:multiple="true"
|
|
||||||
:mode="mode"
|
|
||||||
:label="t('harvester.setting.upgrade.pauseNodes')"
|
|
||||||
:options="nodesOptions"
|
|
||||||
@update:value="update"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="errors.length"
|
|
||||||
class="error"
|
|
||||||
>
|
|
||||||
{{ errors[0] }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.error {
|
|
||||||
color: #d9534f;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -69,15 +69,6 @@ export default {
|
|||||||
:mode="mode"
|
:mode="mode"
|
||||||
label-key="harvester.setting.vmForceDeletionPolicy.period"
|
label-key="harvester.setting.vmForceDeletionPolicy.period"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LabeledInput
|
|
||||||
v-if="parseDefaultValue.enable"
|
|
||||||
v-model:value.number="parseDefaultValue.vmMigrationTimeout"
|
|
||||||
class="mb-20"
|
|
||||||
type="number"
|
|
||||||
:mode="mode"
|
|
||||||
label-key="harvester.setting.vmForceDeletionPolicy.vmMigrationTimeout"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -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,86 +1,70 @@
|
|||||||
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 = [
|
||||||
],
|
...featuresV132,
|
||||||
'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': [],
|
// https://github.com/harvester/dashboard/releases/tag/v1.4.2
|
||||||
'v1.6.0': [
|
const featuresV142 = [
|
||||||
'customSupportBundle',
|
...featuresV141,
|
||||||
'csiOnlineExpandValidation',
|
'refreshIntervalInSecond',
|
||||||
'vmNetworkMigration',
|
'allowEmptySnapshotClassName'
|
||||||
'kubeovnVpcSubnet',
|
];
|
||||||
'rancherClusterSetting',
|
|
||||||
'cpuMemoryHotplug',
|
// https://github.com/harvester/dashboard/releases/tag/v1.4.3-rc1
|
||||||
'cdiSettings',
|
const featuresV143 = [
|
||||||
'vmCloneRunStrategy',
|
...featuresV142,
|
||||||
],
|
];
|
||||||
'v1.6.1': [],
|
|
||||||
'v1.7.0': [
|
// https://github.com/harvester/harvester-ui-extension/releases/tag/v1.5.0
|
||||||
'vmMachineTypeAuto',
|
const featuresV150 = [
|
||||||
'lhV2VolExpansion',
|
...featuresV142
|
||||||
'l2VlanTrunkMode',
|
];
|
||||||
'kubevirtMigration',
|
|
||||||
'hotplugNic',
|
export const RELEASE_FEATURES = {
|
||||||
'resumeUpgradePausedNode',
|
'v1.3.0': featuresV130,
|
||||||
],
|
'v1.3.1': featuresV131,
|
||||||
'v1.7.1': [],
|
'v1.3.2': featuresV132,
|
||||||
'v1.8.0': [
|
'v1.3.3': featuresV133,
|
||||||
'hotplugCdRom',
|
'v1.4.0': featuresV140,
|
||||||
'supportBundleFileNameSetting',
|
'v1.4.1': featuresV141,
|
||||||
'clusterRegistrationTLSVerify',
|
'v1.4.2': featuresV142,
|
||||||
'vGPUAsPCIDevice',
|
'v1.4.3': featuresV143,
|
||||||
'instanceManagerResourcesSetting',
|
'v1.5.0': featuresV150
|
||||||
'rwxNetworkSetting',
|
|
||||||
'createPVCWithDataVolume'
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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 {
|
||||||
@@ -33,23 +31,8 @@ import {
|
|||||||
FINGERPRINT,
|
FINGERPRINT,
|
||||||
IMAGE_PROGRESS,
|
IMAGE_PROGRESS,
|
||||||
SNAPSHOT_TARGET_VOLUME,
|
SNAPSHOT_TARGET_VOLUME,
|
||||||
IMAGE_VIRTUAL_SIZE,
|
IMAGE_VIRTUAL_SIZE
|
||||||
IMAGE_STORAGE_CLASS,
|
|
||||||
HARVESTER_DESCRIPTION,
|
|
||||||
VM_IMPORT_SOURCE_VM,
|
|
||||||
VM_IMPORT_SOURCE_CLUSTER,
|
|
||||||
VM_IMPORT_STATUS,
|
|
||||||
VM_IMPORT_SOURCE_V_DC,
|
|
||||||
VM_IMPORT_SOURCE_V_ENDPOINT,
|
|
||||||
VM_IMPORT_SOURCE_V_STATUS,
|
|
||||||
VM_IMPORT_SOURCE_O_REGION,
|
|
||||||
VM_IMPORT_SOURCE_O_ENDPOINT,
|
|
||||||
VM_IMPORT_SOURCE_O_STATUS,
|
|
||||||
VM_IMPORT_SOURCE_OVA_URL,
|
|
||||||
VM_IMPORT_SOURCE_OVA_STATUS,
|
|
||||||
} from './table-headers';
|
} from './table-headers';
|
||||||
import { ADD_ONS } from './harvester-map';
|
|
||||||
import { registerAddonSideNav } from '../utils/dynamic-nav';
|
|
||||||
|
|
||||||
const TEMPLATE = HCI.VM_VERSION;
|
const TEMPLATE = HCI.VM_VERSION;
|
||||||
const MONITORING_GROUP = 'Monitoring & Logging::Monitoring';
|
const MONITORING_GROUP = 'Monitoring & Logging::Monitoring';
|
||||||
@@ -81,6 +64,7 @@ export function init($plugin, store) {
|
|||||||
configureType,
|
configureType,
|
||||||
virtualType,
|
virtualType,
|
||||||
weightGroup,
|
weightGroup,
|
||||||
|
weightType,
|
||||||
} = $plugin.DSL(store, PRODUCT_NAME);
|
} = $plugin.DSL(store, PRODUCT_NAME);
|
||||||
|
|
||||||
const isSingleVirtualCluster = process.env.rancherEnv === PRODUCT_NAME;
|
const isSingleVirtualCluster = process.env.rancherEnv === PRODUCT_NAME;
|
||||||
@@ -167,7 +151,7 @@ export function init($plugin, store) {
|
|||||||
group: 'Root',
|
group: 'Root',
|
||||||
name: HCI.HOST,
|
name: HCI.HOST,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
weight: 499,
|
weight: 399,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
params: { resource: HCI.HOST }
|
params: { resource: HCI.HOST }
|
||||||
@@ -199,7 +183,7 @@ export function init($plugin, store) {
|
|||||||
group: 'root',
|
group: 'root',
|
||||||
name: HCI.VM,
|
name: HCI.VM,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
weight: 498,
|
weight: 299,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
params: { resource: HCI.VM }
|
params: { resource: HCI.VM }
|
||||||
@@ -207,142 +191,6 @@ export function init($plugin, store) {
|
|||||||
exact: false
|
exact: false
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===========================================================================
|
|
||||||
// VM Import Controller UI Flow
|
|
||||||
// ===========================================================================
|
|
||||||
// Define group (Hidden by default)
|
|
||||||
weightGroup('vmimport', 0, false);
|
|
||||||
|
|
||||||
// VirtualMachineImport
|
|
||||||
headers(HCI.VMIMPORT, [
|
|
||||||
STATE,
|
|
||||||
NAME_COL,
|
|
||||||
NAMESPACE_COL,
|
|
||||||
VM_IMPORT_SOURCE_VM,
|
|
||||||
VM_IMPORT_SOURCE_CLUSTER,
|
|
||||||
VM_IMPORT_STATUS,
|
|
||||||
AGE
|
|
||||||
]);
|
|
||||||
configureType(HCI.VMIMPORT, {
|
|
||||||
resource: HCI.VMIMPORT,
|
|
||||||
resourceDetail: HCI.VMIMPORT,
|
|
||||||
resourceEdit: HCI.VMIMPORT,
|
|
||||||
location: {
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
|
||||||
params: { resource: HCI.VMIMPORT }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
virtualType({ // needed to avoid 404 on refresh when combined with registerAddonSideNav()
|
|
||||||
name: HCI.VMIMPORT,
|
|
||||||
labelKey: 'harvester.addons.vmImport.labels.vmimport',
|
|
||||||
group: 'vmimport',
|
|
||||||
namespaced: true,
|
|
||||||
route: {
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
|
||||||
params: { resource: HCI.VMIMPORT }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Source: VMware
|
|
||||||
headers(HCI.VMIMPORT_SOURCE_V, [
|
|
||||||
STATE,
|
|
||||||
NAME_COL,
|
|
||||||
VM_IMPORT_SOURCE_V_ENDPOINT,
|
|
||||||
VM_IMPORT_SOURCE_V_DC,
|
|
||||||
VM_IMPORT_SOURCE_V_STATUS,
|
|
||||||
AGE
|
|
||||||
]);
|
|
||||||
configureType(HCI.VMIMPORT_SOURCE_V, {
|
|
||||||
resource: HCI.VMIMPORT_SOURCE_V,
|
|
||||||
resourceDetail: HCI.VMIMPORT_SOURCE_V,
|
|
||||||
resourceEdit: HCI.VMIMPORT_SOURCE_V,
|
|
||||||
location: {
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
|
||||||
params: { resource: HCI.VMIMPORT_SOURCE_V }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
virtualType({ // needed to avoid 404 on refresh when combined with registerAddonSideNav()
|
|
||||||
name: HCI.VMIMPORT_SOURCE_V,
|
|
||||||
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceVMWare',
|
|
||||||
group: 'vmimport',
|
|
||||||
namespaced: true,
|
|
||||||
route: {
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
|
||||||
params: { resource: HCI.VMIMPORT_SOURCE_V }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Source: OpenStack
|
|
||||||
headers(HCI.VMIMPORT_SOURCE_O, [
|
|
||||||
STATE,
|
|
||||||
NAME_COL,
|
|
||||||
VM_IMPORT_SOURCE_O_ENDPOINT,
|
|
||||||
VM_IMPORT_SOURCE_O_REGION,
|
|
||||||
VM_IMPORT_SOURCE_O_STATUS,
|
|
||||||
AGE
|
|
||||||
]);
|
|
||||||
configureType(HCI.VMIMPORT_SOURCE_O, {
|
|
||||||
resource: HCI.VMIMPORT_SOURCE_O,
|
|
||||||
resourceDetail: HCI.VMIMPORT_SOURCE_O,
|
|
||||||
resourceEdit: HCI.VMIMPORT_SOURCE_O,
|
|
||||||
location: {
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
|
||||||
params: { resource: HCI.VMIMPORT_SOURCE_O }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
virtualType({ // needed to avoid 404 on refresh when combined with registerAddonSideNav()
|
|
||||||
name: HCI.VMIMPORT_SOURCE_O,
|
|
||||||
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOpenStack',
|
|
||||||
group: 'vmimport',
|
|
||||||
namespaced: true,
|
|
||||||
route: {
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
|
||||||
params: { resource: HCI.VMIMPORT_SOURCE_O }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Source: OVA
|
|
||||||
headers(HCI.VMIMPORT_SOURCE_OVA, [
|
|
||||||
STATE,
|
|
||||||
NAME_COL,
|
|
||||||
VM_IMPORT_SOURCE_OVA_URL,
|
|
||||||
VM_IMPORT_SOURCE_OVA_STATUS,
|
|
||||||
AGE
|
|
||||||
]);
|
|
||||||
configureType(HCI.VMIMPORT_SOURCE_OVA, {
|
|
||||||
resource: HCI.VMIMPORT_SOURCE_OVA,
|
|
||||||
resourceDetail: HCI.VMIMPORT_SOURCE_OVA,
|
|
||||||
resourceEdit: HCI.VMIMPORT_SOURCE_OVA,
|
|
||||||
location: {
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
|
||||||
params: { resource: HCI.VMIMPORT_SOURCE_OVA }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
virtualType({ // needed to avoid 404 on refresh when combined with registerAddonSideNav()
|
|
||||||
name: HCI.VMIMPORT_SOURCE_OVA,
|
|
||||||
labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOVA',
|
|
||||||
group: 'vmimport',
|
|
||||||
namespaced: true,
|
|
||||||
route: {
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
|
||||||
params: { resource: HCI.VMIMPORT_SOURCE_OVA }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Enable SideNav based on Addon Status
|
|
||||||
registerAddonSideNav(store, PRODUCT_NAME, {
|
|
||||||
addonName: ADD_ONS.VM_IMPORT_CONTROLLER,
|
|
||||||
resourceType: HCI.ADD_ONS,
|
|
||||||
navGroup: 'vmimport',
|
|
||||||
types: [
|
|
||||||
HCI.VMIMPORT_SOURCE_V,
|
|
||||||
HCI.VMIMPORT_SOURCE_O,
|
|
||||||
HCI.VMIMPORT_SOURCE_OVA,
|
|
||||||
HCI.VMIMPORT
|
|
||||||
]
|
|
||||||
});
|
|
||||||
// ===========================================================================
|
|
||||||
|
|
||||||
basicType([HCI.VOLUME]);
|
basicType([HCI.VOLUME]);
|
||||||
configureType(HCI.VOLUME, {
|
configureType(HCI.VOLUME, {
|
||||||
location: {
|
location: {
|
||||||
@@ -360,7 +208,7 @@ export function init($plugin, store) {
|
|||||||
ifHaveType: PVC,
|
ifHaveType: PVC,
|
||||||
name: HCI.VOLUME,
|
name: HCI.VOLUME,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
weight: 497,
|
weight: 199,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
params: { resource: HCI.VOLUME }
|
params: { resource: HCI.VOLUME }
|
||||||
@@ -373,8 +221,6 @@ export function init($plugin, store) {
|
|||||||
STATE,
|
STATE,
|
||||||
NAME_COL,
|
NAME_COL,
|
||||||
NAMESPACE_COL,
|
NAMESPACE_COL,
|
||||||
HARVESTER_DESCRIPTION,
|
|
||||||
IMAGE_STORAGE_CLASS,
|
|
||||||
IMAGE_PROGRESS,
|
IMAGE_PROGRESS,
|
||||||
IMAGE_DOWNLOAD_SIZE,
|
IMAGE_DOWNLOAD_SIZE,
|
||||||
IMAGE_VIRTUAL_SIZE,
|
IMAGE_VIRTUAL_SIZE,
|
||||||
@@ -386,7 +232,7 @@ export function init($plugin, store) {
|
|||||||
group: 'root',
|
group: 'root',
|
||||||
name: HCI.IMAGE,
|
name: HCI.IMAGE,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
weight: 496,
|
weight: 198,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
params: { resource: HCI.IMAGE }
|
params: { resource: HCI.IMAGE }
|
||||||
@@ -401,19 +247,19 @@ export function init($plugin, store) {
|
|||||||
group: 'root',
|
group: 'root',
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
name: 'projects-namespaces',
|
name: 'projects-namespaces',
|
||||||
weight: 495,
|
weight: 98,
|
||||||
route: { name: `${ PRODUCT_NAME }-c-cluster-projectsnamespaces` },
|
route: { name: `${ PRODUCT_NAME }-c-cluster-projectsnamespaces` },
|
||||||
exact: true,
|
exact: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isSingleVirtualCluster) {
|
if (isSingleVirtualCluster) {
|
||||||
headers(NAMESPACE, [STATE, NAME_UNLINKED, DESCRIPTION, AGE]);
|
headers(NAMESPACE, [STATE, NAME_UNLINKED, AGE]);
|
||||||
basicType([NAMESPACE]);
|
basicType([NAMESPACE]);
|
||||||
virtualType({
|
virtualType({
|
||||||
labelKey: 'harvester.namespace.label',
|
labelKey: 'harvester.namespace.label',
|
||||||
name: NAMESPACE,
|
name: NAMESPACE,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
weight: 495,
|
weight: 89,
|
||||||
route: {
|
route: {
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||||
params: { resource: NAMESPACE }
|
params: { resource: NAMESPACE }
|
||||||
@@ -573,8 +419,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,
|
||||||
],
|
],
|
||||||
@@ -591,8 +435,9 @@ export function init($plugin, store) {
|
|||||||
'backupAndSnapshot'
|
'backupAndSnapshot'
|
||||||
);
|
);
|
||||||
|
|
||||||
weightGroup('networks', 494, true);
|
weightGroup('networks', 300, true);
|
||||||
weightGroup('backupAndSnapshot', 493, true);
|
weightType(NAMESPACE, 299, true);
|
||||||
|
weightGroup('backupAndSnapshot', 289, true);
|
||||||
|
|
||||||
basicType(
|
basicType(
|
||||||
[
|
[
|
||||||
@@ -604,7 +449,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,
|
||||||
@@ -701,36 +545,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: {
|
||||||
@@ -997,26 +811,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,6 +1,3 @@
|
|||||||
import { HCI } from '../types';
|
|
||||||
|
|
||||||
// TODO: delete this not used variable
|
|
||||||
export const MemoryUnit = [{
|
export const MemoryUnit = [{
|
||||||
label: 'Mi',
|
label: 'Mi',
|
||||||
value: 'Mi'
|
value: 'Mi'
|
||||||
@@ -39,12 +36,6 @@ export const VOLUME_TYPE = [{
|
|||||||
value: 'cd-rom'
|
value: 'cd-rom'
|
||||||
}];
|
}];
|
||||||
|
|
||||||
export const VOLUME_HOTPLUG_ACTION = {
|
|
||||||
INSERT_CDROM_IMAGE: 'INSERT_CDROM_IMAGE',
|
|
||||||
EJECT_CDROM_IMAGE: 'EJECT_CDROM_IMAGE',
|
|
||||||
DETACH_DISK: 'DETACH_DISK'
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ACCESS_CREDENTIALS = {
|
export const ACCESS_CREDENTIALS = {
|
||||||
RESET_PWD: 'userPassword',
|
RESET_PWD: 'userPassword',
|
||||||
INJECT_SSH: 'sshPublicKey'
|
INJECT_SSH: 'sshPublicKey'
|
||||||
@@ -78,7 +69,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 = {
|
||||||
@@ -89,14 +79,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
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -7,78 +7,60 @@ export const LABELS_TO_IGNORE_REGEX = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const HCI = {
|
export const HCI = {
|
||||||
CLOUD_INIT: 'harvesterhci.io/cloud-init-template',
|
CLOUD_INIT: 'harvesterhci.io/cloud-init-template',
|
||||||
CURRENT_IP: 'rke2.io/internal-ip',
|
CURRENT_IP: 'rke2.io/internal-ip',
|
||||||
IMAGE_ID: 'harvesterhci.io/imageId',
|
IMAGE_ID: 'harvesterhci.io/imageId',
|
||||||
SSH_NAMES: 'harvesterhci.io/sshNames',
|
SSH_NAMES: 'harvesterhci.io/sshNames',
|
||||||
NETWORK_IPS: 'network.harvesterhci.io/ips',
|
NETWORK_IPS: 'network.harvesterhci.io/ips',
|
||||||
TEMPLATE_VERSION_CUSTOM_NAME: 'template-version.harvesterhci.io/customName',
|
TEMPLATE_VERSION_CUSTOM_NAME: 'template-version.harvesterhci.io/customName',
|
||||||
CREATOR: 'harvesterhci.io/creator',
|
CREATOR: 'harvesterhci.io/creator',
|
||||||
OS: 'harvesterhci.io/os',
|
OS: 'harvesterhci.io/os',
|
||||||
GOLDEN_IMAGE: 'harvesterhci.io/goldenImage',
|
NETWORK_TYPE: 'network.harvesterhci.io/type',
|
||||||
NETWORK_TYPE: 'network.harvesterhci.io/type',
|
VM_NAME: 'harvesterhci.io/vmName',
|
||||||
VM_NAME: 'harvesterhci.io/vmName',
|
VM_NAME_PREFIX: 'harvesterhci.io/vmNamePrefix',
|
||||||
VM_NAME_PREFIX: 'harvesterhci.io/vmNamePrefix',
|
VM_RESERVED_MEMORY: 'harvesterhci.io/reservedMemory',
|
||||||
VM_RESERVED_MEMORY: 'harvesterhci.io/reservedMemory',
|
MAINTENANCE_STATUS: 'harvesterhci.io/maintain-status',
|
||||||
MAINTENANCE_STATUS: 'harvesterhci.io/maintain-status',
|
HOST_CUSTOM_NAME: 'harvesterhci.io/host-custom-name',
|
||||||
HOST_CUSTOM_NAME: 'harvesterhci.io/host-custom-name',
|
HOST_CONSOLE_URL: 'harvesterhci.io/host-console-url',
|
||||||
HOST_CONSOLE_URL: 'harvesterhci.io/host-console-url',
|
RESTORE_NAME: 'restore.harvesterhci.io/name',
|
||||||
RESTORE_NAME: 'restore.harvesterhci.io/name',
|
NODE_ROLE_MASTER: 'node-role.kubernetes.io/master',
|
||||||
NODE_ROLE_MASTER: 'node-role.kubernetes.io/master',
|
NODE_ROLE_CONTROL_PLANE: 'node-role.kubernetes.io/control-plane',
|
||||||
NODE_ROLE_CONTROL_PLANE: 'node-role.kubernetes.io/control-plane',
|
NODE_ROLE_ETCD: 'node-role.harvesterhci.io/witness',
|
||||||
NODE_ROLE_ETCD: 'node-role.harvesterhci.io/witness',
|
PROMOTE_STATUS: 'harvesterhci.io/promote-status',
|
||||||
PROMOTE_STATUS: 'harvesterhci.io/promote-status',
|
MIGRATION_STATE: 'harvesterhci.io/migrationState',
|
||||||
CLONE_BACKEND_STORAGE_STATUS: 'harvesterhci.io/clone-backend-storage-status',
|
VOLUME_CLAIM_TEMPLATE: 'harvesterhci.io/volumeClaimTemplates',
|
||||||
MIGRATION_STATE: 'harvesterhci.io/migrationState',
|
IMAGE_NAME: 'harvesterhci.io/image-name',
|
||||||
VOLUME_CLAIM_TEMPLATE: 'harvesterhci.io/volumeClaimTemplates',
|
INIT_IP: 'etcd.rke2.cattle.io/node-address',
|
||||||
IMAGE_NAME: 'harvesterhci.io/image-name',
|
NODE_SCHEDULABLE: 'kubevirt.io/schedulable',
|
||||||
INIT_IP: 'etcd.rke2.cattle.io/node-address',
|
NETWORK_ROUTE: 'network.harvesterhci.io/route',
|
||||||
NODE_SCHEDULABLE: 'kubevirt.io/schedulable',
|
MATCHED_NODES: 'network.harvesterhci.io/matched-nodes',
|
||||||
NETWORK_ROUTE: 'network.harvesterhci.io/route',
|
OS_UPGRADE_IMAGE: 'harvesterhci.io/os-upgrade-image',
|
||||||
MATCHED_NODES: 'network.harvesterhci.io/matched-nodes',
|
LATEST_UPGRADE: 'harvesterhci.io/latestUpgrade',
|
||||||
UPGRADE: 'harvesterhci.io/upgrade',
|
UPGRADE_STATE: 'harvesterhci.io/upgradeState',
|
||||||
OS_UPGRADE_IMAGE: 'harvesterhci.io/os-upgrade-image',
|
REAY_MESSAGE: 'harvesterhci.io/read-message',
|
||||||
LATEST_UPGRADE: 'harvesterhci.io/latestUpgrade',
|
DYNAMIC_SSHKEYS_NAMES: 'harvesterhci.io/dynamic-ssh-key-names',
|
||||||
UPGRADE_STATE: 'harvesterhci.io/upgradeState',
|
DYNAMIC_SSHKEYS_USERS: 'harvesterhci.io/dynamic-ssh-key-users',
|
||||||
REAY_MESSAGE: 'harvesterhci.io/read-message',
|
IMAGE_SUFFIX: 'harvesterhci.io/image-type',
|
||||||
DYNAMIC_SSHKEYS_NAMES: 'harvesterhci.io/dynamic-ssh-key-names',
|
OS_TYPE: 'harvesterhci.io/os-type',
|
||||||
DYNAMIC_SSHKEYS_USERS: 'harvesterhci.io/dynamic-ssh-key-users',
|
STORAGE_PROVISIONER: 'harvesterhci.io/storageProvisioner',
|
||||||
IMAGE_SUFFIX: 'harvesterhci.io/image-type',
|
HOST_REQUEST: 'management.cattle.io/pod-requests',
|
||||||
OS_TYPE: 'harvesterhci.io/os-type',
|
STORAGE_CLASS: 'harvesterhci.io/storageClassName',
|
||||||
STORAGE_PROVISIONER: 'harvesterhci.io/storageProvisioner',
|
STORAGE_NETWORK: 'storage-network.settings.harvesterhci.io',
|
||||||
SKIP_SINGLE_REPLICA_DETACHED_VOL: 'harvesterhci.io/skipSingleReplicaDetachedVol',
|
ADDON_EXPERIMENTAL: 'addon.harvesterhci.io/experimental',
|
||||||
HOST_REQUEST: 'management.cattle.io/pod-requests',
|
VOLUME_ERROR: 'longhorn.io/volume-scheduling-error',
|
||||||
STORAGE_CLASS: 'harvesterhci.io/storageClassName',
|
KVM_AMD_CPU: 'cpu-feature.node.kubevirt.io/svm',
|
||||||
STORAGE_NETWORK: 'storage-network.settings.harvesterhci.io',
|
KVM_INTEL_CPU: 'cpu-feature.node.kubevirt.io/vmx',
|
||||||
ADDON_EXPERIMENTAL: 'addon.harvesterhci.io/experimental',
|
NODE_MANUFACTURER: 'manufacturer',
|
||||||
ADDON_DISPLAYNAME: 'addon.harvesterhci.io/displayName',
|
NODE_MODEL: 'model',
|
||||||
VOLUME_ERROR: 'longhorn.io/volume-scheduling-error',
|
NODE_SERIAL_NUMBER: 'serialNumber',
|
||||||
VOLUME_FOR_VM: 'harvesterhci.io/volumeForVirtualMachine',
|
VM_INSUFFICIENT: 'harvesterhci.io/insufficient-resource-quota',
|
||||||
KVM_AMD_CPU: 'cpu-feature.node.kubevirt.io/svm',
|
NODE_NTP_SYNC_STATUS: 'node.harvesterhci.io/ntp-service',
|
||||||
KVM_INTEL_CPU: 'cpu-feature.node.kubevirt.io/vmx',
|
PARENT_SRIOV: 'harvesterhci.io/parent-sriov-network-device',
|
||||||
NODE_MANUFACTURER: 'manufacturer',
|
PARENT_SRIOV_GPU: 'harvesterhci.io/parentSRIOVGPUDevice',
|
||||||
NODE_MODEL: 'model',
|
VM_MAINTENANCE_MODE_STRATEGY: 'harvesterhci.io/maintain-mode-strategy',
|
||||||
NODE_SERIAL_NUMBER: 'serialNumber',
|
NODE_CPU_MANAGER_UPDATE_STATUS: 'harvesterhci.io/cpu-manager-update-status',
|
||||||
VM_INSUFFICIENT: 'harvesterhci.io/insufficient-resource-quota',
|
CPU_MANAGER: 'cpumanager',
|
||||||
NODE_NTP_SYNC_STATUS: 'node.harvesterhci.io/ntp-service',
|
VM_DEVICE_ALLOCATION_DETAILS: 'harvesterhci.io/deviceAllocationDetails',
|
||||||
PARENT_SRIOV: 'harvesterhci.io/parent-sriov-network-device',
|
SVM_BACKUP_ID: 'harvesterhci.io/svmbackupId',
|
||||||
PARENT_SRIOV_GPU: 'harvesterhci.io/parentSRIOVGPUDevice',
|
DISABLE_LONGHORN_V2_ENGINE: 'node.longhorn.io/disable-v2-data-engine',
|
||||||
VM_MAINTENANCE_MODE_STRATEGY: 'harvesterhci.io/maintain-mode-strategy',
|
|
||||||
NODE_CPU_MANAGER_UPDATE_STATUS: 'harvesterhci.io/cpu-manager-update-status',
|
|
||||||
CPU_MANAGER: 'cpumanager',
|
|
||||||
VM_DEVICE_ALLOCATION_DETAILS: 'harvesterhci.io/deviceAllocationDetails',
|
|
||||||
SVM_BACKUP_ID: 'harvesterhci.io/svmbackupId',
|
|
||||||
DISABLE_LONGHORN_V2_ENGINE: 'node.longhorn.io/disable-v2-data-engine',
|
|
||||||
K8S_ARCH: 'kubernetes.io/arch',
|
|
||||||
IMAGE_DISPLAY_NAME: 'harvesterhci.io/imageDisplayName',
|
|
||||||
CUSTOM_IP: 'harvesterhci.io/custom-ip',
|
|
||||||
IMPORTED_IMAGE: 'migration.harvesterhci.io/imported',
|
|
||||||
VM_CPU_MEMORY_HOTPLUG: 'harvesterhci.io/enableCPUAndMemoryHotplug',
|
|
||||||
FILESYSTEM_OVERHEAD: 'cdi.harvesterhci.io/filesystemOverhead',
|
|
||||||
CLONE_STRATEGY: 'cdi.harvesterhci.io/storageProfileCloneStrategy',
|
|
||||||
VOLUME_MODE_ACCESS_MODES: 'cdi.harvesterhci.io/storageProfileVolumeModeAccessModes',
|
|
||||||
VOLUME_SNAPSHOT_CLASS: 'cdi.harvesterhci.io/storageProfileVolumeSnapshotClass',
|
|
||||||
MAC_ADDRESS: 'harvesterhci.io/mac-address',
|
|
||||||
NODE_UPGRADE_PAUSE_MAP: 'harvesterhci.io/node-upgrade-pause-map',
|
|
||||||
CDI_POPULATOR_KIND: 'cdi.kubevirt.io/storage.populator.kind',
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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',
|
||||||
@@ -16,11 +17,9 @@ export const HCI_SETTING = {
|
|||||||
DEFAULT_STORAGE_CLASS: 'default-storage-class',
|
DEFAULT_STORAGE_CLASS: 'default-storage-class',
|
||||||
SUPPORT_BUNDLE_TIMEOUT: 'support-bundle-timeout',
|
SUPPORT_BUNDLE_TIMEOUT: 'support-bundle-timeout',
|
||||||
SUPPORT_BUNDLE_EXPIRATION: 'support-bundle-expiration',
|
SUPPORT_BUNDLE_EXPIRATION: 'support-bundle-expiration',
|
||||||
SUPPORT_BUNDLE_FILE_NAME: 'support-bundle-file-name',
|
|
||||||
SUPPORT_BUNDLE_IMAGE: 'support-bundle-image',
|
SUPPORT_BUNDLE_IMAGE: 'support-bundle-image',
|
||||||
SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT: 'support-bundle-node-collection-timeout',
|
SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT: 'support-bundle-node-collection-timeout',
|
||||||
STORAGE_NETWORK: 'storage-network',
|
STORAGE_NETWORK: 'storage-network',
|
||||||
RWX_NETWORK: 'rwx-network',
|
|
||||||
VM_FORCE_RESET_POLICY: 'vm-force-reset-policy',
|
VM_FORCE_RESET_POLICY: 'vm-force-reset-policy',
|
||||||
SSL_CERTIFICATES: 'ssl-certificates',
|
SSL_CERTIFICATES: 'ssl-certificates',
|
||||||
SSL_PARAMETERS: 'ssl-parameters',
|
SSL_PARAMETERS: 'ssl-parameters',
|
||||||
@@ -29,19 +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',
|
|
||||||
INSTANCE_MANAGER_RESOURCES: 'instance-manager-resources'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HCI_ALLOWED_SETTINGS = {
|
export const HCI_ALLOWED_SETTINGS = {
|
||||||
@@ -60,31 +52,20 @@ 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_FILE_NAME]: {
|
[HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT]: { featureFlag: 'supportBundleNodeCollectionTimeoutSetting' },
|
||||||
kind: 'string', canReset: true, featureFlag: 'supportBundleFileNameSetting'
|
|
||||||
},
|
|
||||||
[HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT]: { kind: 'number', featureFlag: 'supportBundleNodeCollectionTimeoutSetting' },
|
|
||||||
[HCI_SETTING.SUPPORT_BUNDLE_IMAGE]: { kind: 'json', from: 'import' },
|
[HCI_SETTING.SUPPORT_BUNDLE_IMAGE]: { kind: 'json', from: 'import' },
|
||||||
[HCI_SETTING.STORAGE_NETWORK]: {
|
[HCI_SETTING.STORAGE_NETWORK]: { kind: 'custom', from: 'import' },
|
||||||
kind: 'custom', from: 'import', canReset: true
|
|
||||||
},
|
|
||||||
[HCI_SETTING.RWX_NETWORK]: {
|
|
||||||
kind: 'json', from: 'import', canReset: true, featureFlag: 'rwxNetworkSetting'
|
|
||||||
},
|
|
||||||
[HCI_SETTING.VM_FORCE_RESET_POLICY]: { kind: 'json', from: 'import' },
|
[HCI_SETTING.VM_FORCE_RESET_POLICY]: { kind: 'json', from: 'import' },
|
||||||
[HCI_SETTING.SSL_CERTIFICATES]: { kind: 'json', from: 'import' },
|
[HCI_SETTING.SSL_CERTIFICATES]: { kind: 'json', from: 'import' },
|
||||||
[HCI_SETTING.SSL_PARAMETERS]: {
|
[HCI_SETTING.SSL_PARAMETERS]: {
|
||||||
@@ -93,6 +74,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
|
||||||
},
|
},
|
||||||
@@ -105,38 +87,18 @@ 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',
|
|
||||||
},
|
|
||||||
[HCI_SETTING.INSTANCE_MANAGER_RESOURCES]: {
|
|
||||||
kind: 'json', from: 'import', featureFlag: 'instanceManagerResourcesSetting'
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = {
|
export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = {
|
||||||
[HCI_SETTING.CLUSTER_REGISTRATION_URL]: {
|
[HCI_SETTING.CLUSTER_REGISTRATION_URL]: {
|
||||||
kind: 'custom',
|
kind: 'url',
|
||||||
from: 'import',
|
|
||||||
canReset: true,
|
canReset: true,
|
||||||
},
|
},
|
||||||
[HCI_SETTING.UI_PL]: {
|
[HCI_SETTING.UI_PL]: {
|
||||||
|
|||||||
@@ -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 = {
|
||||||
@@ -89,144 +88,3 @@ export const MACHINE_POOLS = {
|
|||||||
align: 'center',
|
align: 'center',
|
||||||
width: 100,
|
width: 100,
|
||||||
};
|
};
|
||||||
|
|
||||||
// The STORAGE_CLASS column in VM image list page
|
|
||||||
export const IMAGE_STORAGE_CLASS = {
|
|
||||||
name: 'imageStorageClass',
|
|
||||||
labelKey: 'harvester.tableHeaders.storageClass',
|
|
||||||
sort: 'imageStorageClass',
|
|
||||||
value: 'imageStorageClass',
|
|
||||||
align: 'left',
|
|
||||||
width: 150,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const HARVESTER_DESCRIPTION = {
|
|
||||||
...DESCRIPTION,
|
|
||||||
width: 150,
|
|
||||||
};
|
|
||||||
|
|
||||||
// The CIDR_BLOCK column in VPC list page
|
|
||||||
export const CIDR_BLOCK = {
|
|
||||||
name: 'cidrBlock',
|
|
||||||
labelKey: 'harvester.subnet.cidrBlock.label',
|
|
||||||
sort: 'cidrBlock',
|
|
||||||
value: 'spec.cidrBlock',
|
|
||||||
align: 'left',
|
|
||||||
};
|
|
||||||
|
|
||||||
// The Protocol column in VPC list page
|
|
||||||
export const PROTOCOL = {
|
|
||||||
name: 'protocol',
|
|
||||||
labelKey: 'harvester.subnet.protocol.label',
|
|
||||||
sort: 'protocol',
|
|
||||||
value: 'spec.protocol',
|
|
||||||
align: 'left',
|
|
||||||
};
|
|
||||||
|
|
||||||
// The Provider column in VPC list page
|
|
||||||
export const PROVIDER = {
|
|
||||||
name: 'provider',
|
|
||||||
labelKey: 'harvester.subnet.provider.label',
|
|
||||||
sort: 'provider',
|
|
||||||
value: 'spec.provider',
|
|
||||||
align: 'left',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Source VM column in migration.harvesterhci.io.virtualmachineimport list page
|
|
||||||
export const VM_IMPORT_SOURCE_VM = {
|
|
||||||
name: 'sourceVm',
|
|
||||||
labelKey: 'harvester.tableHeaders.vmImportSourceVm',
|
|
||||||
value: 'spec.virtualMachineName',
|
|
||||||
sort: 'spec.virtualMachineName',
|
|
||||||
align: 'left',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Source Cluster column in migration.harvesterhci.io.virtualmachineimport list page
|
|
||||||
export const VM_IMPORT_SOURCE_CLUSTER = {
|
|
||||||
name: 'sourceCluster',
|
|
||||||
labelKey: 'harvester.tableHeaders.vmImportSourceCluster',
|
|
||||||
value: 'spec.sourceCluster.name',
|
|
||||||
sort: 'spec.sourceCluster.name',
|
|
||||||
align: 'left',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Import Status column in migration.harvesterhci.io.virtualmachineimport list page
|
|
||||||
export const VM_IMPORT_STATUS = {
|
|
||||||
name: 'importStatus',
|
|
||||||
labelKey: 'harvester.tableHeaders.vmImportStatus',
|
|
||||||
value: 'status.importStatus',
|
|
||||||
sort: 'status.importStatus',
|
|
||||||
align: 'left',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Datacenter column in migration.harvesterhci.io.vmwaresource list page
|
|
||||||
export const VM_IMPORT_SOURCE_V_DC = {
|
|
||||||
name: 'datacenter',
|
|
||||||
labelKey: 'harvester.tableHeaders.vmImportSourceVDatacenter',
|
|
||||||
value: 'spec.dc',
|
|
||||||
sort: 'spec.dc',
|
|
||||||
align: 'left',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Endpoint column in migration.harvesterhci.io.vmwaresource list page
|
|
||||||
export const VM_IMPORT_SOURCE_V_ENDPOINT = {
|
|
||||||
name: 'endpoint',
|
|
||||||
labelKey: 'harvester.tableHeaders.vmImportSourceVEndpoint',
|
|
||||||
value: 'spec.endpoint',
|
|
||||||
sort: 'spec.endpoint',
|
|
||||||
align: 'left',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Cluster Status column in migration.harvesterhci.io.vmwaresource list page
|
|
||||||
export const VM_IMPORT_SOURCE_V_STATUS = {
|
|
||||||
name: 'clusterStatus',
|
|
||||||
labelKey: 'harvester.tableHeaders.vmImportSourceVClusterStatus',
|
|
||||||
value: 'status.status',
|
|
||||||
sort: 'status.status',
|
|
||||||
align: 'left',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Region column in migration.harvesterhci.io.openstacksource list page
|
|
||||||
export const VM_IMPORT_SOURCE_O_REGION = {
|
|
||||||
name: 'region',
|
|
||||||
labelKey: 'harvester.tableHeaders.vmImportSourceORegion',
|
|
||||||
value: 'spec.region',
|
|
||||||
sort: 'spec.region',
|
|
||||||
align: 'left',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Endpoint column in migration.harvesterhci.io.openstacksource list page
|
|
||||||
export const VM_IMPORT_SOURCE_O_ENDPOINT = {
|
|
||||||
name: 'endpoint',
|
|
||||||
labelKey: 'harvester.tableHeaders.vmImportSourceOEndpoint',
|
|
||||||
value: 'spec.endpoint',
|
|
||||||
sort: 'spec.endpoint',
|
|
||||||
align: 'left',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Cluster Status column in migration.harvesterhci.io.openstacksource list page
|
|
||||||
export const VM_IMPORT_SOURCE_O_STATUS = {
|
|
||||||
name: 'clusterStatus',
|
|
||||||
labelKey: 'harvester.tableHeaders.vmImportSourceOClusterStatus',
|
|
||||||
value: 'status.status',
|
|
||||||
sort: 'status.status',
|
|
||||||
align: 'left',
|
|
||||||
};
|
|
||||||
|
|
||||||
// URL column in migration.harvesterhci.io.ovasource list page
|
|
||||||
export const VM_IMPORT_SOURCE_OVA_URL = {
|
|
||||||
name: 'url',
|
|
||||||
labelKey: 'harvester.tableHeaders.vmImportSourceOVAUrl',
|
|
||||||
value: 'spec.url',
|
|
||||||
sort: 'spec.url',
|
|
||||||
align: 'left',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Status column in migration.harvesterhci.io.ovasource list page
|
|
||||||
export const VM_IMPORT_SOURCE_OVA_STATUS = {
|
|
||||||
name: 'status',
|
|
||||||
labelKey: 'harvester.tableHeaders.vmImportSourceOVAStatus',
|
|
||||||
value: 'status.status',
|
|
||||||
sort: 'status.status',
|
|
||||||
align: 'left',
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -2,47 +2,3 @@ export const BACKUP_TYPE = {
|
|||||||
BACKUP: 'backup',
|
BACKUP: 'backup',
|
||||||
SNAPSHOT: 'snapshot'
|
SNAPSHOT: 'snapshot'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NETWORK_TYPE = {
|
|
||||||
L2VLAN: 'L2VlanNetwork',
|
|
||||||
UNTAGGED: 'UntaggedNetwork',
|
|
||||||
OVERLAY: 'OverlayNetwork',
|
|
||||||
L2TRUNK_VLAN: 'L2VlanTrunkNetwork',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const VOLUME_MODE = {
|
|
||||||
BLOCK: 'Block',
|
|
||||||
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',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const VMIMPORT_SOURCE_PROVIDER = {
|
|
||||||
VMWARE: 'vmware',
|
|
||||||
OPENSTACK: 'openstack',
|
|
||||||
OVA: 'ova',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const VMIMPORT_SOURCE_KINDS = {
|
|
||||||
VMWARE: 'VmwareSource',
|
|
||||||
OPENSTACK: 'OpenstackSource',
|
|
||||||
OVA: 'OvaSource',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CDI_POPULATOR_KIND = {
|
|
||||||
VOLUME_IMPORT_SOURCE: 'VolumeImportSource',
|
|
||||||
VOLUME_CLONE_SOURCE: 'VolumeCloneSource',
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -4,7 +4,6 @@ import { formatSi, exponentNeeded, UNITS } from '@shell/utils/units';
|
|||||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
import { LONGHORN, METRIC } from '@shell/config/types';
|
import { LONGHORN, METRIC } from '@shell/config/types';
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import { UNIT_SUFFIX } from '../../utils/unit';
|
|
||||||
import HarvesterCPUUsed from '../../formatters/HarvesterCPUUsed';
|
import HarvesterCPUUsed from '../../formatters/HarvesterCPUUsed';
|
||||||
import HarvesterMemoryUsed from '../../formatters/HarvesterMemoryUsed';
|
import HarvesterMemoryUsed from '../../formatters/HarvesterMemoryUsed';
|
||||||
import HarvesterStorageUsed from '../../formatters/HarvesterStorageUsed';
|
import HarvesterStorageUsed from '../../formatters/HarvesterStorageUsed';
|
||||||
@@ -122,7 +121,7 @@ export default {
|
|||||||
memoryUnits() {
|
memoryUnits() {
|
||||||
const exponent = exponentNeeded(this.memoryTotal, 1024);
|
const exponent = exponentNeeded(this.memoryTotal, 1024);
|
||||||
|
|
||||||
return `${ UNITS[exponent] }${ UNIT_SUFFIX }`;
|
return `${ UNITS[exponent] }iB`;
|
||||||
},
|
},
|
||||||
|
|
||||||
nodeType() {
|
nodeType() {
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import ArrayListGrouped from '@shell/components/form/ArrayListGrouped';
|
|||||||
import Loading from '@shell/components/Loading.vue';
|
import Loading from '@shell/components/Loading.vue';
|
||||||
import SortableTable from '@shell/components/SortableTable';
|
import SortableTable from '@shell/components/SortableTable';
|
||||||
import Banner from '@components/Banner/Banner.vue';
|
import Banner from '@components/Banner/Banner.vue';
|
||||||
import { UNIT_SUFFIX } from '../../utils/unit';
|
|
||||||
import metricPoller from '@shell/mixins/metric-poller';
|
import metricPoller from '@shell/mixins/metric-poller';
|
||||||
import {
|
import {
|
||||||
METRIC, NODE, LONGHORN, POD, EVENT
|
METRIC, NODE, LONGHORN, POD, EVENT
|
||||||
@@ -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,
|
||||||
@@ -180,7 +178,7 @@ export default {
|
|||||||
minExponent: 3,
|
minExponent: 3,
|
||||||
maxExponent: 3,
|
maxExponent: 3,
|
||||||
maxPrecision: 2,
|
maxPrecision: 2,
|
||||||
suffix: UNIT_SUFFIX,
|
suffix: 'iB',
|
||||||
};
|
};
|
||||||
|
|
||||||
const longhornDisks = Object.keys(diskStatus).map((key) => {
|
const longhornDisks = Object.keys(diskStatus).map((key) => {
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export default {
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col span-6 mb-20">
|
<div class="col span-6 mb-20">
|
||||||
<LabelValue
|
<LabelValue
|
||||||
:name="t('harvester.schedule.cron.label')"
|
:name="t('harvester.schedule.cron')"
|
||||||
:value="cronExpression"
|
:value="cronExpression"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+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>
|
||||||
|
|||||||
@@ -1,175 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { mapState } from 'vuex';
|
|
||||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
|
||||||
import { alternateLabel } from '@shell/utils/platform';
|
|
||||||
import AsyncButton from '@shell/components/AsyncButton';
|
|
||||||
import { Banner } from '@components/Banner';
|
|
||||||
import { Card } from '@components/Card';
|
|
||||||
import { escapeHtml } from '@shell/utils/string';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @name ConfirmExecutionDialog
|
|
||||||
* @description Dialog component to confirm the related resources before executing the action.
|
|
||||||
*/
|
|
||||||
export default {
|
|
||||||
name: 'ConfirmExecutionDialog',
|
|
||||||
|
|
||||||
emits: ['close'],
|
|
||||||
|
|
||||||
components: {
|
|
||||||
AsyncButton,
|
|
||||||
Banner,
|
|
||||||
Card,
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
|
||||||
/**
|
|
||||||
* @property resources to be deleted.
|
|
||||||
* @type {Resource[]} Array of the resource model's instance
|
|
||||||
*/
|
|
||||||
resources: {
|
|
||||||
type: Array,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return { errors: [] };
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
...mapState('action-menu', ['modalData']),
|
|
||||||
|
|
||||||
warningMessageKey() {
|
|
||||||
return this.modalData.warningMessageKey;
|
|
||||||
},
|
|
||||||
|
|
||||||
names() {
|
|
||||||
return this.resources.map((obj) => obj.nameDisplay).slice(0, 5);
|
|
||||||
},
|
|
||||||
|
|
||||||
resourceNames() {
|
|
||||||
return this.names.reduce((res, name, i) => {
|
|
||||||
if (i >= 5) {
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
res += `<b>${ escapeHtml(name) }</b>`;
|
|
||||||
if (i === this.names.length - 1) {
|
|
||||||
res += this.plusMore;
|
|
||||||
} else {
|
|
||||||
res += i === this.resources.length - 2 ? ' and ' : ', ';
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}, '');
|
|
||||||
},
|
|
||||||
|
|
||||||
plusMore() {
|
|
||||||
const remaining = this.resources.length - this.names.length;
|
|
||||||
|
|
||||||
return this.t('dialog.confirmExecution.andOthers', { count: remaining }, true);
|
|
||||||
},
|
|
||||||
|
|
||||||
type() {
|
|
||||||
const types = new Set(this.resources.reduce((array, each) => {
|
|
||||||
array.push(each.type);
|
|
||||||
|
|
||||||
return array;
|
|
||||||
}, []));
|
|
||||||
|
|
||||||
if (types.size > 1) {
|
|
||||||
return this.t('generic.resource', { count: this.resources.length });
|
|
||||||
}
|
|
||||||
|
|
||||||
const schema = this.resources[0]?.schema;
|
|
||||||
|
|
||||||
if ( !schema ) {
|
|
||||||
return `resource${ this.resources.length === 1 ? '' : 's' }`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.$store.getters['type-map/labelFor'](schema, this.resources.length);
|
|
||||||
},
|
|
||||||
|
|
||||||
protip() {
|
|
||||||
return this.t('dialog.confirmExecution.protip', { alternateLabel });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
escapeHtml,
|
|
||||||
|
|
||||||
close() {
|
|
||||||
this.errors = [];
|
|
||||||
this.$emit('close');
|
|
||||||
},
|
|
||||||
|
|
||||||
async apply(buttonDone) {
|
|
||||||
try {
|
|
||||||
for (const resource of this.resources) {
|
|
||||||
await resource.doActionGrowl(this.modalData.action, {});
|
|
||||||
}
|
|
||||||
buttonDone(true);
|
|
||||||
this.close();
|
|
||||||
} catch (e) {
|
|
||||||
this.errors = exceptionToErrorsArray(e);
|
|
||||||
buttonDone(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Card :show-highlight-border="false">
|
|
||||||
<template #title>
|
|
||||||
<h4 class="text-default-text">
|
|
||||||
{{ t('dialog.confirmExecution.title') }}
|
|
||||||
</h4>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #body>
|
|
||||||
<div class="pl-10 pr-10">
|
|
||||||
<span
|
|
||||||
v-clean-html="t(warningMessageKey, { type, names: resourceNames }, true)"
|
|
||||||
></span>
|
|
||||||
<div class="text-info mt-20">
|
|
||||||
{{ protip }}
|
|
||||||
</div>
|
|
||||||
<Banner
|
|
||||||
v-for="(error, i) in errors"
|
|
||||||
:key="i"
|
|
||||||
:label="error"
|
|
||||||
color="error"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #actions>
|
|
||||||
<div class="actions">
|
|
||||||
<button
|
|
||||||
class="btn role-secondary"
|
|
||||||
@click="close"
|
|
||||||
>
|
|
||||||
{{ t('generic.cancel') }}
|
|
||||||
</button>
|
|
||||||
<AsyncButton
|
|
||||||
mode="apply"
|
|
||||||
class="btn bg-primary ml-10"
|
|
||||||
:disabled="applyDisabled"
|
|
||||||
@click="apply"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang='scss' scoped>
|
|
||||||
.modal-container {
|
|
||||||
max-width: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
width: 100%;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -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,7 +4,6 @@ import { Card } from '@components/Card';
|
|||||||
import AsyncButton from '@shell/components/AsyncButton';
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
import { escapeHtml } from '@shell/utils/string';
|
import { escapeHtml } from '@shell/utils/string';
|
||||||
import { HCI } from '../types';
|
import { HCI } from '../types';
|
||||||
import { getHarvesterUserName } from '../utils/auth';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterEnablePciPassthrough',
|
name: 'HarvesterEnablePciPassthrough',
|
||||||
@@ -35,7 +34,16 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async save(buttonCb) {
|
async save(buttonCb) {
|
||||||
const userName = getHarvesterUserName(this.$store.getters);
|
// isSingleProduct == this is a standalone Harvester cluster
|
||||||
|
const isSingleProduct = this.$store.getters['isSingleProduct'];
|
||||||
|
let userName = 'admin';
|
||||||
|
|
||||||
|
// if this is imported Harvester, there may be users other than 'admin
|
||||||
|
if (!isSingleProduct) {
|
||||||
|
const user = this.$store.getters['auth/v3User'];
|
||||||
|
|
||||||
|
userName = user?.username || user?.id;
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < this.resources.length; i++) {
|
for (let i = 0; i < this.resources.length; i++) {
|
||||||
const actionResource = this.resources[i];
|
const actionResource = this.resources[i];
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { Card } from '@components/Card';
|
|||||||
import AsyncButton from '@shell/components/AsyncButton';
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
import { escapeHtml } from '@shell/utils/string';
|
import { escapeHtml } from '@shell/utils/string';
|
||||||
import { HCI } from '../types';
|
import { HCI } from '../types';
|
||||||
import { getHarvesterUserName } from '../utils/auth';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterEnableUSBPassthrough',
|
name: 'HarvesterEnableUSBPassthrough',
|
||||||
@@ -35,7 +34,16 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async save(buttonCb) {
|
async save(buttonCb) {
|
||||||
const userName = getHarvesterUserName(this.$store.getters);
|
// isSingleProduct == this is a standalone Harvester cluster
|
||||||
|
const isSingleProduct = this.$store.getters['isSingleProduct'];
|
||||||
|
let userName = 'admin';
|
||||||
|
|
||||||
|
// if this is imported Harvester, there may be users other than 'admin
|
||||||
|
if (!isSingleProduct) {
|
||||||
|
const user = this.$store.getters['auth/v3User'];
|
||||||
|
|
||||||
|
userName = user?.username || user?.id;
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < this.resources.length; i++) {
|
for (let i = 0; i < this.resources.length; i++) {
|
||||||
const actionResource = this.resources[i];
|
const actionResource = this.resources[i];
|
||||||
|
|||||||
+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>
|
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
|
|
||||||
import { STORAGE_CLASS } from '@shell/config/types';
|
|
||||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
|
||||||
import { sortBy } from '@shell/utils/sort';
|
|
||||||
import { isInternalStorageClass } from '../utils/storage-class';
|
|
||||||
|
|
||||||
import { Card } from '@components/Card';
|
|
||||||
import { Banner } from '@components/Banner';
|
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
||||||
import AsyncButton from '@shell/components/AsyncButton';
|
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'HarvesterDataMigrationDialog',
|
|
||||||
|
|
||||||
emits: ['close'],
|
|
||||||
|
|
||||||
components: {
|
|
||||||
AsyncButton, Banner, Card, LabeledInput, LabeledSelect
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
|
||||||
resources: {
|
|
||||||
type: Array,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
this.storageClasses = await this.$store.dispatch('harvester/findAll', { type: STORAGE_CLASS });
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
targetVolumeName: '',
|
|
||||||
targetStorageClassName: '',
|
|
||||||
errors: [],
|
|
||||||
storageClasses: [],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
...mapGetters({ t: 'i18n/t' }),
|
|
||||||
|
|
||||||
actionResource() {
|
|
||||||
return this.resources[0];
|
|
||||||
},
|
|
||||||
|
|
||||||
storageClassOptions() {
|
|
||||||
return sortBy(
|
|
||||||
this.storageClasses
|
|
||||||
.filter((sc) => !isInternalStorageClass(sc.metadata?.name))
|
|
||||||
.map((sc) => ({
|
|
||||||
label: sc.metadata?.name,
|
|
||||||
value: sc.metadata?.name
|
|
||||||
})),
|
|
||||||
'label'
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
disableSave() {
|
|
||||||
return !this.targetVolumeName || !this.targetStorageClassName;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
close() {
|
|
||||||
this.targetVolumeName = '';
|
|
||||||
this.targetStorageClassName = '';
|
|
||||||
this.errors = [];
|
|
||||||
this.$emit('close');
|
|
||||||
},
|
|
||||||
|
|
||||||
async apply(buttonDone) {
|
|
||||||
if (!this.actionResource) {
|
|
||||||
buttonDone(false);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.targetVolumeName) {
|
|
||||||
const name = this.t('harvester.modal.dataMigration.fields.targetVolumeName.label');
|
|
||||||
|
|
||||||
this['errors'] = [this.t('validation.required', { key: name })];
|
|
||||||
buttonDone(false);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.targetStorageClassName) {
|
|
||||||
const name = this.t('harvester.modal.dataMigration.fields.targetStorageClassName.label');
|
|
||||||
|
|
||||||
this['errors'] = [this.t('validation.required', { key: name })];
|
|
||||||
buttonDone(false);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.actionResource.doAction('dataMigration', {
|
|
||||||
targetVolumeName: this.targetVolumeName,
|
|
||||||
targetStorageClassName: this.targetStorageClassName
|
|
||||||
}, {}, false);
|
|
||||||
|
|
||||||
buttonDone(true);
|
|
||||||
this.close();
|
|
||||||
} catch (err) {
|
|
||||||
const error = err?.data || err;
|
|
||||||
|
|
||||||
this['errors'] = exceptionToErrorsArray(error);
|
|
||||||
buttonDone(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Card :show-highlight-border="false">
|
|
||||||
<template #title>
|
|
||||||
{{ t('harvester.modal.dataMigration.title') }}
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #body>
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="targetVolumeName"
|
|
||||||
:label="t('harvester.modal.dataMigration.fields.targetVolumeName.label')"
|
|
||||||
:placeholder="t('harvester.modal.dataMigration.fields.targetVolumeName.placeholder')"
|
|
||||||
class="mb-20"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="targetStorageClassName"
|
|
||||||
:label="t('harvester.modal.dataMigration.fields.targetStorageClassName.label')"
|
|
||||||
:placeholder="t('harvester.modal.dataMigration.fields.targetStorageClassName.placeholder')"
|
|
||||||
:options="storageClassOptions"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Banner
|
|
||||||
v-for="(err, i) in errors"
|
|
||||||
:key="i"
|
|
||||||
color="error"
|
|
||||||
:label="err"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template
|
|
||||||
#actions
|
|
||||||
class="actions"
|
|
||||||
>
|
|
||||||
<div class="buttons">
|
|
||||||
<button
|
|
||||||
class="btn role-secondary mr-10"
|
|
||||||
@click="close"
|
|
||||||
>
|
|
||||||
{{ t('generic.cancel') }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<AsyncButton
|
|
||||||
mode="apply"
|
|
||||||
:disabled="disableSave"
|
|
||||||
@click="apply"
|
|
||||||
/>
|
|
||||||
</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',
|
||||||
@@ -35,14 +34,9 @@ export default {
|
|||||||
|
|
||||||
await allHash(hash);
|
await allHash(hash);
|
||||||
|
|
||||||
const allStorages = this.allStorageClasses;
|
const defaultStorage = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS).find((s) => s.isDefault);
|
||||||
const defaultStorage = allStorages.find((s) => s.isDefault);
|
|
||||||
|
|
||||||
if (this.isLonghornV1Volume) {
|
this['storageClassName'] = defaultStorage?.metadata?.name || 'longhorn';
|
||||||
this['storageClassName'] = defaultStorage?.metadata?.name || 'longhorn';
|
|
||||||
} else {
|
|
||||||
this['storageClassName'] = this.nonLonghornV1StorageClasses[0]?.metadata?.name || '';
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
@@ -60,7 +54,7 @@ export default {
|
|||||||
...mapGetters({ t: 'i18n/t' }),
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
|
||||||
actionResource() {
|
actionResource() {
|
||||||
return this.resources[0] || {};
|
return this.resources[0];
|
||||||
},
|
},
|
||||||
|
|
||||||
namespaces() {
|
namespaces() {
|
||||||
@@ -79,48 +73,24 @@ export default {
|
|||||||
return out;
|
return out;
|
||||||
},
|
},
|
||||||
|
|
||||||
isLonghornV1Volume() {
|
|
||||||
return this.actionResource?.isLonghornV1 === true;
|
|
||||||
},
|
|
||||||
|
|
||||||
disableSave() {
|
disableSave() {
|
||||||
return !(this.name && this.namespace && this.storageClassName);
|
return !(this.name && this.namespace && this.storageClassName);
|
||||||
},
|
},
|
||||||
|
|
||||||
allStorageClasses() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
return this.$store.getters[`${ inStore }/all`](STORAGE_CLASS) || [];
|
|
||||||
},
|
|
||||||
|
|
||||||
nonLonghornV1StorageClasses() {
|
|
||||||
return this.allStorageClasses.filter((s) => s.isLonghornV1 === false) || [];
|
|
||||||
},
|
|
||||||
|
|
||||||
storageClassOptions() {
|
storageClassOptions() {
|
||||||
let storages = this.allStorageClasses;
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const storages = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS);
|
||||||
|
|
||||||
// Volume with non-longhorn v1 sc can't be exported to longhorn v1 sc image
|
const out = storages.filter((s) => !s.parameters?.backingImage).map((s) => {
|
||||||
if (!this.isLonghornV1Volume) {
|
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||||
storages = this.allStorageClasses.filter((s) => s.isLonghornV1 === false);
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = storages.filter((s) => !s.parameters?.backingImage).map((s) => {
|
|
||||||
let label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
|
||||||
const isInternal = isInternalStorageClass(s.metadata?.name);
|
|
||||||
|
|
||||||
if (isInternal) {
|
|
||||||
label += ` (${ this.t('harvester.storage.internal.label') })`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label,
|
label,
|
||||||
value: s.name,
|
value: s.name,
|
||||||
disabled: isInternal,
|
|
||||||
};
|
};
|
||||||
}) || [];
|
}) || [];
|
||||||
|
|
||||||
return options;
|
return out;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1,165 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
|
||||||
import { HCI } from '../types';
|
|
||||||
import { Card } from '@components/Card';
|
|
||||||
import { Banner } from '@components/Banner';
|
|
||||||
import AsyncButton from '@shell/components/AsyncButton';
|
|
||||||
import AppModal from '@shell/components/AppModal';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'HarvesterImageDownloaderDialog',
|
|
||||||
|
|
||||||
emits: ['close'],
|
|
||||||
|
|
||||||
components: {
|
|
||||||
AsyncButton, Banner, Card, AppModal
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
|
||||||
resources: {
|
|
||||||
type: Array,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return { errors: [], isOpen: false };
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
...mapGetters({ t: 'i18n/t' }),
|
|
||||||
|
|
||||||
downloadImageInProgress() {
|
|
||||||
return this.$store.getters['harvester-common/isDownloadImageInProgress'];
|
|
||||||
},
|
|
||||||
|
|
||||||
image() {
|
|
||||||
return this.resources[0] || {};
|
|
||||||
},
|
|
||||||
|
|
||||||
imageName() {
|
|
||||||
return this.image?.name || '';
|
|
||||||
},
|
|
||||||
|
|
||||||
imageVirtualSize() {
|
|
||||||
return this.image?.virtualSize || this.image?.downSize || '';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
async cancelDownload() {
|
|
||||||
const url = this.image?.links?.downloadcancel;
|
|
||||||
|
|
||||||
if (url) {
|
|
||||||
await this.$store.dispatch('harvester/request', { url });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async close() {
|
|
||||||
if (this.downloadImageInProgress) {
|
|
||||||
this.$store.commit('harvester-common/setDownloadImageCancel', true);
|
|
||||||
this.$store.commit('harvester-common/setDownloadImageInProgress', false);
|
|
||||||
await this.cancelDownload();
|
|
||||||
}
|
|
||||||
this.$emit('close');
|
|
||||||
},
|
|
||||||
|
|
||||||
async startDownload(buttonCb) {
|
|
||||||
// clean the download image CRD first.
|
|
||||||
await this.cancelDownload();
|
|
||||||
this.$store.commit('harvester-common/setDownloadImageCancel', false);
|
|
||||||
this.$store.commit('harvester-common/setDownloadImageInProgress', false);
|
|
||||||
this.errors = [];
|
|
||||||
|
|
||||||
const name = this.image?.name || '';
|
|
||||||
const namespace = this.image?.namespace || '';
|
|
||||||
|
|
||||||
const imageCrd = {
|
|
||||||
apiVersion: 'harvesterhci.io/v1beta1',
|
|
||||||
type: HCI.VM_IMAGE_DOWNLOADER,
|
|
||||||
kind: 'VirtualMachineImageDownloader',
|
|
||||||
metadata: {
|
|
||||||
name,
|
|
||||||
namespace
|
|
||||||
},
|
|
||||||
spec: { imageName: name }
|
|
||||||
};
|
|
||||||
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
const imageCreate = await this.$store.dispatch(`${ inStore }/create`, imageCrd);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await imageCreate.save();
|
|
||||||
this.$store.commit('harvester-common/setDownloadImageId', `${ namespace }/${ name }`, { root: true });
|
|
||||||
this.$store.dispatch('harvester-common/downloadImageProgress', { root: true });
|
|
||||||
} catch (err) {
|
|
||||||
this.errors = exceptionToErrorsArray(err);
|
|
||||||
buttonCb(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<app-modal
|
|
||||||
class="image-downloader-modal"
|
|
||||||
name="image-download-dialog"
|
|
||||||
height="auto"
|
|
||||||
:width="600"
|
|
||||||
:click-to-close="false"
|
|
||||||
@close="close"
|
|
||||||
>
|
|
||||||
<Card :show-highlight-border="false">
|
|
||||||
<template #title>
|
|
||||||
{{ t('harvester.modal.downloadImage.title') }}
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #body>
|
|
||||||
<Banner color="info">
|
|
||||||
{{ t('harvester.modal.downloadImage.banner', { size: imageVirtualSize }) }}
|
|
||||||
</Banner>
|
|
||||||
{{ t('harvester.modal.downloadImage.startMessage') }}
|
|
||||||
<br /><br />
|
|
||||||
<Banner
|
|
||||||
v-for="(err, i) in errors"
|
|
||||||
:key="i"
|
|
||||||
color="error"
|
|
||||||
:label="err"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #actions>
|
|
||||||
<div class="actions">
|
|
||||||
<div class="buttons">
|
|
||||||
<button
|
|
||||||
class="btn role-secondary mr-10"
|
|
||||||
@click="close"
|
|
||||||
>
|
|
||||||
{{ t('generic.cancel') }}
|
|
||||||
</button>
|
|
||||||
<AsyncButton
|
|
||||||
type="submit"
|
|
||||||
mode="download"
|
|
||||||
:disabled="downloadImageInProgress"
|
|
||||||
@click="startDownload"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Card>
|
|
||||||
</app-modal>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.actions {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttons {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
|
||||||
import { mapState, mapGetters } from 'vuex';
|
|
||||||
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 { HCI } from '../types';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'HarvesterInsertCdRomVolume',
|
|
||||||
|
|
||||||
emits: ['close'],
|
|
||||||
|
|
||||||
components: {
|
|
||||||
AsyncButton,
|
|
||||||
Card,
|
|
||||||
LabeledInput,
|
|
||||||
LabeledSelect,
|
|
||||||
Banner
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
|
||||||
resources: {
|
|
||||||
type: Array,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
try {
|
|
||||||
this.images = await this.$store.dispatch('harvester/findAll', { type: HCI.IMAGE });
|
|
||||||
} catch (err) {
|
|
||||||
this.errors = exceptionToErrorsArray(err);
|
|
||||||
this.images = [];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
imageName: '',
|
|
||||||
images: [],
|
|
||||||
errors: [],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
...mapState('action-menu', ['modalData']),
|
|
||||||
...mapGetters({ t: 'i18n/t' }),
|
|
||||||
|
|
||||||
actionResource() {
|
|
||||||
return this.resources?.[0];
|
|
||||||
},
|
|
||||||
|
|
||||||
isFormValid() {
|
|
||||||
return this.imageName !== '';
|
|
||||||
},
|
|
||||||
|
|
||||||
deviceName() {
|
|
||||||
return this.modalData.name;
|
|
||||||
},
|
|
||||||
|
|
||||||
imagesOption() {
|
|
||||||
return this.images
|
|
||||||
.filter((image) => {
|
|
||||||
const labels = image.metadata?.labels || {};
|
|
||||||
const type = labels[HCI_ANNOTATIONS.IMAGE_SUFFIX];
|
|
||||||
|
|
||||||
return type === 'iso';
|
|
||||||
})
|
|
||||||
.map((image) => {
|
|
||||||
return ({
|
|
||||||
label: this.imageOptionLabel(image),
|
|
||||||
value: image.id,
|
|
||||||
disabled: image.isImportedImage
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
close() {
|
|
||||||
this.imageName = '';
|
|
||||||
this.errors = [];
|
|
||||||
this.$emit('close');
|
|
||||||
},
|
|
||||||
|
|
||||||
imageOptionLabel(image) {
|
|
||||||
return `${ image.metadata.namespace }/${ image.spec.displayName }`;
|
|
||||||
},
|
|
||||||
|
|
||||||
async save(buttonCb) {
|
|
||||||
if (!this.actionResource) {
|
|
||||||
buttonCb(false);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
deviceName: this.deviceName,
|
|
||||||
imageName: this.imageName
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await this.actionResource.doAction('insertCdRomVolume', payload);
|
|
||||||
|
|
||||||
if ([200, 204].includes(res?._status)) {
|
|
||||||
this.$store.dispatch('growl/success', {
|
|
||||||
title: this.t('generic.notification.title.succeed'),
|
|
||||||
message: this.t('harvester.modal.insertCdRomVolume.success', {
|
|
||||||
deviceName: this.deviceName,
|
|
||||||
imageName: this.imageName,
|
|
||||||
})
|
|
||||||
}, { 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.insertCdRomVolume.title')"
|
|
||||||
class="text-default-text"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #body>
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="deviceName"
|
|
||||||
:label="t('generic.name')"
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="imageName"
|
|
||||||
class="mt-20"
|
|
||||||
:label="t('harvester.modal.insertCdRomVolume.image')"
|
|
||||||
:options="imagesOption"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<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>
|
|
||||||
@@ -5,7 +5,6 @@ import { Banner } from '@components/Banner';
|
|||||||
import AsyncButton from '@shell/components/AsyncButton';
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
import UnitInput from '@shell/components/form/UnitInput';
|
import UnitInput from '@shell/components/form/UnitInput';
|
||||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
import { GIBIBYTE } from '../utils/unit';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterVMQuotaDialog',
|
name: 'HarvesterVMQuotaDialog',
|
||||||
@@ -30,7 +29,6 @@ export default {
|
|||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
GIBIBYTE,
|
|
||||||
totalSnapshotSize: '',
|
totalSnapshotSize: '',
|
||||||
errors: []
|
errors: []
|
||||||
};
|
};
|
||||||
@@ -46,6 +44,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.totalSnapshotSize = '';
|
this.totalSnapshotSize = '';
|
||||||
this.$emit('close');
|
this.$emit('close');
|
||||||
@@ -93,7 +92,7 @@ export default {
|
|||||||
:input-exponent="3"
|
:input-exponent="3"
|
||||||
:increment="1024"
|
:increment="1024"
|
||||||
:output-modifier="true"
|
:output-modifier="true"
|
||||||
:suffix="GIBIBYTE"
|
suffix="GiB"
|
||||||
class="mb-20"
|
class="mb-20"
|
||||||
/>
|
/>
|
||||||
<Banner
|
<Banner
|
||||||
|
|||||||
@@ -1,239 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
|
|
||||||
import { PVC } from '@shell/config/types';
|
|
||||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
|
||||||
import { sortBy } from '@shell/utils/sort';
|
|
||||||
import { HCI } from '../types';
|
|
||||||
import { parseVolumeClaimTemplates } from '@pkg/harvester/utils/vm';
|
|
||||||
|
|
||||||
import { Card } from '@components/Card';
|
|
||||||
import { Banner } from '@components/Banner';
|
|
||||||
import AsyncButton from '@shell/components/AsyncButton';
|
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'HarvesterStorageMigrationDialog',
|
|
||||||
|
|
||||||
emits: ['close'],
|
|
||||||
|
|
||||||
components: {
|
|
||||||
AsyncButton, Banner, Card, LabeledSelect
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
|
||||||
resources: {
|
|
||||||
type: Array,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
this.allPVCs = await this.$store.dispatch('harvester/findAll', { type: PVC });
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
sourceVolume: '',
|
|
||||||
targetVolume: '',
|
|
||||||
errors: [],
|
|
||||||
allPVCs: [],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
...mapGetters({ t: 'i18n/t' }),
|
|
||||||
|
|
||||||
actionResource() {
|
|
||||||
return this.resources[0];
|
|
||||||
},
|
|
||||||
|
|
||||||
sourceVolumeOptions() {
|
|
||||||
const volumes = this.actionResource.spec?.template?.spec?.volumes || [];
|
|
||||||
|
|
||||||
return sortBy(
|
|
||||||
volumes
|
|
||||||
.map((v) => v.persistentVolumeClaim?.claimName)
|
|
||||||
.filter((name) => !!name)
|
|
||||||
.map((name) => ({
|
|
||||||
label: name,
|
|
||||||
value: name
|
|
||||||
})),
|
|
||||||
'label'
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
namespacePVCs() {
|
|
||||||
return this.allPVCs.filter((pvc) => pvc.metadata.namespace === this.actionResource.metadata.namespace);
|
|
||||||
},
|
|
||||||
|
|
||||||
vmUsedVolumeNames() {
|
|
||||||
const allVMs = this.$store.getters['harvester/all'](HCI.VM) || [];
|
|
||||||
const names = new Set();
|
|
||||||
|
|
||||||
allVMs.forEach((vm) => {
|
|
||||||
// Collect volume names from spec.template.spec.volumes (both PVC and DataVolume references)
|
|
||||||
const volumes = vm.spec?.template?.spec?.volumes || [];
|
|
||||||
|
|
||||||
volumes.forEach((v) => {
|
|
||||||
const name = v.persistentVolumeClaim?.claimName || v.dataVolume?.name;
|
|
||||||
|
|
||||||
if (name) {
|
|
||||||
names.add(`${ vm.metadata.namespace }/${ name }`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Collect volume names from volumeClaimTemplates annotation
|
|
||||||
const templates = parseVolumeClaimTemplates(vm);
|
|
||||||
|
|
||||||
templates.forEach((t) => {
|
|
||||||
if (t.metadata?.name) {
|
|
||||||
names.add(`${ vm.metadata.namespace }/${ t.metadata.name }`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return names;
|
|
||||||
},
|
|
||||||
|
|
||||||
targetVolumeOptions() {
|
|
||||||
return sortBy(
|
|
||||||
this.namespacePVCs
|
|
||||||
.filter((pvc) => {
|
|
||||||
// Exclude volumes used by any VM (via spec.volumes or volumeClaimTemplates)
|
|
||||||
if (this.vmUsedVolumeNames.has(`${ pvc.metadata.namespace }/${ pvc.metadata.name }`)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.map((pvc) => ({
|
|
||||||
label: pvc.metadata.name,
|
|
||||||
value: pvc.metadata.name
|
|
||||||
})),
|
|
||||||
'label'
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
disableSave() {
|
|
||||||
return !this.sourceVolume || !this.targetVolume;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
close() {
|
|
||||||
this.sourceVolume = '';
|
|
||||||
this.targetVolume = '';
|
|
||||||
this.errors = [];
|
|
||||||
this.$emit('close');
|
|
||||||
},
|
|
||||||
|
|
||||||
async apply(buttonDone) {
|
|
||||||
if (!this.actionResource) {
|
|
||||||
buttonDone(false);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.sourceVolume) {
|
|
||||||
const name = this.t('harvester.modal.storageMigration.fields.sourceVolume.label');
|
|
||||||
|
|
||||||
this['errors'] = [this.t('validation.required', { key: name })];
|
|
||||||
buttonDone(false);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.targetVolume) {
|
|
||||||
const name = this.t('harvester.modal.storageMigration.fields.targetVolume.label');
|
|
||||||
|
|
||||||
this['errors'] = [this.t('validation.required', { key: name })];
|
|
||||||
buttonDone(false);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.actionResource.doAction('storageMigration', {
|
|
||||||
sourceVolume: this.sourceVolume,
|
|
||||||
targetVolume: this.targetVolume
|
|
||||||
}, {}, false);
|
|
||||||
|
|
||||||
buttonDone(true);
|
|
||||||
this.close();
|
|
||||||
} catch (err) {
|
|
||||||
const error = err?.data || err;
|
|
||||||
|
|
||||||
this['errors'] = exceptionToErrorsArray(error);
|
|
||||||
buttonDone(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Card :show-highlight-border="false">
|
|
||||||
<template #title>
|
|
||||||
{{ t('harvester.modal.storageMigration.title') }}
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #body>
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="sourceVolume"
|
|
||||||
:label="t('harvester.modal.storageMigration.fields.sourceVolume.label')"
|
|
||||||
:placeholder="t('harvester.modal.storageMigration.fields.sourceVolume.placeholder')"
|
|
||||||
:options="sourceVolumeOptions"
|
|
||||||
class="mb-20"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="targetVolume"
|
|
||||||
:label="t('harvester.modal.storageMigration.fields.targetVolume.label')"
|
|
||||||
:placeholder="t('harvester.modal.storageMigration.fields.targetVolume.placeholder')"
|
|
||||||
:options="targetVolumeOptions"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Banner
|
|
||||||
v-for="(err, i) in errors"
|
|
||||||
:key="i"
|
|
||||||
color="error"
|
|
||||||
:label="err"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template
|
|
||||||
#actions
|
|
||||||
class="actions"
|
|
||||||
>
|
|
||||||
<div class="buttons">
|
|
||||||
<button
|
|
||||||
class="btn role-secondary mr-10"
|
|
||||||
@click="close"
|
|
||||||
>
|
|
||||||
{{ t('generic.cancel') }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<AsyncButton
|
|
||||||
mode="apply"
|
|
||||||
:disabled="disableSave"
|
|
||||||
@click="apply"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.actions {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttons {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -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,111 +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"
|
||||||
:append-to-body="false"
|
:label="stringify(err)"
|
||||||
:options="namespaceOptions"
|
/>
|
||||||
class="mb-10 namespace-select"
|
</div>
|
||||||
:tooltip="t('harvester.modal.bundle.namespaces.tooltip', _, true)"
|
|
||||||
@update:value="updateNamespaces"
|
<div class="footer mt-20">
|
||||||
/>
|
<button
|
||||||
<UnitInput
|
class="btn btn-sm role-secondary mr-10"
|
||||||
v-model:value="timeout"
|
@click="close"
|
||||||
:label="t('harvester.modal.bundle.timeout.label')"
|
|
||||||
class="mb-10"
|
|
||||||
type="number"
|
|
||||||
:min="0"
|
|
||||||
:tooltip="t('harvester.modal.bundle.timeout.tooltip', _, true)"
|
|
||||||
:suffix="timeout > 1 ? 'Minutes' : 'Minute'"
|
|
||||||
@keydown="onKeyDown"
|
|
||||||
@update:value="val => updateNumberValue('timeout', val)"
|
|
||||||
/>
|
|
||||||
<UnitInput
|
|
||||||
v-model:value="expiration"
|
|
||||||
:label="t('harvester.modal.bundle.expiration.label')"
|
|
||||||
class="mb-10"
|
|
||||||
type="number"
|
|
||||||
:min="0"
|
|
||||||
:tooltip="t('harvester.modal.bundle.expiration.tooltip', _, true)"
|
|
||||||
:suffix="expiration > 1 ? 'Minutes' : 'Minute'"
|
|
||||||
@keydown="onKeyDown"
|
|
||||||
@update:value="val => updateNumberValue('expiration', val)"
|
|
||||||
/>
|
|
||||||
<UnitInput
|
|
||||||
v-model:value="nodeTimeout"
|
|
||||||
:label="t('harvester.modal.bundle.nodeTimeout.label')"
|
|
||||||
class="mb-10"
|
|
||||||
type="number"
|
|
||||||
:min="0"
|
|
||||||
:tooltip="t('harvester.modal.bundle.nodeTimeout.tooltip', _, true)"
|
|
||||||
:suffix="nodeTimeout > 1 ? 'Minutes' : 'Minute'"
|
|
||||||
@keydown="onKeyDown"
|
|
||||||
@update:value="val => updateNumberValue('nodeTimeout', val)"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
<div
|
|
||||||
v-for="(err, idx) in errors"
|
|
||||||
:key="idx"
|
|
||||||
>
|
>
|
||||||
<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>
|
||||||
@@ -355,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);
|
||||||
@@ -369,15 +204,12 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
|
height: 218px;
|
||||||
|
|
||||||
.circle {
|
.circle {
|
||||||
padding: 10px 0;
|
padding-top: 20px;
|
||||||
height: 160px;
|
height: 160px;
|
||||||
}
|
}
|
||||||
.namespace-select {
|
|
||||||
:deep(.vs__dropdown-menu) {
|
|
||||||
max-height: 210px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
|
|||||||
+12
-62
@@ -1,16 +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';
|
||||||
|
|
||||||
const VOLUME = 'volume';
|
|
||||||
const NETWORK = 'network';
|
|
||||||
const CDROM = 'cdrom';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterHotUnplug',
|
name: 'HarvesterHotUnplugModal',
|
||||||
|
|
||||||
emits: ['close'],
|
emits: ['close'],
|
||||||
|
|
||||||
@@ -38,43 +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() {
|
|
||||||
const keys = {
|
|
||||||
[VOLUME]: 'harvester.virtualMachine.hotUnplug.detachVolume.title',
|
|
||||||
[CDROM]: 'harvester.virtualMachine.hotUnplug.ejectCdRomVolume.title',
|
|
||||||
[NETWORK]: 'harvester.virtualMachine.hotUnplug.detachNIC.title',
|
|
||||||
};
|
|
||||||
|
|
||||||
return keys[this.modalData.type];
|
|
||||||
},
|
|
||||||
|
|
||||||
actionLabelKey() {
|
|
||||||
const keys = {
|
|
||||||
[VOLUME]: 'harvester.virtualMachine.hotUnplug.detachVolume.actionLabels',
|
|
||||||
[CDROM]: 'harvester.virtualMachine.hotUnplug.ejectCdRomVolume.actionLabels',
|
|
||||||
[NETWORK]: 'harvester.virtualMachine.hotUnplug.detachNIC.actionLabels',
|
|
||||||
};
|
|
||||||
|
|
||||||
return keys[this.modalData.type];
|
|
||||||
},
|
|
||||||
|
|
||||||
successMessageKey() {
|
|
||||||
const keys = {
|
|
||||||
[VOLUME]: 'harvester.virtualMachine.hotUnplug.detachVolume.success',
|
|
||||||
[CDROM]: 'harvester.virtualMachine.hotUnplug.ejectCdRomVolume.success',
|
|
||||||
[NETWORK]: 'harvester.virtualMachine.hotUnplug.detachNIC.success',
|
|
||||||
};
|
|
||||||
|
|
||||||
return keys[this.modalData.type];
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -85,22 +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.modalData.type === VOLUME) {
|
|
||||||
res = await this.actionResource.doAction('removeVolume', { diskName: this.name });
|
|
||||||
} else if (this.modalData.type === NETWORK) {
|
|
||||||
res = await this.actionResource.doAction('removeNic', { interfaceName: this.name });
|
|
||||||
} else {
|
|
||||||
res = await this.actionResource.doAction('ejectCdRomVolume', { deviceName: 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 }
|
||||||
);
|
);
|
||||||
@@ -110,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,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
|
||||||
@@ -157,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>
|
||||||
@@ -178,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"
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user