Compare commits

..
Author SHA1 Message Date
Andy Lee 2a7e51ef4c Merge pull request #96 from harvester/mergify/bp/release-harvester-v1.5/pr-92
chore: bump release version to v1.0.3 (backport #92)
2025-01-17 10:04:08 +08:00
Andy Lee f726fb99e5 Merge pull request #95 from harvester/mergify/bp/release-harvester-v1.5/pr-91
fix: bring default valueContext when customizing vm-import controller (backport #91)
2025-01-17 10:02:07 +08:00
andy.lee 9c19ae1f9b chore: bump release version to v1.0.3
Signed-off-by: andy.lee <andy.lee@suse.com>
(cherry picked from commit 6644651fe5)
2025-01-17 01:58:33 +00:00
andy.lee 6ece2d0284 fix: bring default valueContext fwhen customizing vm-import controller
Signed-off-by: andy.lee <andy.lee@suse.com>
(cherry picked from commit 2d6bc65b26)
2025-01-17 01:57:43 +00:00
Andy Lee aec424b007 Merge pull request #90 from harvester/mergify/bp/release-harvester-v1.5/pr-86
Replace sortablejs-vue3 (backport #86)
2025-01-15 11:51:24 +08:00
Francesco Torchia ff80b91311 Fix Volumes draggable key
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
(cherry picked from commit d9db1263d1)
2025-01-14 16:03:46 +00:00
Francesco Torchia 52252c16f7 Fix draggable Volumes disabled prop
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
(cherry picked from commit 10a8f18784)
2025-01-14 16:03:45 +00:00
Francesco Torchia 19cf1e9302 Replace sortablejs-vue3
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
(cherry picked from commit a7b4366d7c)
2025-01-14 16:03:45 +00:00
Andy Lee ddb8e8106b Merge pull request #87 from harvester/mergify/bp/release-harvester-v1.5/pr-85
fix: filter schedule style and functionality (backport #85)
2025-01-14 13:23:23 +08:00
Andy Lee 9af5ff7b12 Merge pull request #88 from harvester/mergify/bp/release-harvester-v1.5/pr-84
fix: resume schedule job error (backport #84)
2025-01-14 13:12:17 +08:00
andy.lee fff6153f15 fix: resume schedule job error
Signed-off-by: andy.lee <andy.lee@suse.com>
(cherry picked from commit 8116358011)
2025-01-14 04:56:24 +00:00
andy.lee 1ecac7adf4 chore: disable filter button if all rows do not have sourceSchedule
Signed-off-by: andy.lee <andy.lee@suse.com>
(cherry picked from commit d72857f374)
2025-01-14 04:56:23 +00:00
andy.lee 408818904a fix: filter schedule style and functionality
Signed-off-by: andy.lee <andy.lee@suse.com>
(cherry picked from commit 57ab9e2830)
2025-01-14 04:56:23 +00:00
Andy Lee b96637e6d9 Merge pull request #82 from harvester/mergify/bp/release-harvester-v1.5/pr-79
fix: add missing wording in delete VM modal  (backport #79)
2025-01-13 13:39:16 +08:00
andy.lee 8bdaca777f fix: delete VM modal style and wording
Signed-off-by: andy.lee <andy.lee@suse.com>
(cherry picked from commit 7e32ea453b)
2025-01-13 05:37:38 +00:00
Andy Lee d278687e75 Merge pull request #81 from harvester/mergify/bp/release-harvester-v1.5/pr-72
[porting 1262] Fix vm rows reactivity (backport #72)
2025-01-13 12:03:31 +08:00
Francesco Torchia 3ea8edb824 [porting 1262] Fix vm rows reactivity
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
(cherry picked from commit c3f2665912)
2025-01-13 03:59:24 +00:00
Andy Lee e33302050b Merge pull request #78 from harvester/mergify/bp/release-harvester-v1.5/pr-71
chore: add v1.5.0 and v1.3.3 feature flags (backport #71)
2025-01-10 16:35:40 +08:00
andy.lee 8fefb59802 chore: add v1.5.0 and v1.3.3 feature flags
Signed-off-by: andy.lee <andy.lee@suse.com>
(cherry picked from commit 2a58745550)
2025-01-10 06:37:35 +00:00
Andy Lee 872aec1c10 Merge pull request #76 from harvester/mergify/bp/release-harvester-v1.5/pr-68
fix: missing label in VM scheduling tab (backport #68)
2025-01-10 14:35:22 +08:00
Andy Lee 8814bd4cda Merge pull request #77 from harvester/mergify/bp/release-harvester-v1.5/pr-73
fix: exception error in cluster outputs create page (backport #73)
2025-01-10 14:34:57 +08:00
Yi-Ya Chen 72a17cd9c2 fix: replace set
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
(cherry picked from commit 7645d6161f)
2025-01-10 06:31:32 +00:00
andy.lee 8d79b69748 fix: remove translation key
Signed-off-by: andy.lee <andy.lee@suse.com>
(cherry picked from commit 8b43c200b2)
2025-01-10 06:30:46 +00:00
andy.lee 47760a8426 fix: update translation key
Signed-off-by: andy.lee <andy.lee@suse.com>
(cherry picked from commit ab71cb377c)
2025-01-10 06:30:46 +00:00
andy.lee e6fc641a29 fix: missing label in VM scheduling tab
Signed-off-by: andy.lee <andy.lee@suse.com>
(cherry picked from commit eb667fd56b)
2025-01-10 06:30:46 +00:00
Andy Lee 90b85d2be7 Merge pull request #75 from harvester/mergify/bp/release-harvester-v1.5/pr-69
fix: failed to resume /active vm schedule job (backport #69)
2025-01-10 14:26:49 +08:00
Andy Lee ee9575f710 Merge pull request #74 from harvester/mergify/bp/release-harvester-v1.5/pr-70
fix: serial console not work (backport #70)
2025-01-10 14:25:30 +08:00
andy.lee 7d97e6821c fix: resume/active scheduling job failed
Signed-off-by: andy.lee <andy.lee@suse.com>
(cherry picked from commit 814d296d75)
2025-01-10 06:23:31 +00:00
andy.lee 42fc9d8fae fix: serial console not work
Signed-off-by: andy.lee <andy.lee@suse.com>
(cherry picked from commit 69d7046bc7)
2025-01-10 06:21:29 +00:00
andy.lee 0f03fc0805 change mergify.yml base branch to main
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-06 21:39:04 +08:00
andy.lee 6d4a7cd1b1 add mergify.yml
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-06 21:38:55 +08:00
Andy Lee f69d445ef7 Merge pull request #66 from harvester/revert-65-test_word
Revert "setting test word for v1.5 release branch"
2025-01-06 20:59:07 +08:00
Andy Lee d358a0a618 Revert "setting test word for v1.5 release branch" 2025-01-06 20:57:38 +08:00
Andy Lee 47841e49b1 Merge pull request #65 from a110605/test_word
setting test word for v1.5 release branch
2025-01-06 20:51:27 +08:00
andy.lee 57c7f0f856 setting test word
Signed-off-by: andy.lee <andy.lee@suse.com>
2025-01-06 20:46:47 +08:00
208 changed files with 5247 additions and 13493 deletions
-1
View File
@@ -14,4 +14,3 @@ dist-pkg
pkg/harvester/index.ts
pkg/harvester/store/*
scripts/pkgfile.js
tmp
-6
View File
@@ -1,6 +0,0 @@
addAssignees: author
addReviewers: true
numberOfReviewers: 0
reviewers:
- a110605
- houhoucoop
+17 -12
View File
@@ -1,14 +1,19 @@
pull_request_rules:
- name: Ask to resolve conflict
conditions:
- conflict
actions:
comment:
message: This pull request is now in conflict. Could you fix it @{{author}}? 🙏
- name: Automatically open backport PR to release-harvester-v1.5
conditions:
- base=main
actions:
backport:
branches:
- "release-harvester-v1.5"
assignees:
- "{{ author }}"
labels:
- "v1.5 backport PR"
commands_restrictions:
backport:
conditions:
- or:
- sender-permission>=write
- sender=github-actions[bot]
- name: Ask to resolve conflict
conditions:
- conflict
actions:
comment:
message: This pull request is now in conflict. Could you fix it @{{author}}? 🙏
+7 -2
View File
@@ -2,13 +2,18 @@
### Summary
### 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:
### Related 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 -->
### Extra technical notes summary
<!-- Outline technical changes which may pass unobserved or may help to understand the process of solving the issue -->
-40
View File
@@ -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
-46
View File
@@ -1,46 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
":semanticCommitTypeAll(deps)",
":semanticCommitScopeDisabled"
],
"baseBranches": [
"main",
"/^release-harvester-v\\d+\\.\\d+$/"
],
"automergeMajor": false,
"semanticCommits": "enabled",
"semanticCommitType": "deps",
"prHourlyLimit": 12,
"timezone": "Asia/Taipei",
"schedule": ["after 10am on sunday"],
"postUpdateOptions": ["yarnDedupeFewer"],
"packageRules": [
{
"matchUpdateTypes": ["major"],
"enabled": false
},
{
"matchPackagePatterns": ["vue", "vue-router", "vuex"],
"matchUpdateTypes": ["major"],
"enabled": false
},
{
"matchPackageNames": ["@rancher/shell"],
"enabled": false
},
{
"matchUpdateTypes": ["minor"],
"groupName": "minor dependencies",
"labels": ["minor-update"],
"reviewers": ["a110605", "houhoucoop"]
},
{
"matchUpdateTypes": ["patch", "digest"],
"automerge": true,
"groupName": "patch digest dependencies",
"labels": ["patch-update", "automerge"]
}
]
}
@@ -1,17 +0,0 @@
name: "[PR Management] Auto Assign Reviewer & Assignee"
on:
pull_request_target:
types: [opened, ready_for_review]
permissions:
pull-requests: write
jobs:
auto-assign:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- uses: rancher/gh-issue-mgr/auto-assign-action@main
with:
configuration-path: .github/auto-assign-config.yaml
-94
View File
@@ -1,94 +0,0 @@
name: "[PR Management] Add Labels"
on:
pull_request_target:
types: [opened, reopened]
branches:
- main
- 'release-harvester-v*'
permissions:
pull-requests: write
jobs:
add-require-backport-label:
if: github.event.pull_request.draft == false &&
github.event.pull_request.base.ref == 'main'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.base_ref }}
- name: Fetch release branches and PR labels
id: fetch_info
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
repo="${{ github.repository }}"
pr_number=${{ github.event.pull_request.number }}
release_branches=$(gh api "repos/${repo}/branches" --paginate --jq '.[].name' | grep -E '^release-harvester-v[0-9]+\.[0-9]+$' || true)
if [[ -z "$release_branches" ]]; then
echo "should_label=false" >> "$GITHUB_OUTPUT"
exit 0
fi
latest_branch=$(echo "$release_branches" | sort -Vr | head -n1)
version="${latest_branch#release-harvester-v}"
release_tag="v${version}.0"
tags=$(gh api "repos/${repo}/releases" --paginate --jq '.[].tag_name')
if echo "$tags" | grep -Fxq "$release_tag"; then
echo "should_label=false" >> "$GITHUB_OUTPUT"
exit 0
fi
label="require backport/v${version}"
echo "should_label=true" >> "$GITHUB_OUTPUT"
echo "backport_label=$label" >> "$GITHUB_OUTPUT"
pr_labels=$(gh pr view "$pr_number" --repo "$repo" --json labels --jq '.labels[].name' || echo "")
pr_labels_csv=$(echo "$pr_labels" | tr '\n' ',' | sed 's/,$//')
echo "pr_labels=$pr_labels_csv" >> "$GITHUB_OUTPUT"
- name: Add label if needed
if: steps.fetch_info.outputs.should_label == 'true' && !contains(steps.fetch_info.outputs.pr_labels, steps.fetch_info.outputs.backport_label)
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "Adding label: ${{ steps.fetch_info.outputs.backport_label }}"
gh pr edit ${{ github.event.pull_request.number }} \
--repo ${{ github.repository }} \
--add-label "${{ steps.fetch_info.outputs.backport_label }}"
add-backport-label:
if: github.event.pull_request.draft == false &&
startsWith(github.event.pull_request.base.ref, 'release-harvester-v')
runs-on: ubuntu-latest
steps:
- name: Check conditions for backport label
id: check
run: |
IS_MERGIFY=$(echo '${{ github.event.pull_request.user.login }}' | grep -iq 'mergify' && echo true || echo false)
TARGET_BRANCH=${{ github.event.pull_request.base.ref }}
echo "IS_MERGIFY=$IS_MERGIFY" >> $GITHUB_OUTPUT
echo "TARGET_BRANCH=$TARGET_BRANCH" >> $GITHUB_OUTPUT
- name: Add label if needed
if: steps.check.outputs.IS_MERGIFY == 'true' && startsWith(steps.check.outputs.TARGET_BRANCH, 'release-harvester-v')
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TARGET_BRANCH="${{ steps.check.outputs.TARGET_BRANCH }}"
version="${TARGET_BRANCH#release-harvester-v}"
label="backport/v${version}"
echo "Adding label $label"
gh pr edit ${{ github.event.pull_request.number }} \
--repo ${{ github.repository }} \
--add-label "$label"
-49
View File
@@ -1,49 +0,0 @@
name: "[PR Management] Request Backport via Mergify"
on:
pull_request_target:
types: [closed]
branches: [main]
permissions:
pull-requests: write
jobs:
comment-backport:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.base_ref }}
- name: Post Mergify backport command
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
pr_number=${{ github.event.pull_request.number }}
repo="${{ github.repository }}"
labels_json='${{ toJson(github.event.pull_request.labels.*.name) }}'
labels=$(echo "$labels_json" | jq -r '.[] // empty')
echo "Labels on PR: $labels"
matches=$(echo "$labels" | grep -oE '^require backport/v[0-9]+\.[0-9]+$' || true)
if [[ -z "$matches" ]]; then
echo "No back‑port labels found — skipping."
exit 0
fi
branches=$(echo "$matches" \
| sed -E 's/^require backport\/v/release-harvester-v/' \
| sort -u | tr '\n' ' ')
branches=${branches%% }
cmd="@Mergifyio backport $branches"
echo "Posting Mergify command: $cmd"
gh pr comment "$pr_number" --repo "$repo" --body "$cmd"
@@ -1,36 +0,0 @@
name: Build Harvester Catalog Image and Publish on Release
on:
workflow_dispatch:
release:
types: [published]
defaults:
run:
shell: bash
working-directory: ./
jobs:
check-version:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Check package version
env:
TAG_VERSION: ${{ github.event.release.tag_name }}
run: ./scripts/check-version.sh $TAG_VERSION
build-and-push-extension-catalog:
needs: check-version
uses: ./.github/workflows/build-extension-catalog.yml
permissions:
actions: write
contents: read
packages: write
id-token: write
with:
registry_target: docker.io
registry_user: rancher
secrets: inherit
@@ -1,4 +1,4 @@
name: Build Standalone on PR Merge
name: Build Dashboard (Branch)
on:
push:
@@ -11,16 +11,16 @@ on:
- main
- 'release-harvester-v*'
- '*-dev'
types:
types:
- merged
jobs:
build-validation:
name: Build Test
uses: ./.github/workflows/run-lint.yaml
uses: ./.github/workflows/test.yaml
build:
name: Build and Upload Package
uses: ./.github/workflows/build-and-publish-standalone.yaml
uses: ./.github/workflows/build-and-upload.yaml
needs:
- build-validation
permissions:
@@ -0,0 +1,21 @@
name: Build Dashboard (Release)
on:
push:
tags:
- v[1-9].*
jobs:
build-validation:
name: Build Test
uses: ./.github/workflows/test.yaml
build:
name: Build and Upload Package
uses: ./.github/workflows/build-and-upload.yaml
needs:
- build-validation
permissions:
contents: read
packages: write
id-token: write
with:
CI_BUILD_TAG: ${{github.ref_name}}
@@ -1,4 +1,4 @@
name: Build Harvester Dashboard and Publish
name: Build Harvester Dashboard and Upload
on:
workflow_call:
@@ -1,70 +0,0 @@
name: Build and release Extension Catalog Image to registry
on:
workflow_call:
inputs:
registry_target:
required: true
type: string
registry_user:
required: true
type: string
outputs:
build-extension-catalog-job-status:
value: ${{ jobs.build-extension-catalog.outputs.build-status }}
jobs:
build-and-push-extension-catalog:
name: Build container image
if: github.ref_type == 'tag' || (github.ref == 'refs/heads/main' && github.event_name != 'pull_request')
runs-on: ubuntu-latest
permissions:
actions: write
contents: read
packages: write
id-token: write
outputs:
build-status: ${{ job.status }}
steps:
- name: Read Secrets
uses: rancher-eio/read-vault-secrets@main
with:
secrets: |
secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials username | DOCKER_USERNAME ;
secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials password | DOCKER_PASSWORD ;
- name: Checkout repository (normal flow)
uses: actions/checkout@v4
- name: Enable Corepack
run: corepack enable
- name: Configure Git
run: |
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ env.DOCKER_USERNAME }}
password: ${{ env.DOCKER_PASSWORD }}
- name: Setup Helm
uses: azure/setup-helm@v3
with:
version: v3.8.0
- name: Setup Nodejs with yarn caching
uses: actions/setup-node@v4
with:
node-version: '20'
cache: yarn
- name: Install dependencies
run: yarn
- name: Build and push UI image
run: |
publish="yarn publish-pkgs -cp -r ${{ inputs.registry_target }} -o ${{ inputs.registry_user }}"
$publish
@@ -0,0 +1,34 @@
name: Build and Release Extension Charts
on:
workflow_dispatch:
release:
types: [released]
defaults:
run:
shell: bash
working-directory: ./
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run tests
uses: ./.github/actions/lint
build-extension-charts:
needs:
- lint
uses: rancher/dashboard/.github/workflows/build-extension-charts.yml@master
permissions:
actions: write
contents: write
deployments: write
pages: write
with:
target_branch: gh-pages
tagged_release: ${{ github.ref_name }}
@@ -1,140 +0,0 @@
name: Build and Release Extension Charts on PR Merge
on:
push:
branches:
- 'release-harvester-v*'
- 'main'
jobs:
setup-target-branch:
runs-on: ubuntu-latest
outputs:
target_branch: ${{ steps.get-version.outputs.target_branch }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Determine target branch
id: get-version
run: |
if [[ "${{ github.ref_name }}" == "main" ]]; then
TARGET_BRANCH="main-head"
elif [[ "${{ github.ref_name }}" =~ ^release-harvester-v([0-9]+\.[0-9]+)$ ]]; then
TARGET_BRANCH="v${BASH_REMATCH[1]}-head"
else
echo "Error: invalid branch format." && exit 1
fi
echo "target_branch=${TARGET_BRANCH}" >> $GITHUB_ENV
echo "target_branch=${TARGET_BRANCH}" >> $GITHUB_OUTPUT
- name: Ensure target branch exists
run: |
git fetch --all
if ! git ls-remote --exit-code --heads origin "${{ env.target_branch }}"; then
git checkout gh-pages
git checkout -b "${{ env.target_branch }}"
git push origin "${{ env.target_branch }}"
fi
extract-version:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.get_version.outputs.version }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Extract version from package.json
id: get_version
run: |
VERSION=$(jq -r '.version' pkg/harvester/package.json)
echo "VERSION=${VERSION}" >> $GITHUB_ENV
echo "version=${VERSION}" >> $GITHUB_OUTPUT
build-extension-charts:
needs:
- setup-target-branch
- extract-version
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup environment
run: |
corepack enable
yarn install --frozen-lockfile
- name: Setup Helm
uses: azure/setup-helm@v3
with:
version: v3.8.0
- name: Build Helm charts
run: |
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
uses: actions/upload-artifact@v4
with:
name: charts
path: tmp
release:
needs:
- setup-target-branch
- extract-version
- build-extension-charts
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout release branch
uses: actions/checkout@v4
with:
ref: '${{ github.ref_name }}'
- name: Get last commit hash
id: last_commit
run: |
LAST_COMMIT=$(git rev-parse HEAD)
echo "LAST_COMMIT=${LAST_COMMIT}" >> $GITHUB_ENV
- name: Checkout target branch
uses: actions/checkout@v4
with:
ref: '${{ needs.setup-target-branch.outputs.target_branch }}'
- name: Remove old artifacts
run: |
rm -rf extensions/harvester/${{ needs.extract-version.outputs.version }}
rm -rf charts/harvester/${{ needs.extract-version.outputs.version }}
rm -f assets/harvester/harvester-${{ needs.extract-version.outputs.version }}.tgz
- name: Configure Git
run: |
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: charts
- name: Commit and push artifacts
run: |
git add ./{assets,charts,extensions,index.yaml}
git commit -m "CI Build Artifacts (commit: ${{ env.LAST_COMMIT }}, version: ${{ needs.extract-version.outputs.version }})"
git push origin ${{ needs.setup-target-branch.outputs.target_branch }}
- name: Run Helm chart releaser
uses: helm/chart-releaser-action@v1.7.0
with:
charts_dir: ./charts
env:
CR_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
CR_SKIP_EXISTING: true
@@ -1,54 +0,0 @@
name: Build and Release Extension Charts on Release
on:
workflow_dispatch:
release:
types: [released]
defaults:
run:
shell: bash
working-directory: ./
jobs:
setup-release-tag:
runs-on: ubuntu-latest
outputs:
release_tag: ${{ steps.determine_tag.outputs.release_tag }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Determine release tag
id: determine_tag
run: |
if [[ "${{ github.event.release.tag_name }}" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
RELEASE_TAG="harvester-${BASH_REMATCH[1]}"
echo "${RELEASE_TAG}"
echo "release_tag=${RELEASE_TAG}" >> $GITHUB_OUTPUT
else
echo "Error: invalid tag format." && exit 1
fi
check-version:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Check package version
env:
TAG_VERSION: ${{ github.event.release.tag_name }}
run: ./scripts/check-version.sh $TAG_VERSION
build-extension-charts:
needs:
- setup-release-tag
- check-version
uses: rancher/dashboard/.github/workflows/build-extension-charts.yml@master
permissions:
actions: write
contents: write
deployments: write
pages: write
with:
target_branch: gh-pages
tagged_release: '${{ needs.setup-release-tag.outputs.release_tag }}'
@@ -1,27 +0,0 @@
name: Build Standalone on Release
on:
push:
tags:
- v[1-9].*
jobs:
check-version:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Check package version
env:
TAG_VERSION: ${{github.ref_name}}
run: ./scripts/check-version.sh $TAG_VERSION
build:
name: Build and Upload Package
uses: ./.github/workflows/build-and-publish-standalone.yaml
needs: check-version
permissions:
contents: read
packages: write
id-token: write
with:
CI_BUILD_TAG: ${{github.ref_name}}
-30
View File
@@ -1,30 +0,0 @@
name: "[PR Management] Add PR Label"
on:
pull_request_target:
types: [opened, reopened, edited]
branches:
- main
- 'release-harvester-v*'
permissions:
pull-requests: write
jobs:
auto-assign-pr-label:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.base_ref }}
- name: Setup Nodejs and yarn install
uses: ./.github/actions/setup
- name: Set PR label
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR_LABEL=$(node ./scripts/extract-release-label.mjs "${{ github.event.pull_request.title }}")
echo "PR_LABEL = $PR_LABEL"
gh pr edit ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --add-label "$PR_LABEL"
-73
View File
@@ -1,73 +0,0 @@
name: Tests
on:
workflow_call: # This tells GH that the workflow is reusable
push:
branches:
- main
- 'release-*'
pull_request:
types: [opened, reopened, edited, synchronize]
branches:
- main
- 'release-*'
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Need full history for commit-lint
- name: Run tests
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 }}
+23
View File
@@ -0,0 +1,23 @@
name: Tests
on:
workflow_call: # This tells GH that the workflow is reusable
push:
branches:
- main
- 'release-*'
pull_request:
branches:
- main
- 'release-*'
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run tests
uses: ./.github/actions/lint
-1
View File
@@ -1 +0,0 @@
npx --no -- commitlint --edit $1
-1
View File
@@ -1 +0,0 @@
yarn lint
+40 -133
View File
@@ -1,169 +1,76 @@
# 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
For detailed installation instructions, please refer to the [official Harvester documentation](https://docs.harvesterhci.io/v1.5/rancher/harvester-ui-extension#installation-on-rancher-210).
To install the Harvester UI Extension in Rancher 2.10.0 and newer:
- 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
Run the extension standalone with hot reload at `https://localhost:8005`.
For development, serve with hot reload at https://localhost:8005
```bash
# Install dependencies
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
# Example with specific server version
RANCHER_ENV=harvester VUE_APP_SERVER_VERSION=v1.5.0 API=https://192.168.1.123 yarn dev
```
# Use VUE_APP_SERVER_VERSION to specify the Harvester server version
# 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
RANCHER_ENV=harvester
VUE_APP_SERVER_VERSION=v1.5.0
API=https://192.168.1.123
# Example
RANCHER_ENV=harvester VUE_APP_SERVER_VERSION=v1.4.0 API=https://192.168.1.123 yarn dev
```
### 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
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
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
Copyright (c) 2014-2025 [SUSE, LLC.](https://www.suse.com/)
License
=======
Copyright (c) 2014-2023 [Rancher Labs, Inc.](http://rancher.com)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
-47
View File
@@ -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'),
],
};
+13 -20
View File
@@ -1,13 +1,12 @@
{
"name": "harvester-ui-extension",
"version": "1.7.0",
"version": "0.1.0",
"private": false,
"engines": {
"node": ">=20.0.0"
},
"dependencies": {
"@babel/plugin-transform-class-static-block": "7.28.3",
"@rancher/shell": "3.0.8-rc.8",
"@rancher/shell": "3.0.1-rc.4",
"cache-loader": "^4.1.0",
"color": "4.2.3",
"ip": "2.0.1",
@@ -16,21 +15,21 @@
"yaml": "^2.5.1"
},
"resolutions": {
"@types/node": "~20.19.0",
"cronstrue": "2.59.0",
"@types/node": "~20.10.0",
"cronstrue": "2.50.0",
"d3-color": "3.1.0",
"ejs": "3.1.10",
"follow-redirects": "1.15.11",
"ejs": "3.1.9",
"follow-redirects": "1.15.2",
"glob": "7.2.3",
"glob-parent": "6.0.2",
"json5": "2.2.3",
"@types/lodash": "4.17.21",
"@types/lodash": "4.17.5",
"merge": "2.1.1",
"node-forge": "1.3.3",
"node-forge": "1.3.1",
"nth-check": "2.1.1",
"qs": "6.14.0",
"roarr": "7.21.2",
"semver": "7.7.3",
"qs": "6.11.1",
"roarr": "7.0.4",
"semver": "7.5.4",
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
},
"scripts": {
@@ -41,16 +40,10 @@
"build-pkg": "./node_modules/@rancher/shell/scripts/build-pkg.sh",
"serve-pkgs": "./node_modules/@rancher/shell/scripts/serve-pkgs",
"publish-pkgs": "./node_modules/@rancher/shell/scripts/extension/publish",
"parse-tag-name": "./node_modules/@rancher/shell/scripts/extension/parse-tag-name",
"commitlint": "commitlint --edit",
"prepare": "husky"
"parse-tag-name": "./node_modules/@rancher/shell/scripts/extension/parse-tag-name"
},
"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-promise": "^7.1.0",
"husky": "^9.1.7"
"eslint-plugin-promise": "^7.1.0"
}
}
+1 -9
View File
@@ -1,9 +1 @@
const baseConfig = require('@rancher/shell/pkg/babel.config');
module.exports = {
...baseConfig,
plugins: [
...(baseConfig.plugins || []),
'@babel/plugin-transform-class-static-block'
]
};
module.exports = require('@rancher/shell/pkg/babel.config');
+8 -35
View File
@@ -5,10 +5,8 @@ import { Checkbox } from '@components/Form/Checkbox';
import ModalWithCard from '@shell/components/ModalWithCard';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { Banner } from '@components/Banner';
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import { HCI } from '../types';
import UpgradeInfo from './UpgradeInfo';
export default {
name: 'HarvesterUpgrade',
@@ -30,15 +28,14 @@ export default {
data() {
return {
upgrade: [],
upgradeMessage: [],
errors: '',
selectMode: true,
version: '',
enableLogging: true,
skipSingleReplicaDetachedVol: false,
readyReleaseNote: false,
isOpen: false
upgrade: [],
upgradeMessage: [],
errors: '',
selectMode: true,
version: '',
enableLogging: true,
readyReleaseNote: false,
isOpen: false
};
},
@@ -71,10 +68,6 @@ export default {
return this.$store.getters['harvester/schemaFor'](HCI.UPGRADE_LOG);
},
skipSingleReplicaDetachedVolFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('skipSingleReplicaDetachedVol');
},
releaseLink() {
return `https://github.com/harvester/harvester/releases/tag/${ this.version }`;
}
@@ -111,11 +104,6 @@ export default {
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) {
upgradeValue.spec.logEnabled = this.enableLogging;
}
@@ -202,21 +190,6 @@ export default {
/>
</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">
<p
v-clean-html="t('harvester.upgradePage.releaseTip', {url: releaseLink}, true)"
@@ -190,7 +190,6 @@ export default {
v-clean-tooltip="{
placement: 'bottom-left',
}"
popper-class="upgrade-header-dropdown"
class="hand"
>
<slot name="button-content">
@@ -273,7 +272,7 @@ export default {
<p
v-if="overallMessage"
class="text-error mb-20"
class="text-warning mb-20"
>
{{ overallMessage }}
</p>
@@ -301,14 +300,14 @@ export default {
<ProgressBarList
:title="t('harvester.upgradePage.upgradeNode')"
:percent="nodesPercent"
:precent="nodesPercent"
:list="nodesStatus"
/>
<p class="bordered-section"></p>
<ProgressBarList
:title="t('harvester.upgradePage.upgradeSysService')"
:percent="sysServiceTotal"
:precent="sysServiceTotal"
:list="sysServiceUpgradeMessage"
/>
</div>
@@ -339,12 +338,6 @@ export default {
</div>
</template>
<style lang="scss">
.upgrade-header-dropdown .v-popper__arrow-container {
display: none;
}
</style>
<style lang="scss" scoped>
a {
float: right;
@@ -12,7 +12,7 @@ export default {
default: ''
},
percent: {
precent: {
type: Number,
default: 0
},
@@ -39,18 +39,16 @@ export default {
<template>
<div class="bar-list">
<h4>{{ title }} <span class="float-r text-info">{{ percent }}%</span></h4>
<h4>{{ title }} <span class="float-r text-info">{{ precent }}%</span></h4>
<div>
<div>
<Collapse v-model:open="open">
<template #title>
<div class="total-bar">
<span class="bar">
<PercentageBar
:model-value="percent"
preferred-direction="MORE"
/>
</span>
<span class="bar"><PercentageBar
:model-value="precent"
preferred-direction="MORE"
/></span>
<span
class="on-off"
@click="handleSwitch"
@@ -58,26 +56,28 @@ export default {
</div>
</template>
<div class="custom-content">
<div
v-for="(item, i) in list"
:key="i"
>
<p>
{{ item.name }} <span
class="status"
:class="{ [item.state]: true }"
>{{ item.state }}</span>
</p>
<PercentageBar
:model-value="item.percent"
preferred-direction="MORE"
/>
<p class="warning">
{{ item.message }}
</p>
<template>
<div class="custom-content">
<div
v-for="(item, i) in list"
:key="i"
>
<p>
{{ item.name }} <span
class="status"
:class="{ [item.state]: true }"
>{{ item.state }}</span>
</p>
<PercentageBar
:model-value="item.percent"
preferred-direction="MORE"
/>
<p class="warning">
{{ item.message }}
</p>
</div>
</div>
</div>
</template>
</Collapse>
</div>
</div>
@@ -93,7 +93,7 @@ export default {
.total-bar {
display: flex;
user-select: none;
> .bar {
.bar {
width: 85%;
}
.on-off {
@@ -102,22 +102,25 @@ export default {
}
}
.custom-content {
margin-bottom: 14px;
p {
margin-bottom: 4px;
}
.status {
float: right;
}
.Succeeded, .Upgrading, .Pending {
color: var(--success);
}
.failed {
color: var(--error)
}
.warning {
color: var(--error);
.item {
margin-bottom: 14px;
p {
margin-bottom: 4px;
}
.status {
float: right;
}
.Succeeded, .Upgrading, .Pending {
color: var(--success);
}
.failed {
color: var(--error)
}
.warning {
color: var(--error);
}
}
}
}
</style>
+28 -128
View File
@@ -1,16 +1,14 @@
<script>
import { mapGetters } from 'vuex';
import ActionMenu from '@shell/components/ActionMenuShell';
import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton';
import { HCI_ALLOWED_SETTINGS, HCI_SINGLE_CLUSTER_ALLOWED_SETTING, HCI_SETTING } from '../config/settings';
import { DOC } from '../config/doc-links';
import { docLink } from '../utils/feature-flags';
import { HCI_ALLOWED_SETTINGS, HCI_SETTING } from '../config/settings';
const CATEGORY = {
ui: [
'branding',
'ui-source',
'ui-plugin-index',
'ui-index',
]
};
@@ -21,7 +19,6 @@ export default {
components: {
AsyncButton,
Banner,
ActionMenu
},
props: {
@@ -33,23 +30,15 @@ export default {
category: {
type: String,
required: true,
},
searchQuery: {
type: String,
default: ''
}
},
data() {
const categorySettings = this.filterCategorySettings();
const filteredSettings = this.filterSearchSettings(categorySettings, this.searchQuery);
return {
HCI_SETTING,
categorySettings,
filteredSettings,
originalHideMap: this.createHideMap(categorySettings)
};
},
@@ -59,85 +48,12 @@ export default {
settings: {
deep: true,
handler() {
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 }));
this['categorySettings'] = this.filterCategorySettings();
}
}
},
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() {
return this.settings.filter((s) => {
if (!this.getFeatureEnabled(s.featureFlag)) {
@@ -158,21 +74,25 @@ export default {
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) {
return HCI_ALLOWED_SETTINGS.find((setting) => setting.id === id);
},
openJsonSettings(settings) {
return settings.map((s) => s.hide ? { ...s, hide: false } : s);
},
toggleHide(s) {
const setting = this.filteredSettings.find((setting) => setting.id === s.id);
if (setting) {
setting.hide = !setting.hide;
this.originalHideMap[setting.id] = setting.hide;
}
this.categorySettings.find((setting) => {
if (setting.id === s.id) {
setting.hide = !setting.hide;
}
});
},
async testConnect(buttonDone, value) {
@@ -198,19 +118,6 @@ export default {
}
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>
<div>
<div
v-for="(setting, i) in filteredSettings"
v-for="(setting, i) in categorySettings"
:key="i"
class="advanced-setting mb-20"
>
@@ -241,7 +148,7 @@ export default {
Experimental
</span>
</h1>
<h2 v-clean-html="t(setting.description, getDocLinkParams(setting) || {}, true)">
<h2 v-clean-html="t(setting.description, {}, true)">
</h2>
</div>
<div
@@ -249,12 +156,15 @@ export default {
:id="setting.id"
class="action"
>
<ActionMenu
:resource="setting.data"
:button-aria-label="t('advancedSettings.edit.label')"
data-testid="action-button"
button-role="tertiary"
/>
<button
aria-haspopup="true"
aria-expanded="false"
type="button"
class="btn btn-sm role-multi-action actions"
@click="showActionMenu($event, setting)"
>
<i class="icon icon-actions" />
</button>
</div>
</div>
<div value>
@@ -311,12 +221,6 @@ export default {
{{ setting.data.errMessage }}
</Banner>
</div>
<div
v-if="filteredSettings.length === 0"
class="advanced-setting mb-20 no-search-match"
>
<p> {{ t('harvester.setting.noSearchMatch') }} </p>
</div>
</div>
</template>
@@ -367,8 +271,4 @@ export default {
padding: 2px 10px;
font-size: 12px;
}
.no-search-match {
text-align: center;
}
</style>
+1 -1
View File
@@ -43,7 +43,7 @@ export default {
{{ t('harvester.upgradePage.upgradeInfo.tip') }}
</p>
<p>
<p class="mb-5">
{{ t('harvester.upgradePage.upgradeInfo.moreNotes') }} <a
:href="releaseVersion"
target="_blank"
@@ -257,12 +257,8 @@ export default {
});
},
reconnect() {
this.$refs.novncConsole.reconnect();
},
softReboot() {
this.vmResource.doSoftReboot();
this.vmResource.softrebootVM();
},
showKeysModal() {
@@ -310,13 +306,6 @@ export default {
{{ t("harvester.action.softreboot") }}
</button>
<button
class="btn btn-sm bg-primary"
@click="reconnect"
>
{{ t("harvester.action.reconnect") }}
</button>
<v-dropdown
v-if="!hideCustomKeysBar"
ref="customKeyPopover"
@@ -383,15 +372,3 @@ export default {
background: rgb(40, 40, 40);
}
</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>
@@ -6,7 +6,6 @@ import CreateEditView from '@shell/mixins/create-edit-view';
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { HCI_SETTING } from '../../config/settings';
import UnitInput from '@shell/components/form/UnitInput';
const DEFAULT_TYPE = 's3';
@@ -14,7 +13,7 @@ export default {
name: 'HarvesterEditBackupTarget',
components: {
LabeledInput, LabeledSelect, Tip, Password, MessageLink, UnitInput
LabeledInput, LabeledSelect, Tip, Password, MessageLink
},
mixins: [CreateEditView],
@@ -25,9 +24,7 @@ export default {
try {
parseDefaultValue = JSON.parse(this.value.value);
} catch (error) {
parseDefaultValue = {
type: '', endpoint: '', refreshIntervalInSeconds: 0
};
parseDefaultValue = { type: '', endpoint: '' };
}
// set default type to s3
@@ -65,10 +62,6 @@ export default {
}];
},
refreshIntervalInSecondEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('refreshIntervalInSecond');
},
isS3() {
return this.parseDefaultValue.type === DEFAULT_TYPE;
},
@@ -90,9 +83,7 @@ export default {
try {
parseDefaultValue = JSON.parse(neu.value);
} catch (err) {
parseDefaultValue = {
type: '', endpoint: '', refreshIntervalInSeconds: 0
};
parseDefaultValue = { type: '', endpoint: '' };
}
this['parseDefaultValue'] = parseDefaultValue;
@@ -120,6 +111,7 @@ export default {
if (this.isS3 && !this.parseDefaultValue.virtualHostedStyle) {
this.parseDefaultValue.virtualHostedStyle = false;
}
if (!this.parseDefaultValue.type) {
delete this.value['value'];
} else {
@@ -128,9 +120,7 @@ export default {
},
useDefault() {
this['parseDefaultValue'] = {
type: '', endpoint: '', refreshIntervalInSeconds: 0
};
this['parseDefaultValue'] = { type: '', endpoint: '' };
}
}
};
@@ -149,23 +139,6 @@ export default {
:options="typeOption"
@update:value="update"
/>
<UnitInput
v-if="refreshIntervalInSecondEnabled"
v-model:value="parseDefaultValue.refreshIntervalInSeconds"
:suffix="parseDefaultValue.refreshIntervalInSeconds <= 1 ? 'Second' : 'Seconds'"
:label="t('harvester.backup.refreshInterval.label')"
:mode="mode"
:positive="true"
class="mb-5"
required
@update:value="update"
/>
<Tip
v-if="refreshIntervalInSecondEnabled"
class="mb-20"
icon="icon icon-info"
:text="t('harvester.backup.refreshInterval.tip')"
/>
<LabeledInput
v-model:value="parseDefaultValue.endpoint"
class="mb-5"
@@ -75,11 +75,7 @@ export default {
const csiDrivers = this.$store.getters[`${ inStore }/all`](CSI_DRIVER) || [];
return this.configArr.length >= csiDrivers.length;
},
allowEmptySnapshotClassNameFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('allowEmptySnapshotClassName');
},
}
},
methods: {
@@ -143,7 +139,7 @@ export default {
errors.push(this.t('validation.required', { key: this.t('harvester.setting.csiDriverConfig.volumeSnapshotClassName') }, true));
}
if (!this.allowEmptySnapshotClassNameFeatureEnabled && !config.value.backupVolumeSnapshotClassName) {
if (!config.value.backupVolumeSnapshotClassName) {
errors.push(this.t('validation.required', { key: this.t('harvester.setting.csiDriverConfig.backupVolumeSnapshotClassName') }, true));
}
});
@@ -162,18 +158,10 @@ export default {
this.configArr.splice(idx, 1);
},
isBackupVolumeSnapshotRequired(driver) {
return driver === LONGHORN_DRIVER;
},
disableEdit(driver) {
return driver === LONGHORN_DRIVER;
},
isBackupVolumeSnapshotClassNameDisabled(driver) {
return driver === LONGHORN_DRIVER || this.allowEmptySnapshotClassNameFeatureEnabled;
},
add() {
this.configArr.push({
key: '',
@@ -196,7 +184,6 @@ export default {
<InfoBox
v-for="(driver, idx) in configArr"
:key="idx"
class="box"
>
<button
:disabled="disableEdit(driver.key)"
@@ -239,9 +226,9 @@ export default {
<LabeledSelect
v-model:value="driver.value.backupVolumeSnapshotClassName"
:mode="mode"
:disabled="isBackupVolumeSnapshotClassNameDisabled(driver.key)"
required
:disabled="disableEdit(driver.key)"
:options="getVolumeSnapshotOptions(driver.key)"
:required="isBackupVolumeSnapshotRequired(driver.key)"
:label="t('harvester.setting.csiDriverConfig.backupVolumeSnapshotClassName')"
@keydown.native.enter.prevent="()=>{}"
@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,5 +1,6 @@
<script>
import CreateEditView from '@shell/mixins/create-edit-view';
import { Banner } from '@components/Banner';
import { LabeledInput } from '@components/Form/LabeledInput';
@@ -61,7 +62,7 @@ export default {
<div
class="row"
@input="update"
@update:value="update"
>
<div class="col span-12">
<LabeledInput
@@ -1,154 +0,0 @@
<script>
import { LabeledInput } from '@components/Form/LabeledInput';
import { RadioGroup } from '@components/Form/Radio';
import UnitInput from '@shell/components/form/UnitInput';
import { MEBIBYTE } from '../../utils/unit';
import { Banner } from '@components/Banner';
export default {
name: 'KubevirtMigration',
components: {
LabeledInput,
UnitInput,
RadioGroup,
Banner
},
props: {
registerBeforeHook: {
type: Function,
required: true
},
value: {
type: Object,
default: () => ({
value: '',
default: '{}'
})
},
},
data() {
const migration = this.parseJSON(this.value?.value) || this.parseJSON(this.value?.default) || {};
return {
MEBIBYTE,
migration,
parseError: null,
};
},
created() {
this.registerBeforeHook?.(this.willSave, 'willSave');
},
methods: {
parseJSON(string) {
try {
return JSON.parse(string);
} catch (e) {
this.parseError = this.t('kubevirtMigration.parseError', { error: e.message });
// eslint-disable-next-line no-console
console.error('Failed to parse JSON:', e.message);
return null;
}
},
updateValue() {
if (this.value) {
this.value.value = JSON.stringify(this.migration);
}
},
useDefault() {
if (this.value?.default) {
const defaultMigration = this.parseJSON(this.value.default) || {};
this.migration = defaultMigration;
this.updateValue();
}
},
async willSave() {
this.updateValue();
return Promise.resolve();
},
},
};
</script>
<template>
<div>
<Banner
v-if="parseError"
color="error"
>
{{ parseError }}
</Banner>
<div class="migration-field">
<LabeledInput
v-model:value.number="migration.parallelMigrationsPerCluster"
:label="t('harvester.setting.kubevirtMigration.parallelMigrationsPerCluster')"
type="number"
min="1"
/>
<LabeledInput
v-model:value.number="migration.parallelOutboundMigrationsPerNode"
:label="t('harvester.setting.kubevirtMigration.parallelOutboundMigrationsPerNode')"
type="number"
min="1"
/>
<UnitInput
v-model:value="migration.bandwidthPerMigration"
min="0"
:label="t('harvester.setting.kubevirtMigration.bandwidthPerMigration')"
:mode="mode"
:suffix="MEBIBYTE"
:tooltip="t('harvester.setting.kubevirtMigration.bandwidthPerMigrationTooltip', _, true)"
/>
<UnitInput
v-model:value="migration.completionTimeoutPerGiB"
:label="t('harvester.setting.kubevirtMigration.completionTimeoutPerGiB')"
:mode="mode"
:suffix="migration.completionTimeoutPerGiB === 1 ? 'Second' : 'Seconds'"
min="10"
/>
<UnitInput
v-model:value="migration.progressTimeout"
:label="t('harvester.setting.kubevirtMigration.progressTimeout')"
:mode="mode"
:suffix="migration.progressTimeout === 1 ? 'Second' : 'Seconds'"
min="10"
/>
<div
v-for="field in ['allowAutoConverge','allowPostCopy','unsafeMigrationOverride','allowWorkloadDisruption','disableTLS','matchSELinuxLevelOnMigration']"
:key="field"
>
<label
class="mb-5"
:for="field"
>{{ t(`harvester.setting.kubevirtMigration.${field}`) }}</label>
<RadioGroup
:id="field"
v-model:value="migration[field]"
:options="[
{ label: t('advancedSettings.edit.trueOption'), value: true },
{ label: t('advancedSettings.edit.falseOption'), value: false },
]"
/>
</div>
</div>
</div>
</template>
<style scoped>
.migration-field {
display: flex;
flex-direction: column;
gap: 12px;
}
</style>
@@ -1,222 +0,0 @@
<script>
import CreateEditView from '@shell/mixins/create-edit-view';
import { RadioGroup } from '@components/Form/Radio';
import { SECRET } from '@shell/config/types';
import { exceptionToErrorsArray } from '@shell/utils/error';
import FileSelector, { createOnSelected } from '@shell/components/form/FileSelector';
import YamlEditor from '@shell/components/YamlEditor';
export default {
name: 'HarvesterRancherCluster',
components: {
RadioGroup,
FileSelector,
YamlEditor
},
mixins: [CreateEditView],
props: {
value: {
type: Object,
default: () => ({}),
},
registerBeforeHook: {
type: Function,
required: false,
default: () => {},
},
},
data() {
let parseDefaultValue = {};
try {
const data = this.value.value || this.value.default || '{}';
const parsed = JSON.parse(data);
parseDefaultValue = {
kubeConfig: '',
removeUpstreamClusterWhenNamespaceIsDeleted: parsed.removeUpstreamClusterWhenNamespaceIsDeleted || false
};
} catch (error) {
parseDefaultValue = {
kubeConfig: '',
removeUpstreamClusterWhenNamespaceIsDeleted: false
};
}
return {
parseDefaultValue,
errors: [],
existingSecret: null,
};
},
async created() {
await this.checkExistingSecret();
if (this.registerBeforeHook) {
this.registerBeforeHook(this.willSave, 'willSave');
}
},
methods: {
onKeySelected: createOnSelected('parseDefaultValue.kubeConfig'),
update() {
if (this.parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted) {
this.value['value'] = JSON.stringify({ removeUpstreamClusterWhenNamespaceIsDeleted: true });
} else {
this.value['value'] = this.value.default || '{}';
}
},
async checkExistingSecret() {
const inStore = this.$store.getters['currentProduct'].inStore;
await this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET });
const secrets = this.$store.getters[`${ inStore }/all`](SECRET) || [];
this.existingSecret = secrets.find((secret) => secret.metadata.name === 'rancher-cluster-config' &&
secret.metadata.namespace === 'harvester-system'
);
// If the secret exists and has data, populate the kubeConfig
if (this.existingSecret?.data?.kubeConfig) {
const decodedContent = atob(this.existingSecret.data.kubeConfig);
this.parseDefaultValue.kubeConfig = decodedContent;
this.$nextTick(() => {
this.update();
});
}
},
async createOrUpdateRancherKubeConfigSecret() {
this.errors = [];
// Check if kubeConfig is provided
if (!this.parseDefaultValue.kubeConfig) {
this.errors.push(this.t('validation.required', { key: this.t('harvester.setting.rancherCluster.kubeConfig') }, true));
return Promise.reject(this.errors);
}
try {
let secret;
if (this.existingSecret) {
secret = this.existingSecret;
secret.setData('kubeConfig', this.parseDefaultValue.kubeConfig);
} else {
const inStore = this.$store.getters['currentProduct'].inStore;
secret = await this.$store.dispatch(`${ inStore }/create`, {
apiVersion: 'v1',
kind: 'Secret',
metadata: {
name: 'rancher-cluster-config',
namespace: 'harvester-system'
},
type: 'secret',
data: { kubeConfig: btoa(this.parseDefaultValue.kubeConfig) }
});
}
await secret.save();
return Promise.resolve();
} catch (err) {
this.errors = exceptionToErrorsArray(err);
return Promise.reject(this.errors);
}
},
async deleteRancherKubeConfigSecret() {
if (this.existingSecret) {
this.existingSecret.remove();
}
},
async willSave() {
// Only create or update secret if enabled
if (this.parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted) {
await this.createOrUpdateRancherKubeConfigSecret();
} else {
await this.deleteRancherKubeConfigSecret();
}
return Promise.resolve();
},
useDefault() {
this.parseDefaultValue = {
kubeConfig: '',
removeUpstreamClusterWhenNamespaceIsDeleted: false
};
}
},
watch: {
'parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted'(val, oldVal) {
if (val && !oldVal && this.existingSecret?.data?.kubeConfig) {
// Populate kubeConfig with the existing secret value
this.parseDefaultValue.kubeConfig = atob(this.existingSecret.data.kubeConfig);
}
},
'parseDefaultValue.kubeConfig'(val) {
this.$refs.yaml?.updateValue(val);
}
}
};
</script>
<template>
<div>
<div class="row mt-20">
<div class="col span-12">
<RadioGroup
v-model:value="parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted"
:label="t('harvester.setting.rancherCluster.removeUpstreamClusterWhenNamespaceIsDeleted')"
name="removeUpstreamClusterWhenNamespaceIsDeleted"
:options="[true, false]"
:labels="[t('generic.enabled'), t('generic.disabled')]"
@update:value="update"
/>
</div>
</div>
<div
v-if="parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted"
class="row mt-20"
>
<div class="col span-12">
<FileSelector
class="btn btn-sm bg-primary mb-10"
:label="t('generic.readFromFile')"
@selected="onKeySelected"
/>
<YamlEditor
ref="yaml"
v-model:value="parseDefaultValue.kubeConfig"
class="yaml-editor"
:editor-mode="mode === 'view' ? 'VIEW_CODE' : 'EDIT_CODE'"
@update:value="update"
/>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
$yaml-height: 540px;
:deep() .yaml-editor{
flex: 1;
min-height: $yaml-height;
& .code-mirror .CodeMirror {
position: initial;
height: auto;
min-height: $yaml-height;
}
}
</style>
@@ -12,9 +12,6 @@ import { NODE } from '@shell/config/types';
import { HCI } from '../../types';
import { DOC } from '../../config/doc-links';
import { docLink } from '../../utils/feature-flags';
import { NETWORK_TYPE } from '../../config/types';
const { L2VLAN, UNTAGGED } = NETWORK_TYPE;
export default {
name: 'HarvesterEditStorageNetwork',
@@ -60,14 +57,11 @@ export default {
data() {
let parsedDefaultValue = {};
let openVlan = false;
let networkType = L2VLAN;
try {
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;
} catch (error) {
networkType = L2VLAN;
parsedDefaultValue = {
vlan: '',
clusterNetwork: '',
@@ -79,7 +73,6 @@ export default {
return {
openVlan,
networkType,
errors: [],
exclude,
parsedDefaultValue,
@@ -94,35 +87,16 @@ export default {
},
computed: {
showVlan() {
return this.networkType === L2VLAN;
},
networkTypes() {
const types = [L2VLAN];
if (this.untaggedNetworkSettingEnabled) {
types.push(UNTAGGED);
}
return types;
},
storageNetworkExampleLink() {
const version = this.$store.getters['harvester-common/getServerVersion']();
return docLink(DOC.STORAGE_NETWORK_EXAMPLE, version);
},
untaggedNetworkSettingEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('untaggedNetworkSetting');
},
clusterNetworkOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
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;
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: {
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() {
const exclude = this.exclude.filter((ip) => ip);
@@ -207,7 +138,7 @@ export default {
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));
}
@@ -260,25 +191,13 @@ export default {
/>
<div v-if="openVlan">
<LabeledSelect
v-model:value="networkType"
class="mb-20"
:options="networkTypes"
:mode="mode"
:label="t('harvester.fields.type')"
required
/>
<LabeledInput
v-if="showVlan"
v-model:value.number="parsedDefaultValue.vlan"
type="number"
class="mb-20"
:mode="mode"
required
placeholder="e.g. 1 - 4094"
label-key="harvester.setting.storageNetwork.vlan"
@update:value="inputVlan"
/>
<LabeledSelect
@@ -3,9 +3,6 @@ import { NAMESPACE } from '@shell/config/types';
import CreateEditView from '@shell/mixins/create-edit-view';
import LabeledSelect from '@shell/components/form/LabeledSelect';
const SELECT_ALL = 'select_all';
const UNSELECT_ALL = 'unselect_all';
export default {
name: 'HarvesterBundleNamespaces',
@@ -14,92 +11,47 @@ export default {
mixins: [CreateEditView],
async fetch() {
this.loading = true;
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() {
let namespaces = [];
const namespacesStr = this.value?.value || this.value?.default || '';
const namespaces = namespacesStr ? namespacesStr.split(',') : [];
return {
namespaces,
defaultNamespaces: [],
loading: true
};
if (namespacesStr) {
namespaces = namespacesStr.split(',');
}
return { namespaces };
},
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() {
const mappedNamespaces = this.filteredNamespaces.map((ns) => ({ label: ns, value: ns }));
if (!this.customSupportBundleFeatureEnabled) {
return mappedNamespaces;
}
const allSelected =
this.namespaces.length === this.filteredNamespaces.length &&
this.filteredNamespaces.every((ns) => this.namespaces.includes(ns));
const controlOption = allSelected ? { label: this.t('harvester.modal.bundle.namespaces.unselectAll'), value: UNSELECT_ALL } : { label: this.t('harvester.modal.bundle.namespaces.selectAll'), value: SELECT_ALL };
return [controlOption, ...mappedNamespaces];
return this.$store.getters['harvester/all'](NAMESPACE).map((N) => {
return {
label: N.id,
value: N.id
};
});
}
},
methods: {
update(selected) {
if (selected.includes(SELECT_ALL)) {
this.namespaces = [...this.filteredNamespaces];
} else if (selected.includes(UNSELECT_ALL)) {
this.namespaces = [];
} else {
this.namespaces = selected.filter((val) => val !== SELECT_ALL && val !== UNSELECT_ALL);
}
update() {
const namespaceStr = this.namespaces.join(',');
this.value.value = this.namespaces.join(',');
this.value['value'] = namespaceStr;
}
},
watch: {
'value.value'(newVal) {
const raw = newVal || this.value.default || '';
this.namespaces = raw ? raw.split(',') : [];
'value.value': {
handler(neu) {
if (neu === this.value.default || !neu) {
this.namespaces = [];
}
},
deep: true
}
}
};
@@ -110,7 +62,6 @@ export default {
<div class="col span-12">
<LabeledSelect
v-model:value="namespaces"
:loading="loading"
:multiple="true"
label-key="nameNsDescription.namespace.label"
:mode="mode"
@@ -1,163 +0,0 @@
<script>
import CreateEditView from '@shell/mixins/create-edit-view';
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { RadioGroup } from '@components/Form/Radio';
import { mapGetters } from 'vuex';
export default {
name: 'HarvesterUpgradeConfig',
components: {
LabeledInput,
LabeledSelect,
RadioGroup
},
mixins: [CreateEditView],
data() {
let parseDefaultValue = {};
try {
parseDefaultValue = this.value.value ? JSON.parse(this.value.value) : JSON.parse(this.value.default);
} catch (error) {
parseDefaultValue = JSON.parse(this.value.default);
}
parseDefaultValue = this.normalizeValue(parseDefaultValue);
return {
parseDefaultValue,
errors: []
};
},
computed: {
...mapGetters({ t: 'i18n/t' }),
strategyOptions() {
return [
{ value: 'sequential', label: 'sequential' },
{ value: 'skip', label: 'skip' },
{ value: 'parallel', label: 'parallel' }
];
}
},
created() {
this.update();
},
methods: {
normalizeValue(obj) {
if (!obj.imagePreloadOption) {
obj.imagePreloadOption = { strategy: { type: 'sequential' } };
}
if (!obj.imagePreloadOption.strategy) {
obj.imagePreloadOption.strategy = { type: 'sequential' };
}
if (!obj.imagePreloadOption.strategy.type) {
obj.imagePreloadOption.strategy.type = 'sequential';
}
// Only set concurrency if type is 'parallel'
if (obj.imagePreloadOption.strategy.type === 'parallel') {
if (typeof obj.imagePreloadOption.strategy.concurrency !== 'number') {
obj.imagePreloadOption.strategy.concurrency = 0;
}
} else {
delete obj.imagePreloadOption.strategy.concurrency;
}
if (typeof obj.restoreVM !== 'boolean') {
obj.restoreVM = false;
}
return obj;
},
update() {
try {
// Clone to avoid mutating the form state
const valueToSave = JSON.parse(JSON.stringify(this.parseDefaultValue));
if (valueToSave.imagePreloadOption && valueToSave.imagePreloadOption.strategy) {
if (valueToSave.imagePreloadOption.strategy.type !== 'parallel') {
delete valueToSave.imagePreloadOption.strategy.concurrency;
}
}
this.value['value'] = JSON.stringify(valueToSave, null, 2);
this.errors = [];
} catch (e) {
this.errors = ['Invalid JSON'];
}
}
},
watch: {
value: {
handler(neu) {
let parseDefaultValue;
try {
parseDefaultValue = JSON.parse(neu.value);
} catch (err) {
parseDefaultValue = JSON.parse(this.value.default);
}
parseDefaultValue = this.normalizeValue(parseDefaultValue);
this['parseDefaultValue'] = parseDefaultValue;
this.update();
},
deep: true
}
}
};
</script>
<template>
<div>
<div
class="row"
@input="update"
>
<div class="col span-12">
<label class="mb-5"><b>{{ t('harvester.setting.upgrade.imagePreloadStrategy') }}</b></label>
<LabeledSelect
v-model:value="parseDefaultValue.imagePreloadOption.strategy.type"
class="mb-20"
:mode="mode"
:label="t('harvester.setting.upgrade.strategyType')"
:options="strategyOptions"
@update:value="update"
/>
<LabeledInput
v-if="parseDefaultValue.imagePreloadOption.strategy.type === 'parallel'"
v-model:value.number="parseDefaultValue.imagePreloadOption.strategy.concurrency"
class="mb-20"
:mode="mode"
:label="t('harvester.setting.upgrade.concurrency')"
min="0"
type="number"
/>
<label class="mb-5"><b>{{ t('harvester.setting.upgrade.restoreVM') }}</b></label>
<RadioGroup
v-model:value="parseDefaultValue.restoreVM"
class="mb-20"
name="restoreVM"
:options="[true, false]"
:labels="[t('generic.enabled'), t('generic.disabled')]"
@update:value="update"
/>
<div
v-if="errors.length"
class="error"
>
{{ errors[0] }}
</div>
</div>
</div>
</div>
</template>
<style scoped>
.error {
color: #d9534f;
margin-top: 5px;
}
</style>
@@ -59,7 +59,7 @@ export default {
name="model"
:options="[true,false]"
:labels="[t('generic.enabled'), t('generic.disabled')]"
@update:value="update"
@input="update"
/>
<LabeledInput
@@ -1,270 +0,0 @@
<script>
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
import { RadioGroup } from '@components/Form/Radio';
import { Banner } from '@components/Banner';
import ArrayList from '@shell/components/form/ArrayList';
import { allHash } from '@shell/utils/promise';
import { isValidCIDR } from '@shell/utils/validators/cidr';
import { NODE } from '@shell/config/types';
import { _EDIT } from '@shell/config/query-params';
import { HCI } from '../../types';
const DEFAULT_NETWORK = {
clusterNetwork: '',
vlan: '',
range: '',
exclude: [],
};
export default {
name: 'VMMigrationNetwork',
components: {
LabeledInput,
LabeledSelect,
RadioGroup,
Banner,
ArrayList,
},
props: {
registerBeforeHook: {
type: Function,
required: true,
},
mode: {
type: String,
default: _EDIT,
},
value: {
type: Object,
default: () => ({ value: '' }),
},
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
try {
await allHash({
clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }),
vlanStatus: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VLAN_STATUS }),
nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }),
});
this.fetchError = null;
} catch (e) {
console.error('Failed to fetch network data:', e); // eslint-disable-line no-console
this.fetchError = this.t('harvester.setting.vmMigrationNetwork.fetchError', { error: e.message || e }, true);
}
},
data() {
const { parsed, enabled, parseError } = this.parseInitialValue();
return {
enabled,
network: { ...DEFAULT_NETWORK, ...parsed },
fetchError: null,
parseError,
};
},
created() {
this.registerBeforeHook?.(this.willSave, 'willSave');
},
computed: {
allErrors() {
return [this.fetchError, this.parseError].filter(Boolean);
},
clusterNetworkOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const networks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
return networks.map((net) => ({
label: net.isReadyForStorageNetwork ? net.id : `${ net.id } (${ this.t('generic.notReady') })`,
value: net.id,
disabled: !net.isReadyForStorageNetwork,
}));
},
disableEdit() {
return !!(this.fetchError || this.parseError);
},
},
methods: {
parseInitialValue() {
let parsed = {};
let enabled = false;
let parseError = null;
try {
if (typeof this.value.value === 'string' && this.value.value.trim()) {
parsed = JSON.parse(this.value.value);
enabled = true;
}
} catch (e) {
console.error('[VMMigrationNetwork] Failed to parse value:', e); // eslint-disable-line no-console
parseError = this.t('harvester.setting.vmMigrationNetwork.parseError', { error: e.message }, true);
}
if (!Array.isArray(parsed.exclude)) {
parsed.exclude = [];
}
return {
parsed, enabled, parseError
};
},
clearErrors() {
this.fetchError = null;
this.parseError = null;
},
inputVlan(val) {
this.network.vlan = val ? Math.min(4094, Math.max(1, Number(val))) : '';
this.update();
},
useDefault() {
this.network = { ...DEFAULT_NETWORK };
this.value.value = '';
this.enabled = false;
this.clearErrors();
},
update() {
try {
this.value.value = this.enabled ? JSON.stringify({
...this.network,
exclude: (this.network.exclude || []).filter((e) => !!e?.trim()),
}) : '';
} catch (e) {
console.error('Failed to stringify network config:', e); // eslint-disable-line no-console
this.value.value = '';
}
},
validateInputs() {
const errors = [];
if (!this.network.clusterNetwork) {
errors.push(this.t('validation.required', { key: this.t('harvester.setting.vmMigrationNetwork.clusterNetwork') }, true));
}
if (!this.network.range) {
errors.push(this.t('validation.required', { key: this.t('harvester.setting.vmMigrationNetwork.range.label') }, true));
} else if (!isValidCIDR(this.network.range)) {
errors.push(this.t('harvester.setting.vmMigrationNetwork.range.invalid', null, true));
}
if (this.network.vlan === '') {
errors.push(this.t('validation.required', { key: this.t('harvester.setting.vmMigrationNetwork.vlan') }, true));
} else {
const vlan = Number(this.network.vlan);
if (isNaN(vlan) || vlan < 1 || vlan > 4094) {
errors.push(this.t('validation.between', {
key: this.t('harvester.setting.vmMigrationNetwork.vlan'),
min: 1,
max: 4094,
}, true));
}
}
for (const cidr of this.network.exclude || []) {
if (cidr && !isValidCIDR(cidr)) {
errors.push(this.t('harvester.setting.storageNetwork.exclude.invalid', { value: cidr }, true));
}
}
return errors;
},
async willSave() {
if (!this.enabled) {
this.useDefault();
return Promise.resolve();
}
this.update();
const errors = this.validateInputs();
return errors.length ? Promise.reject(errors) : Promise.resolve();
},
},
};
</script>
<template>
<div>
<Banner
v-for="(errorMsg, index) in allErrors"
:key="index"
color="error"
>
{{ errorMsg }}
</Banner>
<RadioGroup
v-model:value="enabled"
class="mb-20"
name="enableMigrationNetwork"
:options="[true, false]"
:labels="[t('generic.enabled'), t('generic.disabled')]"
@update:value="update"
/>
<template v-if="enabled">
<LabeledSelect
v-model:value="network.clusterNetwork"
required
label-key="harvester.setting.vmMigrationNetwork.clusterNetwork"
class="mb-20"
:mode="mode"
:options="clusterNetworkOptions"
:disabled="disableEdit"
@update:value="update"
/>
<LabeledInput
v-model:value.number="network.vlan"
required
type="number"
class="mb-20"
:min="1"
:max="4094"
:mode="mode"
placeholder="e.g. 1 - 4094"
label-key="harvester.setting.vmMigrationNetwork.vlan"
:disabled="disableEdit"
@update:value="inputVlan"
/>
<LabeledInput
v-model:value="network.range"
required
class="mb-5"
:mode="mode"
:placeholder="t('harvester.setting.vmMigrationNetwork.range.placeholder')"
label-key="harvester.setting.vmMigrationNetwork.range.label"
:disabled="disableEdit"
@update:value="update"
/>
<ArrayList
v-model:value="network.exclude"
:show-header="true"
:default-add-value="''"
:mode="mode"
:add-disabled="disableEdit"
:add-label="t('harvester.setting.vmMigrationNetwork.exclude.addButton')"
:value-label="t('harvester.setting.vmMigrationNetwork.exclude.label')"
:value-placeholder="t('harvester.setting.storageNetwork.exclude.placeholder')"
@update:value="update"
/>
</template>
</div>
</template>
+5 -10
View File
@@ -1,13 +1,8 @@
// suffix of doc link, see utils/feature-flags.js how to get complete doc link
export const DOC = {
CONSOLE_URL: `/host/#remote-console`,
RANCHER_INTEGRATION_URL: `/rancher/rancher-integration`,
KSMTUNED_MODE: `/host/#ksmtuned-mode`,
UPGRADE_URL: `/upgrade/index`,
UPGRADE_CONFIG_URL: `/advanced/index#upgrade-config`,
STORAGE_NETWORK_EXAMPLE: `/advanced/storagenetwork#configuration-example`,
SUPPORT_BUNDLE_NAMESPACES: `/advanced/index/#support-bundle-namespaces`,
VPC_CONFIGURATION_EXAMPLES: `/networking/kubeovn-vpc#vpc-peering-configuration-examples`,
NETWORK_POLICY: `/networking/kubeovn-vm-isolation/#network-policies`,
TRANSPARENT_HUGEPAGES: `https://docs.kernel.org/admin-guide/mm/transhuge.html`,
CONSOLE_URL: `/host/#remote-console`,
RANCHER_INTEGRATION_URL: `/rancher/rancher-integration`,
STORAGE_NETWORK_EXAMPLE: `/advanced/storagenetwork#configuration-example`,
KSMTUNED_MODE: `/host/#ksmtuned-mode`,
UPGRADE_URL: `/upgrade/index`
};
+55 -73
View File
@@ -1,75 +1,57 @@
import semver from 'semver';
// https://github.com/harvester/dashboard/releases/tag/v1.3.0
const featuresV130 = [
'supportHarvesterClusterVersion'
];
const FEATURE_FLAGS = {
'v1.3.0': [
'supportHarvesterClusterVersion'
],
'v1.3.1': [
'autoRotateRke2CertsSetting',
'supportBundleNodeCollectionTimeoutSetting'
],
'v1.3.2': [
'kubeconfigDefaultTokenTTLMinutesSetting',
'improveMaintenanceMode',
],
'v1.3.3': [],
'v1.4.0': [
'cpuPinning',
'usbPassthrough',
'volumeEncryption',
'schedulingVMBackup',
'vmSnapshotQuota',
'longhornV2LVMSupport',
'improveMaintenanceMode',
'upgradeConfigSetting'
],
'v1.4.1': [],
'v1.4.2': [
'refreshIntervalInSecond',
'allowEmptySnapshotClassName'
],
'v1.4.3': [],
'v1.5.0': [
'tpmPersistentState',
'efiPersistentState',
'untaggedNetworkSetting',
'skipSingleReplicaDetachedVol',
'thirdPartyStorage',
'liveMigrationProgress'
],
'v1.5.1': [],
'v1.6.0': [
'customSupportBundle',
'csiOnlineExpandValidation',
'vmNetworkMigration',
'kubeovnVpcSubnet',
'rancherClusterSetting',
'cpuMemoryHotplug',
'cdiSettings',
'vmCloneRunStrategy',
],
'v1.6.1': [],
'v1.7.0': [
'vmMachineTypeAuto',
'lhV2VolExpansion',
'l2VlanTrunkMode',
'kubevirtMigration',
'hotplugNic'
]
// https://github.com/harvester/dashboard/releases/tag/v1.3.1
const featuresV131 = [
...featuresV130,
'autoRotateRke2CertsSetting',
'supportBundleNodeCollectionTimeoutSetting'
];
// https://github.com/harvester/dashboard/releases/tag/v1.3.2
const featuresV132 = [
...featuresV131,
'kubeconfigDefaultTokenTTLMinutesSetting',
'improveMaintenanceMode',
];
// TODO: add v1.3.3 official release note
// https://github.com/harvester/dashboard/releases/tag/v1.3.3-dev-20250105
const featuresV133 = [
...featuresV132,
];
// https://github.com/harvester/dashboard/releases/tag/v1.4.0
const featuresV140 = [
...featuresV132,
'cpuPinning',
'usbPassthrough',
'volumeEncryption',
'schedulingVMBackup',
'vmSnapshotQuota',
'longhornV2LVMSupport',
'improveMaintenanceMode',
];
// TODO: add v1.4.1 official release note
// https://github.com/harvester/dashboard/releases/tag/v1.4.1-rc1
const featuresV141 = [
...featuresV140
];
// TODO: add v1.5.0 official release note
const featuresV150 = [
...featuresV141
];
export const RELEASE_FEATURES = {
'v1.3.0': featuresV130,
'v1.3.1': featuresV131,
'v1.3.2': featuresV132,
'v1.3.3': featuresV133,
'v1.4.0': featuresV140,
'v1.4.1': featuresV141,
'v1.5.0': featuresV150
};
const generateFeatureFlags = () => {
const versions = [...Object.keys(FEATURE_FLAGS)].filter((version) => semver.valid(version)).sort(semver.compare);
const generatedFlags = {};
versions.forEach((version, index) => {
const previousVersion = versions[index - 1];
generatedFlags[version] = previousVersion ? [...generatedFlags[previousVersion], ...FEATURE_FLAGS[version]] : [...FEATURE_FLAGS[version]];
});
return generatedFlags;
};
export const RELEASE_FEATURES = generateFeatureFlags();
+2 -61
View File
@@ -10,7 +10,6 @@ import {
LOGGING,
STORAGE_CLASS,
SECRET,
NETWORK_POLICY
} from '@shell/config/types';
import { HCI, VOLUME_SNAPSHOT } from '../types';
import {
@@ -25,7 +24,6 @@ import {
CONFIGURED_PROVIDERS,
SUB_TYPE,
ADDRESS,
DESCRIPTION,
} from '@shell/config/table-headers';
import { IF_HAVE } from '@shell/store/type-map';
import {
@@ -33,9 +31,7 @@ import {
FINGERPRINT,
IMAGE_PROGRESS,
SNAPSHOT_TARGET_VOLUME,
IMAGE_VIRTUAL_SIZE,
IMAGE_STORAGE_CLASS,
HARVESTER_DESCRIPTION
IMAGE_VIRTUAL_SIZE
} from './table-headers';
const TEMPLATE = HCI.VM_VERSION;
@@ -225,8 +221,6 @@ export function init($plugin, store) {
STATE,
NAME_COL,
NAMESPACE_COL,
HARVESTER_DESCRIPTION,
IMAGE_STORAGE_CLASS,
IMAGE_PROGRESS,
IMAGE_DOWNLOAD_SIZE,
IMAGE_VIRTUAL_SIZE,
@@ -259,7 +253,7 @@ export function init($plugin, store) {
});
if (isSingleVirtualCluster) {
headers(NAMESPACE, [STATE, NAME_UNLINKED, DESCRIPTION, AGE]);
headers(NAMESPACE, [STATE, NAME_UNLINKED, AGE]);
basicType([NAMESPACE]);
virtualType({
labelKey: 'harvester.namespace.label',
@@ -425,8 +419,6 @@ export function init($plugin, store) {
[
HCI.CLUSTER_NETWORK,
HCI.NETWORK_ATTACHMENT,
HCI.VPC,
NETWORK_POLICY,
HCI.LB,
HCI.IP_POOL,
],
@@ -457,7 +449,6 @@ export function init($plugin, store) {
HCI.PCI_DEVICE,
HCI.SR_IOVGPU_DEVICE,
HCI.VGPU_DEVICE,
HCI.MIG_CONFIGURATION,
HCI.USB_DEVICE,
HCI.ADD_ONS,
HCI.SECRET,
@@ -554,36 +545,6 @@ export function init($plugin, store) {
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, {
isCreatable: false,
location: {
@@ -850,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({
labelKey: 'harvester.usb.label',
group: 'advanced',
-15
View File
@@ -1,6 +1,3 @@
import { HCI } from '../types';
// TODO: delete this not used variable
export const MemoryUnit = [{
label: 'Mi',
value: 'Mi'
@@ -72,7 +69,6 @@ export const ADD_ONS = {
RANCHER_MONITORING: 'rancher-monitoring',
VM_IMPORT_CONTROLLER: 'vm-import-controller',
LVM_DRIVER: 'lvm.driver.harvesterhci.io',
KUBEOVN_OPERATOR: 'kubeovn-operator',
};
export const CSI_SECRETS = {
@@ -83,14 +79,3 @@ export const CSI_SECRETS = {
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',
};
// 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
};
+56 -70
View File
@@ -7,74 +7,60 @@ export const LABELS_TO_IGNORE_REGEX = [
];
export const HCI = {
CLOUD_INIT: 'harvesterhci.io/cloud-init-template',
CURRENT_IP: 'rke2.io/internal-ip',
IMAGE_ID: 'harvesterhci.io/imageId',
SSH_NAMES: 'harvesterhci.io/sshNames',
NETWORK_IPS: 'network.harvesterhci.io/ips',
TEMPLATE_VERSION_CUSTOM_NAME: 'template-version.harvesterhci.io/customName',
CREATOR: 'harvesterhci.io/creator',
OS: 'harvesterhci.io/os',
GOLDEN_IMAGE: 'harvesterhci.io/goldenImage',
NETWORK_TYPE: 'network.harvesterhci.io/type',
VM_NAME: 'harvesterhci.io/vmName',
VM_NAME_PREFIX: 'harvesterhci.io/vmNamePrefix',
VM_RESERVED_MEMORY: 'harvesterhci.io/reservedMemory',
MAINTENANCE_STATUS: 'harvesterhci.io/maintain-status',
HOST_CUSTOM_NAME: 'harvesterhci.io/host-custom-name',
HOST_CONSOLE_URL: 'harvesterhci.io/host-console-url',
RESTORE_NAME: 'restore.harvesterhci.io/name',
NODE_ROLE_MASTER: 'node-role.kubernetes.io/master',
NODE_ROLE_CONTROL_PLANE: 'node-role.kubernetes.io/control-plane',
NODE_ROLE_ETCD: 'node-role.harvesterhci.io/witness',
PROMOTE_STATUS: 'harvesterhci.io/promote-status',
MIGRATION_STATE: 'harvesterhci.io/migrationState',
VOLUME_CLAIM_TEMPLATE: 'harvesterhci.io/volumeClaimTemplates',
IMAGE_NAME: 'harvesterhci.io/image-name',
INIT_IP: 'etcd.rke2.cattle.io/node-address',
NODE_SCHEDULABLE: 'kubevirt.io/schedulable',
NETWORK_ROUTE: 'network.harvesterhci.io/route',
MATCHED_NODES: 'network.harvesterhci.io/matched-nodes',
UPGRADE: 'harvesterhci.io/upgrade',
OS_UPGRADE_IMAGE: 'harvesterhci.io/os-upgrade-image',
LATEST_UPGRADE: 'harvesterhci.io/latestUpgrade',
UPGRADE_STATE: 'harvesterhci.io/upgradeState',
REAY_MESSAGE: 'harvesterhci.io/read-message',
DYNAMIC_SSHKEYS_NAMES: 'harvesterhci.io/dynamic-ssh-key-names',
DYNAMIC_SSHKEYS_USERS: 'harvesterhci.io/dynamic-ssh-key-users',
IMAGE_SUFFIX: 'harvesterhci.io/image-type',
OS_TYPE: 'harvesterhci.io/os-type',
STORAGE_PROVISIONER: 'harvesterhci.io/storageProvisioner',
SKIP_SINGLE_REPLICA_DETACHED_VOL: 'harvesterhci.io/skipSingleReplicaDetachedVol',
HOST_REQUEST: 'management.cattle.io/pod-requests',
STORAGE_CLASS: 'harvesterhci.io/storageClassName',
STORAGE_NETWORK: 'storage-network.settings.harvesterhci.io',
ADDON_EXPERIMENTAL: 'addon.harvesterhci.io/experimental',
ADDON_DISPLAYNAME: 'addon.harvesterhci.io/displayName',
VOLUME_ERROR: 'longhorn.io/volume-scheduling-error',
VOLUME_FOR_VM: 'harvesterhci.io/volumeForVirtualMachine',
KVM_AMD_CPU: 'cpu-feature.node.kubevirt.io/svm',
KVM_INTEL_CPU: 'cpu-feature.node.kubevirt.io/vmx',
NODE_MANUFACTURER: 'manufacturer',
NODE_MODEL: 'model',
NODE_SERIAL_NUMBER: 'serialNumber',
VM_INSUFFICIENT: 'harvesterhci.io/insufficient-resource-quota',
NODE_NTP_SYNC_STATUS: 'node.harvesterhci.io/ntp-service',
PARENT_SRIOV: 'harvesterhci.io/parent-sriov-network-device',
PARENT_SRIOV_GPU: 'harvesterhci.io/parentSRIOVGPUDevice',
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',
CLOUD_INIT: 'harvesterhci.io/cloud-init-template',
CURRENT_IP: 'rke2.io/internal-ip',
IMAGE_ID: 'harvesterhci.io/imageId',
SSH_NAMES: 'harvesterhci.io/sshNames',
NETWORK_IPS: 'network.harvesterhci.io/ips',
TEMPLATE_VERSION_CUSTOM_NAME: 'template-version.harvesterhci.io/customName',
CREATOR: 'harvesterhci.io/creator',
OS: 'harvesterhci.io/os',
NETWORK_TYPE: 'network.harvesterhci.io/type',
VM_NAME: 'harvesterhci.io/vmName',
VM_NAME_PREFIX: 'harvesterhci.io/vmNamePrefix',
VM_RESERVED_MEMORY: 'harvesterhci.io/reservedMemory',
MAINTENANCE_STATUS: 'harvesterhci.io/maintain-status',
HOST_CUSTOM_NAME: 'harvesterhci.io/host-custom-name',
HOST_CONSOLE_URL: 'harvesterhci.io/host-console-url',
RESTORE_NAME: 'restore.harvesterhci.io/name',
NODE_ROLE_MASTER: 'node-role.kubernetes.io/master',
NODE_ROLE_CONTROL_PLANE: 'node-role.kubernetes.io/control-plane',
NODE_ROLE_ETCD: 'node-role.harvesterhci.io/witness',
PROMOTE_STATUS: 'harvesterhci.io/promote-status',
MIGRATION_STATE: 'harvesterhci.io/migrationState',
VOLUME_CLAIM_TEMPLATE: 'harvesterhci.io/volumeClaimTemplates',
IMAGE_NAME: 'harvesterhci.io/image-name',
INIT_IP: 'etcd.rke2.cattle.io/node-address',
NODE_SCHEDULABLE: 'kubevirt.io/schedulable',
NETWORK_ROUTE: 'network.harvesterhci.io/route',
MATCHED_NODES: 'network.harvesterhci.io/matched-nodes',
OS_UPGRADE_IMAGE: 'harvesterhci.io/os-upgrade-image',
LATEST_UPGRADE: 'harvesterhci.io/latestUpgrade',
UPGRADE_STATE: 'harvesterhci.io/upgradeState',
REAY_MESSAGE: 'harvesterhci.io/read-message',
DYNAMIC_SSHKEYS_NAMES: 'harvesterhci.io/dynamic-ssh-key-names',
DYNAMIC_SSHKEYS_USERS: 'harvesterhci.io/dynamic-ssh-key-users',
IMAGE_SUFFIX: 'harvesterhci.io/image-type',
OS_TYPE: 'harvesterhci.io/os-type',
STORAGE_PROVISIONER: 'harvesterhci.io/storageProvisioner',
HOST_REQUEST: 'management.cattle.io/pod-requests',
STORAGE_CLASS: 'harvesterhci.io/storageClassName',
STORAGE_NETWORK: 'storage-network.settings.harvesterhci.io',
ADDON_EXPERIMENTAL: 'addon.harvesterhci.io/experimental',
VOLUME_ERROR: 'longhorn.io/volume-scheduling-error',
KVM_AMD_CPU: 'cpu-feature.node.kubevirt.io/svm',
KVM_INTEL_CPU: 'cpu-feature.node.kubevirt.io/vmx',
NODE_MANUFACTURER: 'manufacturer',
NODE_MODEL: 'model',
NODE_SERIAL_NUMBER: 'serialNumber',
VM_INSUFFICIENT: 'harvesterhci.io/insufficient-resource-quota',
NODE_NTP_SYNC_STATUS: 'node.harvesterhci.io/ntp-service',
PARENT_SRIOV: 'harvesterhci.io/parent-sriov-network-device',
PARENT_SRIOV_GPU: 'harvesterhci.io/parentSRIOVGPUDevice',
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',
};
-1
View File
@@ -1,2 +1 @@
export const CLUSTER_NETWORK = 'clusterNetwork';
export const VPC = 'vpc';
+13 -38
View File
@@ -4,6 +4,7 @@ export const HCI_SETTING = {
LOG_LEVEL: 'log-level',
SERVER_VERSION: 'server-version',
UI_INDEX: 'ui-index',
UI_PLUGIN_INDEX: 'ui-plugin-index',
UPGRADE_CHECKER_ENABLED: 'upgrade-checker-enabled',
UPGRADE_CHECKER_URL: 'upgrade-checker-url',
VLAN: 'vlan',
@@ -27,18 +28,12 @@ export const HCI_SETTING = {
RELEASE_DOWNLOAD_URL: 'release-download-url',
CCM_CSI_VERSION: 'harvester-csi-ccm-versions',
CSI_DRIVER_CONFIG: 'csi-driver-config',
CSI_ONLINE_EXPAND_VALIDATION: 'csi-online-expand-validation',
VM_TERMINATION_PERIOD: 'default-vm-termination-grace-period-seconds',
NTP_SERVERS: 'ntp-servers',
AUTO_ROTATE_RKE2_CERTS: 'auto-rotate-rke2-certs',
KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES: 'kubeconfig-default-token-ttl-minutes',
LONGHORN_V2_DATA_ENGINE_ENABLED: 'longhorn-v2-data-engine-enabled',
ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO: 'additional-guest-memory-overhead-ratio',
UPGRADE_CONFIG: 'upgrade-config',
VM_MIGRATION_NETWORK: 'vm-migration-network',
RANCHER_CLUSTER: 'rancher-cluster',
MAX_HOTPLUG_RATIO: 'max-hotplug-ratio',
KUBEVIRT_MIGRATION: 'kubevirt-migration'
};
export const HCI_ALLOWED_SETTINGS = {
@@ -57,25 +52,20 @@ export const HCI_ALLOWED_SETTINGS = {
from: 'import',
featureFlag: 'autoRotateRke2CertsSetting'
},
[HCI_SETTING.CSI_DRIVER_CONFIG]: { kind: 'json', from: 'import' },
[HCI_SETTING.CSI_ONLINE_EXPAND_VALIDATION]: {
kind: 'json', from: 'import', featureFlag: 'csiOnlineExpandValidation'
},
[HCI_SETTING.SERVER_VERSION]: { readOnly: true },
[HCI_SETTING.UPGRADE_CHECKER_ENABLED]: { kind: 'boolean' },
[HCI_SETTING.UPGRADE_CHECKER_URL]: { kind: 'url' },
[HCI_SETTING.HTTP_PROXY]: { kind: 'json', from: 'import' },
[HCI_SETTING.ADDITIONAL_CA]: {
[HCI_SETTING.CSI_DRIVER_CONFIG]: { kind: 'json', from: 'import' },
[HCI_SETTING.SERVER_VERSION]: { readOnly: true },
[HCI_SETTING.UPGRADE_CHECKER_ENABLED]: { kind: 'boolean' },
[HCI_SETTING.UPGRADE_CHECKER_URL]: { kind: 'url' },
[HCI_SETTING.HTTP_PROXY]: { kind: 'json', from: 'import' },
[HCI_SETTING.ADDITIONAL_CA]: {
kind: 'multiline', canReset: true, from: 'import'
},
[HCI_SETTING.OVERCOMMIT_CONFIG]: { kind: 'json', from: 'import' },
[HCI_SETTING.SUPPORT_BUNDLE_TIMEOUT]: { kind: 'number' },
[HCI_SETTING.SUPPORT_BUNDLE_EXPIRATION]: { kind: 'number' },
[HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT]: { kind: 'number', featureFlag: 'supportBundleNodeCollectionTimeoutSetting' },
[HCI_SETTING.SUPPORT_BUNDLE_TIMEOUT]: {},
[HCI_SETTING.SUPPORT_BUNDLE_EXPIRATION]: {},
[HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT]: { featureFlag: 'supportBundleNodeCollectionTimeoutSetting' },
[HCI_SETTING.SUPPORT_BUNDLE_IMAGE]: { kind: 'json', from: 'import' },
[HCI_SETTING.STORAGE_NETWORK]: {
kind: 'custom', from: 'import', canReset: true
},
[HCI_SETTING.STORAGE_NETWORK]: { kind: 'custom', from: 'import' },
[HCI_SETTING.VM_FORCE_RESET_POLICY]: { kind: 'json', from: 'import' },
[HCI_SETTING.SSL_CERTIFICATES]: { kind: 'json', from: 'import' },
[HCI_SETTING.SSL_PARAMETERS]: {
@@ -84,6 +74,7 @@ export const HCI_ALLOWED_SETTINGS = {
[HCI_SETTING.SUPPORT_BUNDLE_NAMESPACES]: { from: 'import', canReset: true },
[HCI_SETTING.AUTO_DISK_PROVISION_PATHS]: { canReset: true },
[HCI_SETTING.RELEASE_DOWNLOAD_URL]: { kind: 'url' },
[HCI_SETTING.UI_PLUGIN_INDEX]: { kind: 'url' },
[HCI_SETTING.CONTAINERD_REGISTRY]: {
kind: 'json', from: 'import', canReset: true
},
@@ -96,29 +87,13 @@ export const HCI_ALLOWED_SETTINGS = {
[HCI_SETTING.NTP_SERVERS]: {
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]: {
kind: 'boolean',
experimental: true,
featureFlag: 'longhornV2LVMSupport'
},
[HCI_SETTING.ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO]: { kind: 'string', from: 'import' },
[HCI_SETTING.UPGRADE_CONFIG]: {
kind: 'json',
from: 'import',
featureFlag: 'upgradeConfigSetting',
docPath: 'UPGRADE_CONFIG_URL'
},
[HCI_SETTING.RANCHER_CLUSTER]: {
kind: 'custom', from: 'import', canReset: true, featureFlag: 'rancherClusterSetting'
},
[HCI_SETTING.MAX_HOTPLUG_RATIO]: { kind: 'number', featureFlag: 'cpuMemoryHotplug' },
[HCI_SETTING.VM_MIGRATION_NETWORK]: {
kind: 'json', from: 'import', canReset: true, featureFlag: 'vmNetworkMigration',
},
[HCI_SETTING.KUBEVIRT_MIGRATION]: {
kind: 'json', from: 'import', canReset: true, featureFlag: 'kubevirtMigration',
}
};
export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = {
-43
View File
@@ -1,7 +1,6 @@
/**
* Harvester
*/
import { DESCRIPTION } from '@shell/config/table-headers';
// image
export const IMAGE_DOWNLOAD_SIZE = {
@@ -89,45 +88,3 @@ export const MACHINE_POOLS = {
align: 'center',
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',
};
-27
View File
@@ -2,30 +2,3 @@ export const BACKUP_TYPE = {
BACKUP: 'backup',
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',
};
@@ -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 { LONGHORN, METRIC } from '@shell/config/types';
import { Banner } from '@components/Banner';
import { UNIT_SUFFIX } from '../../utils/unit';
import HarvesterCPUUsed from '../../formatters/HarvesterCPUUsed';
import HarvesterMemoryUsed from '../../formatters/HarvesterMemoryUsed';
import HarvesterStorageUsed from '../../formatters/HarvesterStorageUsed';
@@ -122,7 +121,7 @@ export default {
memoryUnits() {
const exponent = exponentNeeded(this.memoryTotal, 1024);
return `${ UNITS[exponent] }${ UNIT_SUFFIX }`;
return `${ UNITS[exponent] }iB`;
},
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,
NAME,
{
name: 'CPU',
label: 'CPU',
sort: ['displayCPU'],
value: 'displayCPU',
align: 'center',
dashIfEmpty: true,
name: 'vmCPU',
labelKey: 'tableHeaders.cpu',
search: false,
sort: ['spec.template.spec.domain.cpu.cores'],
value: 'spec.template.spec.domain.cpu.cores',
width: 120
},
{
name: 'Memory',
value: 'displayMemory',
sort: ['memorySort'],
align: 'center',
labelKey: 'tableHeaders.memory',
formatter: 'Si',
formatterOpts: {
opts: {
increment: 1024, addSuffix: true, maxExponent: 3, minExponent: 3, suffix: 'i',
},
needParseSi: true
},
name: 'vmRAM',
labelKey: 'glance.memory',
search: false,
sort: ['memorySort'],
value: 'spec.template.spec.domain.resources.limits.memory',
width: 120
},
{
name: 'ip',
@@ -9,7 +9,7 @@ import ArrayListGrouped from '@shell/components/form/ArrayListGrouped';
import Loading from '@shell/components/Loading.vue';
import SortableTable from '@shell/components/SortableTable';
import Banner from '@components/Banner/Banner.vue';
import { UNIT_SUFFIX } from '../../utils/unit';
import metricPoller from '@shell/mixins/metric-poller';
import {
METRIC, NODE, LONGHORN, POD, EVENT
@@ -27,7 +27,6 @@ import Instance from './VirtualMachineInstance';
import Disk from './HarvesterHostDisk';
import VlanStatus from './VlanStatus';
import HarvesterKsmtuned from './HarvesterKsmtuned.vue';
import HarvesterHugepages from './HarvesterHugepages.vue';
import HarvesterSeeder from './HarvesterSeeder';
const LONGHORN_SYSTEM = 'longhorn-system';
@@ -47,7 +46,6 @@ export default {
VlanStatus,
LabelValue,
HarvesterKsmtuned,
HarvesterHugepages,
Loading,
SortableTable,
HarvesterSeeder,
@@ -180,7 +178,7 @@ export default {
minExponent: 3,
maxExponent: 3,
maxPrecision: 2,
suffix: UNIT_SUFFIX,
suffix: 'iB',
};
const longhornDisks = Object.keys(diskStatus).map((key) => {
@@ -211,12 +209,6 @@ export default {
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() {
return !!this.$store.getters['harvester/schemaFor'](HCI.BLOCK_DEVICE);
},
@@ -476,16 +468,6 @@ export default {
/>
</Tab>
<Tab
v-if="hasHugepagesSchema"
name="hugepages"
:weight="0"
:show-header="false"
:label="t('harvester.host.tabs.hugepages')"
>
<HarvesterHugepages :node="value" />
</Tab>
<Tab
v-if="seederEnabled"
name="seeder"
@@ -125,7 +125,6 @@ export default {
:resource="value"
:mode="mode"
:apply-hooks="applyHooks"
@error="e=>errors=e"
>
<Tabbed
v-if="spec"
@@ -172,9 +171,6 @@ export default {
:cpu="cpu"
:mode="mode"
:memory="memory"
:max-cpu="maxCpu"
:max-memory="maxMemory"
:enable-hot-plug="cpuMemoryHotplugEnabled"
/>
</div>
<div class="row">
@@ -118,10 +118,6 @@ export default {
imageName() {
return this.value?.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_NAME] || '-';
},
sourceType() {
return this.value?.spec?.sourceType;
},
}
};
</script>
@@ -256,16 +252,6 @@ export default {
</div>
</div>
<div class="row">
<div class="col span-12">
<LabelValue
:name="t('harvester.image.source')"
:value="sourceType"
class="mb-20"
/>
</div>
</div>
<div
v-if="errorMessage !== '-'"
class="row"
@@ -125,7 +125,6 @@ export default {
:resource="value"
:mode="mode"
:apply-hooks="applyHooks"
@error="e=>errors=e"
>
<Tabbed
v-if="spec"
@@ -172,9 +171,6 @@ export default {
:cpu="cpu"
:mode="mode"
:memory="memory"
:max-cpu="maxCpu"
:max-memory="maxMemory"
:enable-hot-plug="cpuMemoryHotplugEnabled"
/>
<div class="row mb-10">
@@ -5,7 +5,6 @@ import CreateEditView from '@shell/mixins/create-edit-view';
import HarvesterIpAddress from '../../../formatters/HarvesterIpAddress';
import VMConsoleBar from '../../../components/VMConsoleBar';
import { HCI } from '../../../types';
import { getVmCPUMemoryValues } from '../../../utils/cpuMemory';
const UNDEFINED = 'n/a';
@@ -92,9 +91,9 @@ export default {
},
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() {
@@ -17,13 +17,6 @@ export default {
default: () => {
return {};
}
},
vmimResource: {
type: Object,
required: true,
default: () => {
return {};
}
}
},
@@ -32,12 +25,6 @@ export default {
},
computed: {
liveMigrationProgressEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('liveMigrationProgress');
},
migrationPhase() {
return this.vmimResource?.status?.phase || 'N/A';
},
migrationState() {
return this.localResource?.status?.migrationState;
},
@@ -71,18 +58,6 @@ export default {
<template>
<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="col span-6">
<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_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 {
name: 'VMIDetailsPage',
@@ -65,7 +63,6 @@ export default {
hasResourceQuotaSchema: false,
switchToCloud: false,
VM_METRICS_DETAIL_URL,
VM_MIGRATION_DETAIL_URL,
showVmMetrics: false,
};
},
@@ -81,7 +78,6 @@ export default {
events: this.$store.dispatch(`${ inStore }/findAll`, { type: EVENT }),
allSSHs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SSH }),
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 }),
};
@@ -135,39 +131,6 @@ export default {
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() {
return this.allEvents.filter((e) => {
const { name, creationTimestamp } = this.value?.metadata || {};
@@ -175,6 +138,7 @@ export default {
const pvcName = this.value.persistentVolumeClaimName || [];
const involvedName = e?.involvedObject?.name;
const matchPVC = pvcName.find((name) => name === involvedName);
return (involvedName === name || involvedName === podName || matchPVC) && e.firstTimestamp >= creationTimestamp;
@@ -193,10 +157,6 @@ export default {
vm: this.value.name
};
},
liveMigrationProgressEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('liveMigrationProgress');
},
},
methods: {
@@ -213,7 +173,6 @@ export default {
const diskRows = this.getDiskRows(neu);
this['diskRows'] = diskRows;
this['networkRows'] = this.getNetworkRows(neu, { fromTemplate: false, init: false });
},
deep: true
}
@@ -266,7 +225,6 @@ export default {
<Network
v-model:value="networkRows"
mode="view"
:vm="value"
/>
</Tab>
@@ -388,20 +346,6 @@ export default {
<Migration
:value="value"
: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>
+11 -26
View File
@@ -1,13 +1,12 @@
<script>
import { exceptionToErrorsArray } from '@shell/utils/error';
import { mapGetters } from 'vuex';
import { runStrategies as runStrategyOptions } from '../config/harvester-map';
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';
import LabeledSelect from '@shell/components/form/LabeledSelect';
export default {
name: 'CloneVMModal',
@@ -15,7 +14,7 @@ export default {
emits: ['close'],
components: {
AsyncButton, Banner, Checkbox, Card, LabeledInput, LabeledSelect
AsyncButton, Banner, Checkbox, Card, LabeledInput
},
props: {
@@ -27,11 +26,9 @@ export default {
data() {
return {
name: '',
cloneData: true,
errors: [],
runStrategy: runStrategyOptions[1],
runStrategyOptions
name: '',
cloneData: true,
errors: []
};
},
@@ -41,9 +38,6 @@ export default {
actionResource() {
return this.resources[0];
},
vmCloneRunStrategyEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('vmCloneRunStrategy');
},
},
methods: {
@@ -64,8 +58,7 @@ export default {
// deep clone
try {
const payload = this.vmCloneRunStrategyEnabled ? { targetVm: this.name, runStrategy: this.runStrategy } : { targetVm: this.name };
const res = await this.actionResource.doAction('clone', payload, {}, false);
const res = await this.actionResource.doAction('clone', { targetVm: this.name }, {}, false);
if (res._status === 200 || res._status === 204) {
this.$store.dispatch('growl/success', {
@@ -113,19 +106,6 @@ export default {
:label="t('harvester.modal.cloneVM.name')"
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
@@ -147,6 +127,11 @@ export default {
@click="create"
/>
</div>
<Banner
v-for="(err, i) in errors"
:key="i"
/>
</template>
</Card>
</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: {
type: Array,
required: true
},
}
},
data() {
@@ -42,25 +42,8 @@ export default {
computed: {
...mapState('action-menu', ['modalData']),
title() {
return this.modalData?.title || 'dialog.promptRemove.title';
},
formattedType() {
return this.type.toLowerCase();
},
warningMessage() {
if (this.modalData?.warningMessage) return this.modalData.warningMessage;
const isPlural = this.type.endsWith('s');
const thisOrThese = isPlural ? 'these' : 'this';
const defaultMessage = this.t('dialog.promptRemove.warningMessage', {
type: this.formattedType,
thisOrThese,
});
return defaultMessage;
warningMessageKey() {
return this.modalData.warningMessageKey;
},
names() {
@@ -87,12 +70,6 @@ export default {
return this.resources[0].nameDisplay;
},
needConfirmation() {
const { needConfirmation = true } = this.modalData ;
return needConfirmation === true;
},
plusMore() {
const remaining = this.resources.length - this.names.length;
@@ -120,15 +97,11 @@ export default {
},
deleteDisabled() {
if (!this.needConfirmation) {
return false;
}
return this.confirmName !== this.nameToMatch;
},
protip() {
return this.t('dialog.promptRemove.protip', { alternateLabel });
return this.t('promptRemove.protip', { alternateLabel });
},
},
@@ -145,7 +118,6 @@ export default {
try {
for (const resource of this.resources) {
await resource.remove();
if (this.modalData?.extraActionAfterRemove) await this.modalData.extraActionAfterRemove();
}
buttonDone(true);
this.close();
@@ -165,45 +137,35 @@ export default {
>
<template #title>
<h4 class="text-default-text">
{{ t(title, { type }, true) }}
{{ t('promptRemove.title') }}
</h4>
</template>
<template #body>
<div class="pl-10 pr-10">
<span
v-clean-html="warningMessage"
v-clean-html="t(warningMessageKey, { type, names: resourceNames }, true)"
></span>
<div
v-if="needConfirmation"
class="mt-20"
>
<div class="mt-10 mb-10">
<span
v-clean-html="t('dialog.promptRemove.confirmName', {
type: formattedType,
nameToMatch: escapeHtml(nameToMatch)
}, true)"
></span>
</div>
<div class="mb-10">
<CopyToClipboardText :text="nameToMatch" />
</div>
<input
id="confirm"
v-model="confirmName"
type="text"
/>
<div class="text-info mt-20">
{{ protip }}
</div>
<div class="mt-10 mb-10">
<span
v-clean-html="t('promptRemove.confirmName', { nameToMatch: escapeHtml(nameToMatch) }, true)"
></span>
</div>
<div class="mb-10">
<CopyToClipboardText :text="nameToMatch" />
</div>
<input
id="confirm"
v-model="confirmName"
type="text"
/>
<div class="text-info mt-20">
{{ protip }}
</div>
<Banner
v-for="(error, i) in errors"
:key="i"
:label="error"
color="error"
/>
</div>
</template>
+150
View File
@@ -0,0 +1,150 @@
<script>
import { exceptionToErrorsArray } from '@shell/utils/error';
import { mapGetters } from 'vuex';
import { Card } from '@components/Card';
import { Banner } from '@components/Banner';
import { Checkbox } from '@components/Form/Checkbox';
import AsyncButton from '@shell/components/AsyncButton';
import { LabeledInput } from '@components/Form/LabeledInput';
export default {
name: 'CloneVMModal',
emits: ['close'],
components: {
AsyncButton, Banner, Checkbox, Card, LabeledInput
},
props: {
resources: {
type: Array,
required: true
}
},
data() {
return {
name: '',
cloneData: true,
errors: []
};
},
computed: {
...mapGetters({ t: 'i18n/t' }),
actionResource() {
return this.resources[0];
},
},
methods: {
close() {
this.name = '';
this.$emit('close');
},
async create(buttonCb) {
// shadow clone
if (!this.cloneData) {
this.resources[0].goToClone();
buttonCb(false);
this.close();
return;
}
// deep clone
try {
const res = await this.actionResource.doAction('clone', { targetVm: this.name }, {}, false);
if (res._status === 200 || res._status === 204) {
this.$store.dispatch('growl/success', {
title: this.t('harvester.notification.title.succeed'),
message: this.t('harvester.modal.cloneVM.message.success', { name: this.name })
}, { root: true });
this.close();
buttonCb(true);
} else {
const error = [res?.data] || exceptionToErrorsArray(res);
this['errors'] = error;
buttonCb(false);
}
} catch (err) {
const error = err?.data || err;
const message = exceptionToErrorsArray(error);
this['errors'] = message;
buttonCb(false);
}
}
},
};
</script>
<template>
<Card :show-highlight-border="false">
<template #title>
{{ t('harvester.modal.cloneVM.title') }}
</template>
<template #body>
<Checkbox
v-model:value="cloneData"
class="mb-10"
label-key="harvester.modal.cloneVM.type"
/>
<LabeledInput
v-show="cloneData"
v-model:value="name"
class="mb-20"
:label="t('harvester.modal.cloneVM.name')"
required
/>
</template>
<template #actions>
<div class="actions">
<div class="buttons">
<button
class="btn role-secondary mr-10"
@click="close"
>
{{ t('generic.cancel') }}
</button>
<AsyncButton
mode="create"
:action-label="cloneData ? t('harvester.modal.cloneVM.action.create') : t('harvester.modal.cloneVM.action.clone')"
:disabled="cloneData && !name"
@click="create"
/>
</div>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</div>
</template>
</Card>
</template>
<style lang="scss" scoped>
.actions {
width: 100%;
}
.buttons {
display: flex;
justify-content: flex-end;
width: 100%;
}
</style>
@@ -11,7 +11,7 @@ import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
export default {
name: 'HotplugVolumeModal',
name: 'HotplugModal',
emits: ['close'],
@@ -57,12 +57,8 @@ export default {
if (!!pvc.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_ID]) {
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) => {
return {
@@ -90,7 +86,7 @@ export default {
if (res._status === 200 || res._status === 204) {
this.$store.dispatch('growl/success', {
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 });
this.close();
@@ -122,7 +118,7 @@ export default {
>
<template #title>
<h4
v-clean-html="t('harvester.modal.hotplugVolume.title')"
v-clean-html="t('harvester.modal.hotplug.title')"
class="text-default-text"
/>
</template>
@@ -141,12 +137,6 @@ export default {
class="mt-20"
required
/>
<Banner
v-for="(err, i) in errors"
:key="i"
:label="err"
color="error"
/>
</template>
<template #actions>
@@ -166,6 +156,11 @@ export default {
@click="save"
/>
</div>
<Banner
v-for="(err, i) in errors"
:key="i"
/>
</div>
</template>
</Card>
@@ -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>
@@ -103,12 +103,6 @@ export default {
:label="t('generic.name')"
required
/>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template>
<template #actions>
@@ -127,6 +121,13 @@ export default {
@click="save"
/>
</div>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</div>
</template>
</Card>
@@ -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>
@@ -120,12 +120,6 @@ export default {
v-model:value="description"
:label="t('harvester.modal.createTemplate.description')"
/>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template>
<template #actions>
@@ -144,6 +138,11 @@ export default {
@click="save"
/>
</div>
<Banner
v-for="(err, i) in errors"
:key="i"
/>
</div>
</template>
</Card>
@@ -128,12 +128,6 @@ export default {
<Banner color="warning">
<span>{{ t('harvester.modal.ejectCDROM.warnTip') }}</span>
</Banner>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template>
<template #actions>
@@ -153,6 +147,13 @@ export default {
@click="remove"
/>
</div>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</div>
</template>
</Card>
@@ -10,7 +10,6 @@ import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { NAMESPACE, STORAGE_CLASS } from '@shell/config/types';
import { allHash } from '@shell/utils/promise';
import { isInternalStorageClass } from '../utils/storage-class';
export default {
name: 'HarvesterExportImageDialog',
@@ -35,14 +34,9 @@ export default {
await allHash(hash);
const allStorages = this.allStorageClasses;
const defaultStorage = allStorages.find((s) => s.isDefault);
const defaultStorage = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS).find((s) => s.isDefault);
if (this.isLonghornV1Volume) {
this['storageClassName'] = defaultStorage?.metadata?.name || 'longhorn';
} else {
this['storageClassName'] = this.nonLonghornV1StorageClasses[0]?.metadata?.name || '';
}
this['storageClassName'] = defaultStorage?.metadata?.name || 'longhorn';
},
data() {
@@ -60,7 +54,7 @@ export default {
...mapGetters({ t: 'i18n/t' }),
actionResource() {
return this.resources[0] || {};
return this.resources[0];
},
namespaces() {
@@ -79,48 +73,24 @@ export default {
return out;
},
isLonghornV1Volume() {
return this.actionResource?.isLonghornV1 === true;
},
disableSave() {
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() {
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
if (!this.isLonghornV1Volume) {
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') })`;
}
const out = storages.filter((s) => !s.parameters?.backingImage).map((s) => {
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
return {
label,
value: s.name,
disabled: isInternal,
value: s.name,
};
}) || [];
return options;
return out;
},
},
@@ -194,12 +164,6 @@ export default {
class="mt-20"
required
/>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template>
<template #actions>
@@ -218,6 +182,11 @@ export default {
@click="save"
/>
</div>
<Banner
v-for="(err, i) in errors"
:key="i"
/>
</div>
</template>
</Card>
@@ -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>
@@ -161,12 +161,6 @@ export default {
:placeholder="t('harvester.modal.migration.fields.nodeName.placeholder')"
:options="nodeNameList"
/>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template>
<template
@@ -187,6 +181,12 @@ export default {
@click="apply"
/>
</div>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template>
</Card>
</template>
@@ -5,7 +5,6 @@ import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton';
import UnitInput from '@shell/components/form/UnitInput';
import { exceptionToErrorsArray } from '@shell/utils/error';
import { GIBIBYTE } from '../utils/unit';
export default {
name: 'HarvesterVMQuotaDialog',
@@ -30,7 +29,6 @@ export default {
data() {
return {
GIBIBYTE,
totalSnapshotSize: '',
errors: []
};
@@ -46,6 +44,7 @@ export default {
},
methods: {
close() {
this.totalSnapshotSize = '';
this.$emit('close');
@@ -93,15 +92,9 @@ export default {
:input-exponent="3"
:increment="1024"
:output-modifier="true"
:suffix="GIBIBYTE"
suffix="GiB"
class="mb-20"
/>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template>
<template #actions>
@@ -115,6 +108,12 @@ export default {
</button>
<AsyncButton @click="save" />
</div>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</div>
</template>
</Card>
@@ -136,12 +136,6 @@ export default {
:options="backupOption"
required
/>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template>
<template #actions>
@@ -160,6 +154,11 @@ export default {
@click="saveRestore"
/>
</div>
<Banner
v-for="(err, i) in errors"
:key="i"
/>
</div>
</template>
</Card>
+86 -248
View File
@@ -1,5 +1,4 @@
<script>
import { NAMESPACE } from '@shell/config/types';
import { randomStr } from '@shell/utils/string';
import { exceptionToErrorsArray, stringify } from '@shell/utils/error';
import { LabeledInput } from '@components/Form/LabeledInput';
@@ -7,15 +6,7 @@ import AsyncButton from '@shell/components/AsyncButton';
import GraphCircle from '@shell/components/graph/Circle';
import { Banner } from '@components/Banner';
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_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 {
name: 'SupportBundle',
@@ -26,36 +17,14 @@ export default {
AsyncButton,
Banner,
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() {
return {
isOpen: false,
errors: [],
version: '',
clusterName: '',
url: '',
description: '',
namespaces: [],
defaultNamespaces: [],
timeout: '',
expiration: '',
nodeTimeout: '',
url: '',
description: '',
errors: [],
isOpen: false,
};
},
@@ -70,51 +39,23 @@ export default {
percentage() {
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: {
isShowBundleModal: {
immediate: true,
handler(show) {
this.isOpen = show;
}
},
isOpen(newVal) {
if (newVal) {
this.loadDefaultSettings();
} else {
this.resetForm();
}
if (show) {
this.$nextTick(() => {
this.isOpen = true;
});
} else {
this.isOpen = false;
this.url = '';
this.description = '';
}
},
immediate: true
},
},
@@ -124,93 +65,33 @@ export default {
close() {
this.isOpen = false;
this.$store.commit('harvester-common/toggleBundleModal', false);
},
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();
}
this.backUpName = '';
},
async save(buttonCb) {
this.errors = [];
const name = `bundle-${ this.clusterName }-${ this.version }-${ randomStr(5).toLowerCase() }`;
const name = `bundle-${ randomStr(5).toLowerCase() }`;
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 = {
apiVersion: 'harvesterhci.io/v1beta1',
type: HCI.SUPPORT_BUNDLE,
kind: 'SupportBundle',
metadata: { name, namespace },
spec,
metadata: {
name,
namespace
},
spec: {
issueURL: this.url,
description: this.description
}
};
try {
const inStore = this.$store.getters['currentProduct'].inStore;
const bundleValue = await this.$store.dispatch(`${ inStore }/create`, bundleCrd);
const inStore = this.$store.getters['currentProduct'].inStore;
const bundleValue = await this.$store.dispatch(`${ inStore }/create`, bundleCrd);
try {
await bundleValue.save();
this.$store.commit('harvester-common/setLatestBundleId', `${ namespace }/${ name }`, { root: true });
@@ -237,110 +118,69 @@ export default {
@close="close"
>
<div class="p-20">
<h2>{{ t('harvester.modal.bundle.title') }}</h2>
<div class="content">
<div
v-if="bundlePending"
class="circle mb-20"
>
<h2>
{{ t('harvester.modal.bundle.title') }}
</h2>
<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
primary-stroke-color="green"
secondary-stroke-color="lightgrey"
secondary-stroke-color="white"
:stroke-width="6"
:percentage="percentage"
:show-text="true"
/>
</div>
<template v-else>
<p
v-if="customSupportBundleFeatureEnabled"
v-clean-html="t('harvester.modal.bundle.tip', { doc: docLink }, true)"
class="mb-20"
/>
<LabeledInput
v-model:value="url"
:label="t('harvester.modal.bundle.url')"
class="mb-10"
/>
<LabeledInput
v-model:value="description"
required
:label="t('harvester.modal.bundle.description')"
type="multiline"
:min-height="80"
class="mb-10"
/>
</div>
<template v-if="customSupportBundleFeatureEnabled">
<LabeledSelect
v-model:value="namespaces"
:label="t('harvester.modal.bundle.namespaces.label')"
:clearable="true"
:multiple="true"
:options="namespaceOptions"
class="mb-10 label-select"
:tooltip="t('harvester.modal.bundle.namespaces.tooltip', _, true)"
@update:value="updateNamespaces"
/>
<UnitInput
v-model:value="timeout"
: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"
<div
v-for="(err, idx) in errors"
:key="idx"
>
<Banner
color="error"
:label="stringify(err)"
/>
</div>
<div class="footer mt-20">
<button
class="btn btn-sm role-secondary mr-10"
@click="close"
>
<Banner
color="error"
:label="stringify(err)"
/>
</div>
<div class="footer mt-20">
<button
class="btn btn-sm role-secondary mr-10"
@click="close"
>
{{ t('generic.close') }}
</button>
<AsyncButton
type="submit"
mode="generate"
class="btn btn-sm bg-primary"
:disabled="bundlePending"
@click="save"
/>
</div>
{{ t('generic.close') }}
</button>
<AsyncButton
type="submit"
mode="generate"
class="btn btn-sm bg-primary"
:disabled="bundlePending"
@click="save"
/>
</div>
</div>
</app-modal>
@@ -354,10 +194,6 @@ export default {
max-height: 100vh;
}
.labeled-select.taggable ::v-deep(.vs__selected-options .vs__selected.vs__selected > button) {
margin: 0 7px;
}
.bundle {
cursor: pointer;
color: var(--primary);
@@ -368,8 +204,10 @@ export default {
}
.content {
height: 218px;
.circle {
padding: 10px 0;
padding-top: 20px;
height: 160px;
}
}
@@ -1,12 +1,13 @@
<script>
import { mapState, mapGetters } from 'vuex';
import { exceptionToErrorsArray } from '@shell/utils/error';
import { Card } from '@components/Card';
import { Banner } from '@components/Banner';
import AsyncButton from '@shell/components/AsyncButton';
export default {
name: 'HarvesterHotUnplug',
name: 'HarvesterHotUnplugModal',
emits: ['close'],
@@ -34,25 +35,8 @@ export default {
actionResource() {
return this.resources[0];
},
name() {
return this.modalData.name;
},
isVolume() {
return this.modalData.type === 'volume';
},
titleKey() {
return this.isVolume ? 'harvester.virtualMachine.hotUnplug.detachVolume.title' : 'harvester.virtualMachine.hotUnplug.detachNIC.title';
},
actionLabelKey() {
return this.isVolume ? 'harvester.virtualMachine.hotUnplug.detachVolume.actionLabel' : 'harvester.virtualMachine.hotUnplug.detachNIC.actionLabel';
},
successMessageKey() {
return this.isVolume ? 'harvester.virtualMachine.hotUnplug.detachVolume.success' : 'harvester.virtualMachine.hotUnplug.detachNIC.success';
diskName() {
return this.modalData.diskName;
}
},
@@ -63,20 +47,14 @@ export default {
async save(buttonCb) {
try {
let res;
if (this.isVolume) {
res = await this.actionResource.doAction('removeVolume', { diskName: this.name });
} else {
res = await this.actionResource.doAction('removeNic', { interfaceName: this.name });
}
const res = await this.actionResource.doAction('removeVolume', { diskName: this.diskName });
if (res._status === 200 || res._status === 204) {
this.$store.dispatch(
'growl/success',
{
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 }
);
@@ -86,14 +64,14 @@ export default {
} else {
const error = [res?.data] || exceptionToErrorsArray(res);
this.errors = error;
this['errors'] = error;
buttonCb(false);
}
} catch (err) {
const error = err?.data || err;
const message = exceptionToErrorsArray(error);
this.errors = message;
this['errors'] = message;
buttonCb(false);
}
}
@@ -109,15 +87,9 @@ export default {
>
<template #title>
<h4
v-clean-html="t(titleKey, { name })"
v-clean-html="t('harvester.virtualMachine.unplug.title', { name: diskName })"
class="text-default-text"
/>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template>
<template #actions>
@@ -133,12 +105,19 @@ export default {
<AsyncButton
mode="apply"
:action-label="t(actionLabelKey)"
:waiting-label="t(actionLabelKey)"
:success-label="t(actionLabelKey)"
:action-label="t('harvester.virtualMachine.unplug.actionLabel')"
:waiting-label="t('harvester.virtualMachine.unplug.actionLabel')"
:success-label="t('harvester.virtualMachine.unplug.actionLabel')"
@click="save"
/>
</div>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</div>
</template>
</Card>
@@ -154,8 +133,4 @@ export default {
justify-content: flex-end;
width: 100%;
}
::v-deep(.card-title) {
display: block;
}
</style>
@@ -132,12 +132,6 @@ export default {
:label="t('generic.name')"
required
/>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template>
<template #actions>
@@ -156,6 +150,11 @@ export default {
@click="save"
/>
</div>
<Banner
v-for="(err, i) in errors"
:key="i"
/>
</div>
</template>
</Card>
@@ -73,10 +73,8 @@ export default {
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', {
url,
url: `/v1/harvester/${ HCI.VLAN_CONFIG }s/${ data.id }`,
method: 'PUT',
data,
});
@@ -109,12 +107,6 @@ export default {
:placeholder="t('harvester.harvesterVlanConfigMigrateDialog.targetClusterNetwork.placeholder')"
:options="clusterNetworks"
/>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template>
<template #actions>
@@ -133,6 +125,11 @@ export default {
@click="apply"
/>
</div>
<Banner
v-for="(err, i) in errors"
:key="i"
/>
</div>
</template>
</Card>
+6 -6
View File
@@ -70,16 +70,16 @@ export default {
v-clean-html="t('harvester.modal.restart.tip')"
class="pl-10 pr-10"
/>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template>
<template #actions>
<div class="bottom">
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
<div class="buttons">
<button
class="btn role-secondary mr-10"
@@ -145,12 +145,6 @@ export default {
class="mt-20"
required
/>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template>
<template #actions>
@@ -168,6 +162,10 @@ export default {
@click="save"
/>
</div>
<Banner
v-for="(err, i) in errors"
:key="i"
/>
</div>
</template>
</Card>
+6 -6
View File
@@ -79,12 +79,6 @@ export default {
:label="t('harvester.modal.snapshot.name')"
required
/>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template>
<template #actions>
@@ -102,6 +96,12 @@ export default {
@click="save"
/>
</div>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</div>
</template>
</Card>
+4 -6
View File
@@ -98,12 +98,6 @@ export default {
:label="t('harvester.modal.volumeClone.name')"
required
/>
<Banner
v-for="(err, i) in errors"
:key="i"
color="error"
:label="err"
/>
</template>
<template #actions>
@@ -121,6 +115,10 @@ export default {
@click="save"
/>
</div>
<Banner
v-for="(err, i) in errors"
:key="i"
/>
</div>
</template>
</Card>
@@ -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"
:errors="errors"
@finish="save"
@error="e=>errors=e"
>
<component
:is="currentComponent"
@@ -10,7 +10,7 @@ import { Banner } from '@components/Banner';
import CreateEditView from '@shell/mixins/create-edit-view';
const DEFAULT_VALUE = { image: { repository: 'rancher/harvester-nvidia-driver-toolkit' } };
const DEFAULT_VALUE = { image: { repo: 'rancher/harvester-nvidia-driver-toolkit' } };
export default {
name: 'EditAddonNvidiaDriverToolkit',
@@ -122,7 +122,7 @@ export default {
>
<div class="col span-6">
<LabeledInput
v-model:value="valuesContentJson.image.repository"
v-model:value="valuesContentJson.image.repo"
:mode="mode"
:required="true"
label-key="harvester.addons.nvidiaDriverToolkit.image.repository"
@@ -80,7 +80,6 @@ export default {
:apply-hooks="applyHooks"
@finish="save"
@cancel="done"
@error="e=>errors=e"
>
<NameNsDescription
:value="value"
@@ -8,7 +8,7 @@ import LabelValue from '@shell/components/LabelValue';
import { BadgeState } from '@components/BadgeState';
import { Banner } from '@components/Banner';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { RadioGroup } from '@components/Form/Radio';
import { RadioGroup, RadioButton } from '@components/Form/Radio';
import HarvesterDisk from '../../mixins/harvester-disk';
import Tags from '../../components/DiskTags';
import { HCI } from '../../types';
@@ -30,6 +30,7 @@ export default {
BadgeState,
Banner,
RadioGroup,
RadioButton,
ModalWithCard,
Tags,
},
@@ -183,11 +184,12 @@ export default {
},
forceFormattedDisabled() {
const lastFormattedAt = this.blockDevice?.status?.deviceStatus?.fileSystem?.LastFormattedAt;
const fileSystem = this.blockDevice?.status?.deviceStatus?.fileSystem.type;
const systems = ['ext4', 'XFS'];
if (this.blockDevice?.childParts?.length > 0) {
if (lastFormattedAt || this.blockDevice?.childParts?.length > 0) {
return true;
} else if (systems.includes(fileSystem)) {
return false;
@@ -444,7 +446,7 @@ export default {
/>
</div>
<div
v-if="(value.isNew && isLonghornV1) || isCorrupted"
v-if="(value.isNew && isLonghornV1 && !isFormatted) || isCorrupted"
class="col span-6"
>
<RadioGroup
@@ -457,6 +459,15 @@ export default {
:disabled="forceFormattedDisabled"
tooltip-key="harvester.host.disk.forceFormatted.toolTip"
>
<template #1="{option, listeners}">
<RadioButton
:label="option.label"
:val="option.value"
:value="value.forceFormatted"
:disabled="forceFormattedDisabled && !value.forceFormatted"
v-on="listeners"
/>
</template>
</RadioGroup>
</div>
<div
@@ -1,157 +0,0 @@
<script>
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
import { HCI } from '../../types';
import { DOC } from '../../config/doc-links';
export const hugepagesTHPEnabledMode = [{
label: 'Always',
value: 'always',
}, {
label: 'Madvise',
value: 'madvise',
}, {
label: 'Never',
value: 'never',
}];
export const hugepagesTHPShmemEnabledMode = [{
label: 'Always',
value: 'always',
}, {
label: 'Within Size',
value: 'within_size',
}, {
label: 'Advise',
value: 'advise',
}, {
label: 'Never',
value: 'never',
}, {
label: 'Deny',
value: 'deny',
}, {
label: 'Force',
value: 'force',
}];
export const hugepagesTHPDefragMode = [{
label: 'Always',
value: 'always',
}, {
label: 'Defer',
value: 'defer',
}, {
label: 'Defer+Madvise',
value: 'defer+madvise',
}, {
label: 'Madvise',
value: 'madvise',
}, {
label: 'Never',
value: 'never'
}];
export default {
name: 'HarvesterHugepages',
components: { LabeledSelect },
props: {
node: {
type: Object,
required: true,
},
registerBeforeHook: {
type: Function,
required: true,
},
},
computed: {
docsTransparentHugepagesLink() {
return DOC.TRANSPARENT_HUGEPAGES;
},
},
methods: {
async saveHugepages() {
this.hugepages['spec'] = this.spec;
await this.hugepages.save().catch((reason) => {
if (reason?.type === 'error') {
this.$store.dispatch('growl/error', {
title: this.t('harvester.notification.title.error'),
message: reason?.message,
}, { root: true });
return Promise.reject(new Error('saveHugepages error'));
}
});
},
},
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
const hash = await this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.HUGEPAGES });
this.hugepages = hash.find((node) => {
return node.id === this.node.id;
});
this.spec = this.hugepages.spec;
},
data() {
return {
hugepages: {},
spec: { transparent: {} },
hugepagesTHPEnabledMode,
hugepagesTHPShmemEnabledMode,
hugepagesTHPDefragMode,
};
},
created() {
this.registerBeforeHook(this.saveHugepages, 'saveHugepages');
},
};
</script>
<template>
<div>
<div>
<hr class="divider" />
<h3>
<t
k="harvester.host.hugepages.transparent.title"
:raw="true"
:url="docsTransparentHugepagesLink"
/>
</h3>
<div class="row mb-20">
<div class="col span-4">
<LabeledSelect
v-model:value="spec.transparent.enabled"
:label="t('harvester.host.hugepages.transparent.enabled')"
:options="hugepagesTHPEnabledMode"
/>
</div>
<div class="col span-4">
<LabeledSelect
v-model:value="spec.transparent.shmemEnabled"
:label="t('harvester.host.hugepages.transparent.shmemEnabled')"
:options="hugepagesTHPShmemEnabledMode"
/>
</div>
<div class="col span-4">
<LabeledSelect
v-model:value="spec.transparent.defrag"
:label="t('harvester.host.hugepages.transparent.defrag')"
:options="hugepagesTHPDefragMode"
/>
</div>
</div>
</div>
</div>
</template>
@@ -7,8 +7,6 @@ import { Checkbox } from '@components/Form/Checkbox';
import { HCI } from '../../types';
import { DOC } from '../../config/doc-links';
import { docLink } from '../../utils/feature-flags';
import isEqual from 'lodash/isEqual';
import { clone } from '@shell/utils/object';
export const ksmtunedMode = [{
value: 'standard',
@@ -66,13 +64,11 @@ export default {
this.enableMergeAcrossNodes = !!this.ksmtuned.spec?.mergeAcrossNodes;
this.spec = this.ksmtuned.spec;
this.originSpec = clone(this.ksmtuned.spec);
},
data() {
return {
ksmtuned: {},
originSpec: {},
spec: {},
thresCoef: 30,
ksmtunedMode,
@@ -103,11 +99,6 @@ export default {
methods: {
async saveKsmtuned() {
// no ksmtuned spec change, skip save
if (isEqual(this.spec, this.originSpec)) {
return;
}
this.spec.mergeAcrossNodes = this.enableMergeAcrossNodes ? 1 : 0;
this.ksmtuned['spec'] = this.spec;
@@ -20,7 +20,7 @@ import Loading from '@shell/components/Loading.vue';
import MessageLink from '@shell/components/MessageLink';
import { ADD_ONS } from '@pkg/harvester/config/harvester-map';
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '@pkg/harvester/config/harvester';
import { UNIT_SUFFIX } from '../../utils/unit';
import { _EDIT } from '@shell/config/query-params';
import { sortBy } from '@shell/utils/sort';
import { Banner } from '@components/Banner';
@@ -28,7 +28,6 @@ import { HCI } from '../../types';
import HarvesterDisk from './HarvesterDisk';
import HarvesterSeeder from './HarvesterSeeder';
import HarvesterKsmtuned from './HarvesterKsmtuned';
import HarvesterHugepages from './HarvesterHugepages';
import Tags from '../../components/DiskTags';
import { LVM_DRIVER } from '../../models/harvester/storage.k8s.io.storageclass';
import isEqual from 'lodash/isEqual';
@@ -51,7 +50,6 @@ export default {
ArrayListGrouped,
HarvesterDisk,
HarvesterKsmtuned,
HarvesterHugepages,
ButtonDropdown,
KeyValue,
Banner,
@@ -187,7 +185,7 @@ export default {
minExponent: 3,
maxExponent: 3,
maxPrecision: 2,
suffix: UNIT_SUFFIX,
suffix: 'iB',
};
const longhornDisks = Object.keys(diskStatus).map((key) => {
@@ -227,12 +225,6 @@ export default {
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() {
const inStore = this.$store.getters['currentProduct'].inStore;
@@ -465,7 +457,7 @@ export default {
const devPath = d.spec?.devPath;
const deviceType = d.status?.deviceStatus?.details?.deviceType;
const sizeBytes = d.status?.deviceStatus?.capacity?.sizeBytes;
const size = formatSi(sizeBytes, { increment: 1024, suffix: UNIT_SUFFIX });
const size = formatSi(sizeBytes, { increment: 1024 });
const parentDevice = d.status?.deviceStatus?.parentDevice;
const isChildAdded = this.newDisks.find((newDisk) => newDisk.blockDevice?.status?.deviceStatus?.parentDevice === devPath);
const name = d.displayName;
@@ -535,6 +527,7 @@ export default {
id: this.longhornNode.id,
opt: { force: true },
});
await new Promise((resolve) => setTimeout(resolve, '5000'));
await retrySave();
} else {
@@ -655,17 +648,6 @@ export default {
</template>
</ArrayListGrouped>
</Tab>
<Tab
v-if="hasHugepagesSchema"
name="Hugepages"
:weight="70"
:label="t('harvester.host.tabs.hugepages')"
>
<HarvesterHugepages
:node="value"
:register-before-hook="registerBeforeHook"
/>
</Tab>
<Tab
v-if="hasKsmtunedSchema"
name="Ksmtuned"
@@ -75,7 +75,6 @@ export default {
:errors="errors"
:apply-hooks="applyHooks"
@finish="save"
@error="e=>errors=e"
>
<div class="header mb-20">
<FileSelector
@@ -191,7 +191,7 @@ export default {
:validation-passed="true"
:errors="errors"
:can-yaml="true"
@error="e=>errors=e"
@error="e=>errors = e"
@finish="save"
@cancel="done"
>
@@ -189,7 +189,7 @@ export default {
:resource="value"
:subtypes="[]"
:cancel-event="true"
@error="e=>errors=e"
@error="e=>errors = e"
@finish="save"
@cancel="done"
>
@@ -6,17 +6,11 @@ import { LabeledInput } from '@components/Form/LabeledInput';
import { RadioGroup } from '@components/Form/Radio';
import NameNsDescription from '@shell/components/form/NameNsDescription';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { HCI as HCI_LABELS_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import CreateEditView from '@shell/mixins/create-edit-view';
import { allHash } from '@shell/utils/promise';
import { HCI } from '../types';
import { NETWORK_TYPE, L2VLAN_MODE } from '../config/types';
import { removeObject } from '@shell/utils/array';
const {
L2VLAN, UNTAGGED, OVERLAY, L2TRUNK_VLAN
} = NETWORK_TYPE;
const { ACCESS, TRUNK } = L2VLAN_MODE;
const AUTO = 'auto';
const MANUAL = 'manual';
@@ -47,17 +41,18 @@ export default {
data() {
const config = JSON.parse(this.value.spec.config);
const annotations = this.value?.metadata?.annotations || {};
const layer3Network = JSON.parse(annotations[HCI_LABELS_ANNOTATIONS.NETWORK_ROUTE] || '{}');
const type = this.value.vlanType || L2VLAN ;
if ((config.bridge || '').endsWith('-br')) {
config.bridge = config.bridge.slice(0, -3);
}
const type = this.value.vlanType || 'L2VlanNetwork' ;
return {
config,
type,
l2VlanMode: this.value.vlanType === L2TRUNK_VLAN ? TRUNK : ACCESS,
vlanTrunk: this.parseVlanTrunk(config),
layer3Network: {
mode: layer3Network.mode || AUTO,
serverIPAddr: layer3Network.serverIPAddr || '',
@@ -80,30 +75,6 @@ export default {
},
computed: {
clusterBridge: {
get() {
if (!this.config.bridge) {
return '';
}
// remove -br suffix if exists
return this.config?.bridge?.endsWith('-br') ? this.config.bridge.slice(0, -3) : '';
},
set(neu) {
if (neu === '') {
this.config.bridge = '';
return;
}
if (!neu.endsWith('-br')) {
this.config.bridge = `${ neu }-br`;
} else {
this.config.bridge = neu;
}
}
},
modeOptions() {
return [{
label: this.t('harvester.network.layer3Network.mode.auto'),
@@ -114,18 +85,6 @@ export default {
}];
},
l2VlanTrunkModeFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('l2VlanTrunkMode');
},
kubeovnVpcSubnetSupport() {
return this.$store.getters['harvester-common/getFeatureEnabled']('kubeovnVpcSubnet');
},
longhornV2LVMSupport() {
return this.$store.getters['harvester-common/getFeatureEnabled']('longhornV2LVMSupport');
},
clusterNetworkOptions() {
const inStore = this.$store.getters['currentProduct'].inStore;
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
@@ -141,163 +100,52 @@ export default {
});
},
l2VlanModeOptions() {
return [{
label: this.t('harvester.vlanStatus.vlanConfig.l2VlanMode.access'),
value: ACCESS,
}, {
label: this.t('harvester.vlanStatus.vlanConfig.l2VlanMode.trunk'),
value: TRUNK,
}];
},
networkTypes() {
const types = [L2VLAN, UNTAGGED];
if (this.kubeovnVpcSubnetSupport) {
types.push(OVERLAY);
}
return types;
},
isL2VlanNetwork() {
if (this.isView) {
return this.value.vlanType === L2VLAN;
}
return this.type === L2VLAN;
},
isL2VlanTrunkMode() {
if (this.isView) {
return this.value.vlanType === L2TRUNK_VLAN && this.l2VlanMode === TRUNK;
}
return this.type === L2TRUNK_VLAN || (this.l2VlanMode === TRUNK && this.type === L2VLAN);
},
isL2VlanAccessMode() {
if (this.isView) {
return this.value.vlanType === L2VLAN && this.l2VlanMode === ACCESS;
}
return this.type === L2VLAN && this.l2VlanMode === ACCESS;
},
isOverlayNetwork() {
if (this.isView) {
return this.value.vlanType === OVERLAY;
}
return this.type === OVERLAY;
networkType() {
return ['L2VlanNetwork', 'UntaggedNetwork'];
},
isUntaggedNetwork() {
if (this.isView) {
return this.value.vlanType === UNTAGGED;
return this.value.vlanType === 'UntaggedNetwork';
}
return this.type === UNTAGGED;
return this.type === 'UntaggedNetwork';
}
},
watch: {
type(newType) {
if (newType === OVERLAY) { // overlay network configuration
this.config.type = 'kube-ovn';
this.config.provider = `${ this.value.metadata.name }.${ this.value.metadata.namespace }.ovn`;
this.config.server_socket = '/run/openvswitch/kube-ovn-daemon.sock';
} else { // l2vlan or untagged network configuration
this.config.type = 'bridge';
this.config.promiscMode = true;
this.config.ipam = {};
this.config.bridge = '';
delete this.config.provider;
delete this.config.server_socket;
}
},
l2VlanMode(newAccessMode) {
if (this.type !== L2VLAN) {
return;
}
if (newAccessMode === TRUNK) { // trunk mode
this.config.vlan = 0;
this.config.vlanTrunk = this.vlanTrunk;
} else { // access mode
delete this.config.vlanTrunk;
this.config.vlan = '';
}
},
},
methods: {
async saveNetwork(buttonCb) {
const errors = [];
if (this.isL2VlanNetwork || this.isUntaggedNetwork) {
if (this.isL2VlanTrunkMode && this.vlanTrunk.some((trunk) => trunk.minID === '')) {
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.vlanStatus.vlanConfig.vlanTrunk.minId') }));
}
if (this.isL2VlanTrunkMode && this.vlanTrunk.some((trunk) => trunk.maxID === '')) {
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.vlanStatus.vlanConfig.vlanTrunk.maxId') }));
}
if (this.isL2VlanAccessMode && !this.config.vlan && !this.isUntaggedNetwork) {
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('tableHeaders.networkVlan') }));
}
if (!this.config.bridge) {
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.clusterNetwork.label') }));
}
if (this.layer3Network.mode === MANUAL) {
if (!this.layer3Network.gateway) {
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.gateway.label') }));
}
if (!this.layer3Network.cidr) {
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.cidr.label') }));
}
}
if (errors.length > 0) {
buttonCb(false);
this.errors = errors;
return false;
}
this.value.setAnnotation(HCI_LABELS_ANNOTATIONS.NETWORK_ROUTE, JSON.stringify(this.layer3Network));
if (!this.config.vlan && !this.isUntaggedNetwork) {
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('tableHeaders.networkVlan') }));
}
if (!this.config.bridge) {
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.clusterNetwork.label') }));
}
if (this.layer3Network.mode === MANUAL) {
if (!this.layer3Network.gateway) {
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.gateway.label') }));
}
if (!this.layer3Network.cidr) {
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.cidr.label') }));
}
}
if (errors.length > 0) {
buttonCb(false);
this.errors = errors;
return false;
}
this.value.setAnnotation(HCI_LABELS_ANNOTATIONS.NETWORK_ROUTE, JSON.stringify(this.layer3Network));
await this.save(buttonCb);
},
parseVlanTrunk(config) {
if (config?.vlanTrunk && config?.vlanTrunk?.length > 0) {
return config.vlanTrunk;
}
return [{ minID: '', maxID: '' }];
},
removeVlanTrunk(trunk) {
removeObject(this.vlanTrunk, trunk);
},
addVlanTrunk() {
if (!this.config.vlanTrunk) {
this.config.vlanTrunk = [];
}
this.vlanTrunk.push({ minID: this.minId, maxID: this.maxId });
this.config.vlanTrunk = this.vlanTrunk;
},
vlanTrunkChange() {
this.config.vlanTrunk = this.vlanTrunk;
},
input(neu) {
if (neu === '') {
this.config.vlan = '';
@@ -318,19 +166,14 @@ export default {
updateBeforeSave() {
this.config.name = this.value.metadata.name;
if (this.isOverlayNetwork) {
this.config.provider = `${ this.value.metadata.name }.${ this.value.metadata.namespace }.ovn`;
delete this.config.bridge;
delete this.config.promiscMode;
delete this.config.vlan;
delete this.config.ipam;
}
if (this.isUntaggedNetwork) {
delete this.config.vlan;
}
this.value.spec.config = JSON.stringify({ ...this.config });
this.value.spec.config = JSON.stringify({
...this.config,
bridge: `${ this.config.bridge }-br`,
});
},
}
};
@@ -344,7 +187,6 @@ export default {
:errors="errors"
:apply-hooks="applyHooks"
@finish="saveNetwork"
@error="e=>errors=e"
>
<NameNsDescription
ref="nd"
@@ -366,83 +208,14 @@ export default {
<LabeledSelect
v-model:value="type"
class="mb-20"
:options="networkTypes"
:options="networkType"
:mode="mode"
:disabled="isEdit"
:label="t('harvester.fields.type')"
required
/>
<LabeledSelect
v-if="isL2VlanNetwork && l2VlanTrunkModeFeatureEnabled"
v-model:value="l2VlanMode"
class="mb-20"
:options="l2VlanModeOptions"
:mode="mode"
:disabled="isEdit"
:label="t('harvester.vlanStatus.vlanConfig.l2VlanMode.label')"
required
/>
<div v-if="isL2VlanTrunkMode && l2VlanTrunkModeFeatureEnabled">
<div
v-for="(trunk, i) in vlanTrunk"
:key="i"
>
<div class="row mt-10">
<div class="col trunk-span">
<LabeledInput
v-model:value.number="trunk.minID"
class="mb-20"
required
placeholder="e.g. 1-4094"
:min="1"
:max="4094"
type="number"
:label="t('harvester.vlanStatus.vlanConfig.vlanTrunk.minId')"
:mode="mode"
@update:value="vlanTrunkChange"
/>
</div>
<div class="col trunk-span">
<LabeledInput
v-model:value.number="trunk.maxID"
class="mb-20"
:max="4094"
:min="1"
placeholder="e.g. 1-4094"
required
type="number"
:label="t('harvester.vlanStatus.vlanConfig.vlanTrunk.maxId')"
:mode="mode"
@update:value="vlanTrunkChange"
/>
</div>
<div class="col remove-btn mb-20">
<button
type="button"
:disabled="isView || vlanTrunk.length <= 1"
:aria-label="t('generic.ariaLabel.remove', {index: i+1})"
role="button"
class="btn role-link"
@click="removeVlanTrunk(trunk)"
>
{{ t('harvester.fields.remove') }}
</button>
</div>
</div>
</div>
<button
v-if="isL2VlanTrunkMode"
type="button"
class="btn btn-sm bg-primary mb-20"
:disabled="isView"
@click="addVlanTrunk"
>
{{ t('harvester.vlanStatus.vlanConfig.vlanTrunk.add') }}
</button>
</div>
<LabeledInput
v-if="isL2VlanAccessMode"
v-if="!isUntaggedNetwork"
v-model:value.number="config.vlan"
class="mb-20"
required
@@ -452,20 +225,25 @@ export default {
:mode="mode"
@update:value="input"
/>
<LabeledSelect
v-if="!isOverlayNetwork"
v-model:value="clusterBridge"
class="mb-20"
:label="t('harvester.network.clusterNetwork.label')"
required
:disabled="isEdit"
:options="clusterNetworkOptions"
:mode="mode"
:placeholder="t('harvester.network.clusterNetwork.selectPlaceholder')"
/>
<div class="row">
<div
class="col span-12"
>
<LabeledSelect
v-model:value="config.bridge"
class="mb-20"
:label="t('harvester.network.clusterNetwork.label')"
required
:options="clusterNetworkOptions"
:mode="mode"
:placeholder="t('harvester.network.clusterNetwork.selectPlaceholder')"
/>
</div>
</div>
</Tab>
<Tab
v-if="isL2VlanAccessMode"
v-if="!isUntaggedNetwork"
name="layer3Network"
:label="t('harvester.network.tabs.layer3Network')"
:weight="98"
@@ -524,12 +302,3 @@ export default {
</Tabbed>
</CruResource>
</template>
<style lang="scss" scoped>
.remove-btn {
align-self: center;
}
.trunk-span{
flex: 5;
}
</style>
@@ -15,7 +15,6 @@ import { HCI } from '../types';
import { sortBy } from '@shell/utils/sort';
import { BACKUP_TYPE } from '../config/types';
import { _EDIT, _CREATE } from '@shell/config/query-params';
import { isBackupTargetSettingEmpty, isBackupTargetSettingUnavailable } from '../utils/setting';
export default {
name: 'CreateVMSchedule',
@@ -94,7 +93,7 @@ export default {
return this.settings.find( (O) => O.id === 'backup-target');
},
isEmptyValue() {
return isBackupTargetSettingEmpty(this.backupTargetResource);
return this.getBackupTargetValueIsEmpty(this.backupTargetResource);
},
canUpdate() {
return this?.backupTargetResource?.canUpdate;
@@ -110,7 +109,7 @@ export default {
!!this.value.spec.maxFailure;
},
isBackupTargetUnAvailable() {
return this.value.spec.vmbackup.type === BACKUP_TYPE.BACKUP && isBackupTargetSettingUnavailable(this.backupTargetResource);
return this.value.spec.vmbackup.type === BACKUP_TYPE.BACKUP && (this.errorMessage || this.isEmptyValue) && this.canUpdate;
},
vmOptions() {
const nsVmList = this.$store.getters['harvester/all'](HCI.VM).filter((vm) => vm.metadata.namespace === this.value.metadata.namespace);
@@ -167,6 +166,20 @@ export default {
onTypeChange(newType) {
this.value.metadata.name = `svm${ newType }-${ this.value.spec.vmbackup.source.name }`;
},
getBackupTargetValueIsEmpty(resource) {
let out = true;
if (resource?.value) {
try {
const valueJson = JSON.parse(resource?.value);
out = !valueJson.type;
} catch (e) {}
}
return out;
},
validateFailure(count) {
if (this.value.spec.retain && count > this.value.spec.retain) {
this.value.spec['maxFailure'] = this.value.spec.retain;
@@ -185,7 +198,7 @@ export default {
:apply-hooks="applyHooks"
:validation-passed="canSave"
@finish="save"
@error="e=>errors=e"
@error="e=>errors = e"
>
<div class="banner">
<Banner
@@ -220,7 +233,7 @@ export default {
:disabled="isEdit || isView"
:mode="mode"
row
@update:value="onTypeChange"
@input="onTypeChange"
/>
</div>
<div class="row mb-30">

Some files were not shown because too many files have changed in this diff Show More