mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-03 20:56:06 +08:00
Compare commits
20
Commits
v1.6.0-rc5
..
v1.0.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
404395c7d9 | ||
|
|
dc2437db99 | ||
|
|
e8c8a54b9f | ||
|
|
f3e4a8afe8 | ||
|
|
e73c62739f | ||
|
|
696af4e04f | ||
|
|
009f384a73 | ||
|
|
37766454bd | ||
|
|
7943913ef7 | ||
|
|
56a0fefd40 | ||
|
|
ff1d5925a8 | ||
|
|
63b5b980b7 | ||
|
|
1f7fd1246f | ||
|
|
34a25e9fc8 | ||
|
|
fb8d81d953 | ||
|
|
ba810a52b2 | ||
|
|
3f4f30ba0b | ||
|
|
b428aaecf8 | ||
|
|
6b07a03451 | ||
|
|
2c756582e3 |
@@ -1,7 +0,0 @@
|
|||||||
addAssignees: author
|
|
||||||
addReviewers: true
|
|
||||||
numberOfReviewers: 0
|
|
||||||
reviewers:
|
|
||||||
- a110605
|
|
||||||
- houhoucoop
|
|
||||||
- NickChungSUSE
|
|
||||||
@@ -2,7 +2,6 @@ pull_request_rules:
|
|||||||
- name: Automatically open backport PR to release-harvester-v1.5
|
- name: Automatically open backport PR to release-harvester-v1.5
|
||||||
conditions:
|
conditions:
|
||||||
- base=main
|
- base=main
|
||||||
- label="require backport 1.5"
|
|
||||||
actions:
|
actions:
|
||||||
backport:
|
backport:
|
||||||
branches:
|
branches:
|
||||||
@@ -12,19 +11,6 @@ pull_request_rules:
|
|||||||
labels:
|
labels:
|
||||||
- "v1.5 backport PR"
|
- "v1.5 backport PR"
|
||||||
|
|
||||||
- name: Automatically open backport PR to release-harvester-v1.0
|
|
||||||
conditions:
|
|
||||||
- base=main
|
|
||||||
- label="require backport 1.0"
|
|
||||||
actions:
|
|
||||||
backport:
|
|
||||||
branches:
|
|
||||||
- "release-harvester-v1.0"
|
|
||||||
assignees:
|
|
||||||
- "{{ author }}"
|
|
||||||
labels:
|
|
||||||
- "v1.0 backport PR"
|
|
||||||
|
|
||||||
- name: Ask to resolve conflict
|
- name: Ask to resolve conflict
|
||||||
conditions:
|
conditions:
|
||||||
- conflict
|
- conflict
|
||||||
|
|||||||
@@ -2,13 +2,18 @@
|
|||||||
### Summary
|
### Summary
|
||||||
|
|
||||||
### PR Checklists
|
### PR Checklists
|
||||||
- Are backend engineers aware of UI changes ?
|
- Do we need to backport this PR change to the [Harvester Dashboard](https://github.com/harvester/dashboard)?
|
||||||
|
- [ ] Yes, the relevant PR is at:
|
||||||
|
- Are backend engineers aware of UI changes?
|
||||||
- [ ] Yes, the backend owner is:
|
- [ ] Yes, the backend owner is:
|
||||||
|
|
||||||
### Related Issue #
|
### Related Issue #
|
||||||
<!-- Define findings related to the feature or bug issue. -->
|
<!-- Define findings related to the feature or bug issue. -->
|
||||||
|
|
||||||
### Test screenshot or video
|
### Test screenshot/video
|
||||||
<!-- Attach screenshot or video of the changes and eventual comparison if you find it necessary -->
|
<!-- Attach screenshot or video of the changes and eventual comparison if you find it necessary -->
|
||||||
|
|
||||||
|
### Extra technical notes summary
|
||||||
|
<!-- Outline technical changes which may pass unobserved or may help to understand the process of solving the issue -->
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
|
||||||
"extends": ["config:recommended"],
|
|
||||||
"baseBranches": [
|
|
||||||
"master",
|
|
||||||
"main",
|
|
||||||
"/^release-harvester-v\\d+\\.\\d+$/"
|
|
||||||
],
|
|
||||||
"automergeMajor": false,
|
|
||||||
"semanticCommits": "enabled",
|
|
||||||
"semanticCommitType": "chore",
|
|
||||||
"semanticCommitScope": "deps",
|
|
||||||
"prHourlyLimit": 12,
|
|
||||||
"timezone": "Asia/Taipei",
|
|
||||||
"schedule": ["after 10am on sunday"],
|
|
||||||
"postUpdateOptions": ["yarnDedupeFewer"],
|
|
||||||
"packageRules": [
|
|
||||||
{
|
|
||||||
"matchUpdateTypes": ["major"],
|
|
||||||
"enabled": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"matchPackagePatterns": ["vue", "vue-router", "vuex"],
|
|
||||||
"matchUpdateTypes": ["major"],
|
|
||||||
"enabled": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"matchPackageNames": ["@rancher/shell"],
|
|
||||||
"enabled": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"matchUpdateTypes": ["minor"],
|
|
||||||
"groupName": "minor dependencies",
|
|
||||||
"labels": ["minor-update"],
|
|
||||||
"reviewers": ["a110605", "houhoucoop"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"matchUpdateTypes": ["patch", "digest"],
|
|
||||||
"automerge": true,
|
|
||||||
"groupName": "patch digest dependencies",
|
|
||||||
"labels": ["patch-update", "automerge"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
name: "[PR Management] Auto Assign Reviewer & Assignee"
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
types: [opened, ready_for_review]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
auto-assign:
|
|
||||||
if: github.event.pull_request.draft == false
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: rancher/gh-issue-mgr/auto-assign-action@main
|
|
||||||
with:
|
|
||||||
configuration-path: .github/auto-assign-config.yaml
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
name: "[PR Management] Add Labels"
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
types: [opened, reopened]
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- 'release-harvester-v*'
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
add-require-backport-label:
|
|
||||||
if: github.event.pull_request.draft == false &&
|
|
||||||
github.event.pull_request.base.ref == 'main'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: ${{ github.base_ref }}
|
|
||||||
|
|
||||||
- name: Fetch release branches and PR labels
|
|
||||||
id: fetch_info
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
repo="${{ github.repository }}"
|
|
||||||
pr_number=${{ github.event.pull_request.number }}
|
|
||||||
|
|
||||||
release_branches=$(gh api "repos/${repo}/branches" --paginate --jq '.[].name' | grep -E '^release-harvester-v[0-9]+\.[0-9]+$' || true)
|
|
||||||
|
|
||||||
if [[ -z "$release_branches" ]]; then
|
|
||||||
echo "should_label=false" >> "$GITHUB_OUTPUT"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
latest_branch=$(echo "$release_branches" | sort -Vr | head -n1)
|
|
||||||
version="${latest_branch#release-harvester-v}"
|
|
||||||
release_tag="v${version}.0"
|
|
||||||
|
|
||||||
tags=$(gh api "repos/${repo}/releases" --paginate --jq '.[].tag_name')
|
|
||||||
if echo "$tags" | grep -Fxq "$release_tag"; then
|
|
||||||
echo "should_label=false" >> "$GITHUB_OUTPUT"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
label="require backport/v${version}"
|
|
||||||
echo "should_label=true" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "backport_label=$label" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
pr_labels=$(gh pr view "$pr_number" --repo "$repo" --json labels --jq '.labels[].name' || echo "")
|
|
||||||
pr_labels_csv=$(echo "$pr_labels" | tr '\n' ',' | sed 's/,$//')
|
|
||||||
echo "pr_labels=$pr_labels_csv" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: Add label if needed
|
|
||||||
if: steps.fetch_info.outputs.should_label == 'true' && !contains(steps.fetch_info.outputs.pr_labels, steps.fetch_info.outputs.backport_label)
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
|
||||||
echo "Adding label: ${{ steps.fetch_info.outputs.backport_label }}"
|
|
||||||
gh pr edit ${{ github.event.pull_request.number }} \
|
|
||||||
--repo ${{ github.repository }} \
|
|
||||||
--add-label "${{ steps.fetch_info.outputs.backport_label }}"
|
|
||||||
|
|
||||||
add-backport-label:
|
|
||||||
if: github.event.pull_request.draft == false &&
|
|
||||||
startsWith(github.event.pull_request.base.ref, 'release-harvester-v')
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Check conditions for backport label
|
|
||||||
id: check
|
|
||||||
run: |
|
|
||||||
IS_MERGIFY=$(echo '${{ github.event.pull_request.user.login }}' | grep -iq 'mergify' && echo true || echo false)
|
|
||||||
TARGET_BRANCH=${{ github.event.pull_request.base.ref }}
|
|
||||||
|
|
||||||
echo "IS_MERGIFY=$IS_MERGIFY" >> $GITHUB_OUTPUT
|
|
||||||
echo "TARGET_BRANCH=$TARGET_BRANCH" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Add label if needed
|
|
||||||
if: steps.check.outputs.IS_MERGIFY == 'true' && startsWith(steps.check.outputs.TARGET_BRANCH, 'release-harvester-v')
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
|
||||||
TARGET_BRANCH="${{ steps.check.outputs.TARGET_BRANCH }}"
|
|
||||||
version="${TARGET_BRANCH#release-harvester-v}"
|
|
||||||
label="backport/v${version}"
|
|
||||||
echo "Adding label $label"
|
|
||||||
gh pr edit ${{ github.event.pull_request.number }} \
|
|
||||||
--repo ${{ github.repository }} \
|
|
||||||
--add-label "$label"
|
|
||||||
@@ -1,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
|
|
||||||
+4
-4
@@ -1,4 +1,4 @@
|
|||||||
name: Build Standalone on PR Merge
|
name: Build Dashboard (Branch)
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -11,16 +11,16 @@ on:
|
|||||||
- main
|
- main
|
||||||
- 'release-harvester-v*'
|
- 'release-harvester-v*'
|
||||||
- '*-dev'
|
- '*-dev'
|
||||||
types:
|
types:
|
||||||
- merged
|
- merged
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-validation:
|
build-validation:
|
||||||
name: Build Test
|
name: Build Test
|
||||||
uses: ./.github/workflows/run-lint.yaml
|
uses: ./.github/workflows/test.yaml
|
||||||
build:
|
build:
|
||||||
name: Build and Upload Package
|
name: Build and Upload Package
|
||||||
uses: ./.github/workflows/build-and-publish-standalone.yaml
|
uses: ./.github/workflows/build-and-upload.yaml
|
||||||
needs:
|
needs:
|
||||||
- build-validation
|
- build-validation
|
||||||
permissions:
|
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
-1
@@ -1,4 +1,4 @@
|
|||||||
name: Build Harvester Dashboard and Publish
|
name: Build Harvester Dashboard and Upload
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
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
|
|
||||||
+9
-10
@@ -1,4 +1,4 @@
|
|||||||
name: Build and Release Extension Charts on Release
|
name: Build and Release Extension Charts
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@@ -22,27 +22,26 @@ jobs:
|
|||||||
- name: Determine release tag
|
- name: Determine release tag
|
||||||
id: determine_tag
|
id: determine_tag
|
||||||
run: |
|
run: |
|
||||||
if [[ "${{ github.event.release.tag_name }}" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
|
if [[ "${{ github.ref_name }}" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
|
||||||
RELEASE_TAG="harvester-${BASH_REMATCH[1]}"
|
RELEASE_TAG="harvester-${BASH_REMATCH[1]}"
|
||||||
echo "${RELEASE_TAG}"
|
echo "${RELEASE_TAG}"
|
||||||
echo "release_tag=${RELEASE_TAG}" >> $GITHUB_OUTPUT
|
echo "release_tag=${RELEASE_TAG}" >> $GITHUB_OUTPUT
|
||||||
else
|
else
|
||||||
echo "Error: invalid tag format." && exit 1
|
echo "Error: invalid tag format." && exit 1
|
||||||
fi
|
fi
|
||||||
check-version:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@v4
|
with:
|
||||||
- name: Check package version
|
fetch-depth: 1
|
||||||
env:
|
|
||||||
TAG_VERSION: ${{ github.event.release.tag_name }}
|
|
||||||
run: ./scripts/check-version.sh $TAG_VERSION
|
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
uses: ./.github/actions/lint
|
||||||
build-extension-charts:
|
build-extension-charts:
|
||||||
needs:
|
needs:
|
||||||
- setup-release-tag
|
- setup-release-tag
|
||||||
- check-version
|
- lint
|
||||||
uses: rancher/dashboard/.github/workflows/build-extension-charts.yml@master
|
uses: rancher/dashboard/.github/workflows/build-extension-charts.yml@master
|
||||||
permissions:
|
permissions:
|
||||||
actions: write
|
actions: write
|
||||||
@@ -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.4.1
|
|
||||||
with:
|
|
||||||
charts_dir: ./charts
|
|
||||||
env:
|
|
||||||
CR_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
|
||||||
CR_SKIP_EXISTING: true
|
|
||||||
@@ -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}}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
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: 0 # Need full history for commit-lint
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
uses: ./.github/actions/lint
|
|
||||||
|
|
||||||
- name: Validate commit messages
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
echo "GITHUB_EVENT_NAME=$GITHUB_EVENT_NAME"
|
|
||||||
echo "GITHUB_BASE_SHA=$GITHUB_BASE_SHA"
|
|
||||||
echo "GITHUB_HEAD_SHA=$GITHUB_HEAD_SHA"
|
|
||||||
echo "GITHUB_BEFORE=$GITHUB_BEFORE"
|
|
||||||
echo "GITHUB_AFTER=$GITHUB_AFTER"
|
|
||||||
|
|
||||||
if [ "$GITHUB_EVENT_NAME" = "pull_request" ] && [ -n "$GITHUB_BASE_SHA" ] && [ -n "$GITHUB_HEAD_SHA" ]; then
|
|
||||||
FROM="$GITHUB_BASE_SHA"
|
|
||||||
TO="$GITHUB_HEAD_SHA"
|
|
||||||
elif [ -n "$GITHUB_BEFORE" ] && [ -n "$GITHUB_AFTER" ]; then
|
|
||||||
FROM="$GITHUB_BEFORE"
|
|
||||||
TO="$GITHUB_AFTER"
|
|
||||||
else
|
|
||||||
echo "No valid commit range found, skipping commitlint."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "FROM=$FROM"
|
|
||||||
echo "TO=$TO"
|
|
||||||
npx commitlint --from "$FROM" --to "$TO" --verbose
|
|
||||||
env:
|
|
||||||
GITHUB_EVENT_NAME: ${{ github.event_name }}
|
|
||||||
GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
|
||||||
GITHUB_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
||||||
GITHUB_BEFORE: ${{ github.event.before }}
|
|
||||||
GITHUB_AFTER: ${{ github.event.after }}
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
npx --no -- commitlint --edit $1
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
yarn lint
|
|
||||||
@@ -1,170 +1,76 @@
|
|||||||
# harvester-ui-extension
|
# harvester-ui-extension
|
||||||
|
|
||||||
The Harvester UI Extension is a Rancher extension that provides the user interface for [Harvester](https://harvesterhci.io) within the [Rancher Dashboard](https://github.com/rancher/dashboard).
|
Rancher Extension used in [rancher/dashboard](https://github.com/rancher/dashboard) for [Harvester](https://harvesterhci.io) Dashboard UI.
|
||||||
|
|
||||||
> **Note:**
|
`
|
||||||
> This extension is available starting from **Rancher 2.10.0**. Ensure your Rancher version is **2.10.0 or later** to access Harvester integration.
|
From Rancher 2.10.0, Harvester integration is provided by this new Harvester UI Extension.
|
||||||
|
`
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
For 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
|
### Standalone Mode
|
||||||
|
|
||||||
Run the extension standalone with hot reload at `https://localhost:8005`.
|
For development, serve with hot reload at https://localhost:8005
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
yarn install
|
yarn install
|
||||||
|
|
||||||
# Start the development server
|
# Use RANCHER_ENV to specify the endpoint for your Harvester API
|
||||||
RANCHER_ENV=harvester API=https://your-harvester-ip yarn dev
|
RANCHER_ENV=harvester API=https://your-harvester-ip yarn dev
|
||||||
|
|
||||||
# Example with specific server version
|
# Use VUE_APP_SERVER_VERSION to specify the Harvester server version
|
||||||
RANCHER_ENV=harvester VUE_APP_SERVER_VERSION=v1.5.0 API=https://192.168.1.123 yarn dev
|
# in case your are running a dev build of Harvester
|
||||||
```
|
RANCHER_ENV=harvester VUE_APP_SERVER_VERSION=your-harvester-server-version API=https://your-harvester-ip yarn dev
|
||||||
|
|
||||||
You may also define environment variables in a `.env` file:
|
# or put your env variables into a .env file
|
||||||
|
|
||||||
```env
|
# Example
|
||||||
RANCHER_ENV=harvester
|
RANCHER_ENV=harvester VUE_APP_SERVER_VERSION=v1.4.0 API=https://192.168.1.123 yarn dev
|
||||||
VUE_APP_SERVER_VERSION=v1.5.0
|
|
||||||
API=https://192.168.1.123
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Rancher Integration Mode
|
### Rancher Integration Mode
|
||||||
|
|
||||||
To run as a Rancher extension, follow the [Rancher UI Extension Guide](https://extensions.rancher.io/extensions/next/extensions-getting-started#running-the-app).
|
In this mode, you need to have Rancher and run Harvester UI Extension as Rancher extension, see more detail in [Rancher UI extension](https://extensions.rancher.io/extensions/next/extensions-getting-started#running-the-app).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
API=https://your-rancher-ip yarn dev
|
API=https://your-rancher-ip yarn dev
|
||||||
|
|
||||||
|
# Example
|
||||||
|
API=https://192.168.1.123 yarn dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## Commit Message Guidelines
|
|
||||||
|
|
||||||
This project uses [commit-lint](https://commitlint.js.org/) with [Conventional Commits](https://www.conventionalcommits.org/) to ensure consistent and meaningful commit messages.
|
|
||||||
|
|
||||||
### Commit Message Format
|
|
||||||
|
|
||||||
All commit messages must follow the conventional commit format:
|
|
||||||
|
|
||||||
```
|
|
||||||
<type>[optional scope]: <description>
|
|
||||||
|
|
||||||
[optional body]
|
|
||||||
|
|
||||||
[optional footer(s)]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Supported Types
|
|
||||||
|
|
||||||
- **feat**: New features
|
|
||||||
- **fix**: Bug fixes
|
|
||||||
- **docs**: Documentation changes
|
|
||||||
- **style**: Code style changes (formatting, missing semicolons, etc.)
|
|
||||||
- **refactor**: Code refactoring
|
|
||||||
- **perf**: Performance improvements
|
|
||||||
- **test**: Adding or updating tests
|
|
||||||
- **build**: Build system or external dependencies
|
|
||||||
- **ci**: CI/CD changes
|
|
||||||
- **chore**: Other changes that don't modify src or test files
|
|
||||||
- **revert**: Reverts a previous commit
|
|
||||||
- **wip**: Work in progress
|
|
||||||
- **deps**: Dependency updates
|
|
||||||
- **security**: Security fixes
|
|
||||||
|
|
||||||
### Examples
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Feature
|
|
||||||
git commit -m "feat: add new virtual machine creation wizard"
|
|
||||||
|
|
||||||
# Bug fix
|
|
||||||
git commit -m "fix: resolve memory leak in VM console"
|
|
||||||
|
|
||||||
# Documentation
|
|
||||||
git commit -m "docs: update installation instructions"
|
|
||||||
|
|
||||||
# Breaking change
|
|
||||||
git commit -m "feat!: change API endpoint structure
|
|
||||||
|
|
||||||
BREAKING CHANGE: The /api/v1/vms endpoint has been replaced with /api/v2/vms"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Git Hooks
|
|
||||||
|
|
||||||
The project uses [Husky](https://typicode.github.io/husky/) to automatically validate commit messages and run linting before commits:
|
|
||||||
|
|
||||||
- **pre-commit**: Runs ESLint to ensure code quality
|
|
||||||
- **commit-msg**: Validates commit message format using commit-lint
|
|
||||||
|
|
||||||
These hooks are automatically installed when you run `yarn install`.
|
|
||||||
|
|
||||||
### Manual Validation
|
|
||||||
|
|
||||||
You can manually validate commit messages:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Validate the last commit
|
|
||||||
yarn commitlint
|
|
||||||
|
|
||||||
# Validate a specific commit
|
|
||||||
npx commitlint --from <commit-hash>
|
|
||||||
|
|
||||||
# Validate a range of commits
|
|
||||||
npx commitlint --from <start-hash> --to <end-hash>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Branch Structure
|
|
||||||
|
|
||||||
- **`main`** – Main development branch
|
|
||||||
- **`release-harvester-vX.Y`** – Stable release branches per version series
|
|
||||||
- **`vX.Y-head`** – Testing branches for ongoing changes to extension builds in each release series
|
|
||||||
|
|
||||||
> **Note:**
|
|
||||||
> The `vX.Y-head` branches are auto-generated and kept in sync with release branches. Use these for testing the latest changes in each version series.
|
|
||||||
|
|
||||||
## Testing Guidelines
|
|
||||||
|
|
||||||
### UI Extension Testing
|
|
||||||
|
|
||||||
To validate changes in a release series, switch to the appropriate `vX.Y-head` branch. For main branch testing, use `main-head`.
|
|
||||||
|
|
||||||
- Examples:
|
|
||||||
- Test `1.0.x` series → `v1.0-head`
|
|
||||||
- Test `1.5.x` series → `v1.5-head`
|
|
||||||
- Test `1.6.x` series → `v1.6-head`
|
|
||||||
|
|
||||||
**Steps:**
|
|
||||||
1. Navigate to **Rancher UI** → **Local** → **App** → **Repositories**
|
|
||||||
2. Refresh the Harvester repository using the target `vX.Y-head` branch
|
|
||||||
3. Go to the **Extensions** page and install the desired version
|
|
||||||
|
|
||||||
### Standalone Mode Testing
|
|
||||||
|
|
||||||
To test the standalone UI, configure Harvester to load the UI from an external source.
|
|
||||||
|
|
||||||
- Examples of `ui-index`:
|
|
||||||
- Main branch → `https://releases.rancher.com/harvester-ui/dashboard/latest/index.html`
|
|
||||||
- Release series `1.5.x` → `https://releases.rancher.com/harvester-ui/dashboard/release-harvester-v1.5/index.html`
|
|
||||||
|
|
||||||
**Steps:**
|
|
||||||
1. Go to **Harvester UI** → **Advanced** → **Settings** → **UI**
|
|
||||||
2. Set **ui-source** to `External`
|
|
||||||
3. Set **ui-index** to the desired URL
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
If you want to contribute, start by reading this document, then visit our [Getting Started guide](https://extensions.rancher.io/extensions/next/extensions-getting-started) to learn how to develop and submit changes.
|
For developers, after reading through the introduction on this page, head over to our [Getting Started](https://extensions.rancher.io/extensions/next/extensions-getting-started) guide to learn more.
|
||||||
|
|
||||||
## License
|
License
|
||||||
|
=======
|
||||||
Copyright (c) 2014-2025 [SUSE, LLC.](https://www.suse.com/)
|
Copyright (c) 2014-2023 [Rancher Labs, Inc.](http://rancher.com)
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -1,43 +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
|
|
||||||
]
|
|
||||||
],
|
|
||||||
'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'),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
+9
-14
@@ -1,26 +1,26 @@
|
|||||||
{
|
{
|
||||||
"name": "harvester-ui-extension",
|
"name": "harvester-ui-extension",
|
||||||
"version": "1.6.0-rc5",
|
"version": "1.0.5",
|
||||||
"private": false,
|
"private": false,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/plugin-transform-class-static-block": "7.26.0",
|
"@rancher/shell": "3.0.1-rc.4",
|
||||||
"@rancher/shell": "3.0.5-rc.7",
|
|
||||||
"cache-loader": "^4.1.0",
|
"cache-loader": "^4.1.0",
|
||||||
"color": "4.2.3",
|
"color": "4.2.3",
|
||||||
"ip": "2.0.1",
|
"ip": "2.0.1",
|
||||||
"node-polyfill-webpack-plugin": "^3.0.0",
|
"node-polyfill-webpack-plugin": "^3.0.0",
|
||||||
"vue-draggable-next": "^2.2.1",
|
"vue-draggable-next": "^2.2.1",
|
||||||
"yaml": "^2.5.1"
|
"yaml": "^2.5.1",
|
||||||
|
"@babel/plugin-transform-class-static-block": "7.26.0"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@types/node": "~20.10.0",
|
"@types/node": "~20.10.0",
|
||||||
"cronstrue": "2.50.0",
|
"cronstrue": "2.50.0",
|
||||||
"d3-color": "3.1.0",
|
"d3-color": "3.1.0",
|
||||||
"ejs": "3.1.10",
|
"ejs": "3.1.9",
|
||||||
"follow-redirects": "1.15.6",
|
"follow-redirects": "1.15.2",
|
||||||
"glob": "7.2.3",
|
"glob": "7.2.3",
|
||||||
"glob-parent": "6.0.2",
|
"glob-parent": "6.0.2",
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
@@ -41,15 +41,10 @@
|
|||||||
"build-pkg": "./node_modules/@rancher/shell/scripts/build-pkg.sh",
|
"build-pkg": "./node_modules/@rancher/shell/scripts/build-pkg.sh",
|
||||||
"serve-pkgs": "./node_modules/@rancher/shell/scripts/serve-pkgs",
|
"serve-pkgs": "./node_modules/@rancher/shell/scripts/serve-pkgs",
|
||||||
"publish-pkgs": "./node_modules/@rancher/shell/scripts/extension/publish",
|
"publish-pkgs": "./node_modules/@rancher/shell/scripts/extension/publish",
|
||||||
"parse-tag-name": "./node_modules/@rancher/shell/scripts/extension/parse-tag-name",
|
"parse-tag-name": "./node_modules/@rancher/shell/scripts/extension/parse-tag-name"
|
||||||
"commitlint": "commitlint --edit",
|
|
||||||
"prepare": "husky"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^19.8.1",
|
|
||||||
"@commitlint/config-conventional": "^19.8.1",
|
|
||||||
"eslint-plugin-node": "^11.1.0",
|
"eslint-plugin-node": "^11.1.0",
|
||||||
"eslint-plugin-promise": "^7.1.0",
|
"eslint-plugin-promise": "^7.1.0"
|
||||||
"husky": "^9.1.7"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,10 +5,8 @@ import { Checkbox } from '@components/Form/Checkbox';
|
|||||||
import ModalWithCard from '@shell/components/ModalWithCard';
|
import ModalWithCard from '@shell/components/ModalWithCard';
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
|
||||||
import { HCI } from '../types';
|
import { HCI } from '../types';
|
||||||
import UpgradeInfo from './UpgradeInfo';
|
import UpgradeInfo from './UpgradeInfo';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterUpgrade',
|
name: 'HarvesterUpgrade',
|
||||||
|
|
||||||
@@ -30,15 +28,14 @@ export default {
|
|||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
upgrade: [],
|
upgrade: [],
|
||||||
upgradeMessage: [],
|
upgradeMessage: [],
|
||||||
errors: '',
|
errors: '',
|
||||||
selectMode: true,
|
selectMode: true,
|
||||||
version: '',
|
version: '',
|
||||||
enableLogging: true,
|
enableLogging: true,
|
||||||
skipSingleReplicaDetachedVol: false,
|
readyReleaseNote: false,
|
||||||
readyReleaseNote: false,
|
isOpen: false
|
||||||
isOpen: false
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -71,10 +68,6 @@ export default {
|
|||||||
return this.$store.getters['harvester/schemaFor'](HCI.UPGRADE_LOG);
|
return this.$store.getters['harvester/schemaFor'](HCI.UPGRADE_LOG);
|
||||||
},
|
},
|
||||||
|
|
||||||
skipSingleReplicaDetachedVolFeatureEnabled() {
|
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('skipSingleReplicaDetachedVol');
|
|
||||||
},
|
|
||||||
|
|
||||||
releaseLink() {
|
releaseLink() {
|
||||||
return `https://github.com/harvester/harvester/releases/tag/${ this.version }`;
|
return `https://github.com/harvester/harvester/releases/tag/${ this.version }`;
|
||||||
}
|
}
|
||||||
@@ -111,11 +104,6 @@ export default {
|
|||||||
spec: { version: this.version }
|
spec: { version: this.version }
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.skipSingleReplicaDetachedVolFeatureEnabled && this.skipSingleReplicaDetachedVol) {
|
|
||||||
upgradeValue.metadata.annotations =
|
|
||||||
{ [HCI_ANNOTATIONS.SKIP_SINGLE_REPLICA_DETACHED_VOL]: JSON.stringify(this.skipSingleReplicaDetachedVol) };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.canEnableLogging) {
|
if (this.canEnableLogging) {
|
||||||
upgradeValue.spec.logEnabled = this.enableLogging;
|
upgradeValue.spec.logEnabled = this.enableLogging;
|
||||||
}
|
}
|
||||||
@@ -202,21 +190,6 @@ export default {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="skipSingleReplicaDetachedVolFeatureEnabled"
|
|
||||||
class="mb-5"
|
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
v-model:value="skipSingleReplicaDetachedVol"
|
|
||||||
class="check"
|
|
||||||
type="checkbox"
|
|
||||||
:label="t('harvester.upgradePage.skipSingleReplicaDetachedVol')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<hr
|
|
||||||
v-if="version"
|
|
||||||
class="divider"
|
|
||||||
/>
|
|
||||||
<div v-if="version">
|
<div v-if="version">
|
||||||
<p
|
<p
|
||||||
v-clean-html="t('harvester.upgradePage.releaseTip', {url: releaseLink}, true)"
|
v-clean-html="t('harvester.upgradePage.releaseTip', {url: releaseLink}, true)"
|
||||||
|
|||||||
@@ -190,7 +190,6 @@ export default {
|
|||||||
v-clean-tooltip="{
|
v-clean-tooltip="{
|
||||||
placement: 'bottom-left',
|
placement: 'bottom-left',
|
||||||
}"
|
}"
|
||||||
popper-class="upgrade-header-dropdown"
|
|
||||||
class="hand"
|
class="hand"
|
||||||
>
|
>
|
||||||
<slot name="button-content">
|
<slot name="button-content">
|
||||||
@@ -273,7 +272,7 @@ export default {
|
|||||||
|
|
||||||
<p
|
<p
|
||||||
v-if="overallMessage"
|
v-if="overallMessage"
|
||||||
class="text-error mb-20"
|
class="text-warning mb-20"
|
||||||
>
|
>
|
||||||
{{ overallMessage }}
|
{{ overallMessage }}
|
||||||
</p>
|
</p>
|
||||||
@@ -301,14 +300,14 @@ export default {
|
|||||||
|
|
||||||
<ProgressBarList
|
<ProgressBarList
|
||||||
:title="t('harvester.upgradePage.upgradeNode')"
|
:title="t('harvester.upgradePage.upgradeNode')"
|
||||||
:percent="nodesPercent"
|
:precent="nodesPercent"
|
||||||
:list="nodesStatus"
|
:list="nodesStatus"
|
||||||
/>
|
/>
|
||||||
<p class="bordered-section"></p>
|
<p class="bordered-section"></p>
|
||||||
|
|
||||||
<ProgressBarList
|
<ProgressBarList
|
||||||
:title="t('harvester.upgradePage.upgradeSysService')"
|
:title="t('harvester.upgradePage.upgradeSysService')"
|
||||||
:percent="sysServiceTotal"
|
:precent="sysServiceTotal"
|
||||||
:list="sysServiceUpgradeMessage"
|
:list="sysServiceUpgradeMessage"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -339,12 +338,6 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.upgrade-header-dropdown .v-popper__arrow-container {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
a {
|
a {
|
||||||
float: right;
|
float: right;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export default {
|
|||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
|
|
||||||
percent: {
|
precent: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0
|
default: 0
|
||||||
},
|
},
|
||||||
@@ -39,18 +39,16 @@ export default {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="bar-list">
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<Collapse v-model:open="open">
|
<Collapse v-model:open="open">
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="total-bar">
|
<div class="total-bar">
|
||||||
<span class="bar">
|
<span class="bar"><PercentageBar
|
||||||
<PercentageBar
|
:model-value="precent"
|
||||||
:model-value="percent"
|
preferred-direction="MORE"
|
||||||
preferred-direction="MORE"
|
/></span>
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<span
|
<span
|
||||||
class="on-off"
|
class="on-off"
|
||||||
@click="handleSwitch"
|
@click="handleSwitch"
|
||||||
@@ -58,26 +56,28 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="custom-content">
|
<template>
|
||||||
<div
|
<div class="custom-content">
|
||||||
v-for="(item, i) in list"
|
<div
|
||||||
:key="i"
|
v-for="(item, i) in list"
|
||||||
>
|
:key="i"
|
||||||
<p>
|
>
|
||||||
{{ item.name }} <span
|
<p>
|
||||||
class="status"
|
{{ item.name }} <span
|
||||||
:class="{ [item.state]: true }"
|
class="status"
|
||||||
>{{ item.state }}</span>
|
:class="{ [item.state]: true }"
|
||||||
</p>
|
>{{ item.state }}</span>
|
||||||
<PercentageBar
|
</p>
|
||||||
:model-value="item.percent"
|
<PercentageBar
|
||||||
preferred-direction="MORE"
|
:model-value="item.percent"
|
||||||
/>
|
preferred-direction="MORE"
|
||||||
<p class="warning">
|
/>
|
||||||
{{ item.message }}
|
<p class="warning">
|
||||||
</p>
|
{{ item.message }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -93,7 +93,7 @@ export default {
|
|||||||
.total-bar {
|
.total-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
> .bar {
|
.bar {
|
||||||
width: 85%;
|
width: 85%;
|
||||||
}
|
}
|
||||||
.on-off {
|
.on-off {
|
||||||
@@ -102,22 +102,25 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.custom-content {
|
.custom-content {
|
||||||
margin-bottom: 14px;
|
.item {
|
||||||
p {
|
margin-bottom: 14px;
|
||||||
margin-bottom: 4px;
|
p {
|
||||||
}
|
margin-bottom: 4px;
|
||||||
.status {
|
}
|
||||||
float: right;
|
.status {
|
||||||
}
|
float: right;
|
||||||
.Succeeded, .Upgrading, .Pending {
|
}
|
||||||
color: var(--success);
|
.Succeeded, .Upgrading, .Pending {
|
||||||
}
|
color: var(--success);
|
||||||
.failed {
|
}
|
||||||
color: var(--error)
|
.failed {
|
||||||
}
|
color: var(--error)
|
||||||
.warning {
|
}
|
||||||
color: var(--error);
|
.warning {
|
||||||
|
color: var(--error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import ActionMenu from '@shell/components/ActionMenuShell';
|
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import AsyncButton from '@shell/components/AsyncButton';
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
import { HCI_ALLOWED_SETTINGS, HCI_SINGLE_CLUSTER_ALLOWED_SETTING, HCI_SETTING } from '../config/settings';
|
import { HCI_ALLOWED_SETTINGS, HCI_SETTING } from '../config/settings';
|
||||||
import { DOC } from '../config/doc-links';
|
|
||||||
import { docLink } from '../utils/feature-flags';
|
|
||||||
|
|
||||||
const CATEGORY = {
|
const CATEGORY = {
|
||||||
ui: [
|
ui: [
|
||||||
@@ -22,7 +19,6 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
AsyncButton,
|
AsyncButton,
|
||||||
Banner,
|
Banner,
|
||||||
ActionMenu
|
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
@@ -34,23 +30,15 @@ export default {
|
|||||||
category: {
|
category: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
|
||||||
|
|
||||||
searchQuery: {
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
const categorySettings = this.filterCategorySettings();
|
const categorySettings = this.filterCategorySettings();
|
||||||
const filteredSettings = this.filterSearchSettings(categorySettings, this.searchQuery);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
HCI_SETTING,
|
HCI_SETTING,
|
||||||
categorySettings,
|
categorySettings,
|
||||||
filteredSettings,
|
|
||||||
originalHideMap: this.createHideMap(categorySettings)
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -60,85 +48,12 @@ export default {
|
|||||||
settings: {
|
settings: {
|
||||||
deep: true,
|
deep: true,
|
||||||
handler() {
|
handler() {
|
||||||
this.categorySettings = this.filterCategorySettings();
|
this['categorySettings'] = this.filterCategorySettings();
|
||||||
this.filteredSettings = this.filterSearchSettings(this.categorySettings, this.searchQuery);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
searchQuery: {
|
|
||||||
immediate: true,
|
|
||||||
handler(newQuery) {
|
|
||||||
const filtered = this.filterSearchSettings(this.categorySettings, newQuery);
|
|
||||||
|
|
||||||
this.filteredSettings = newQuery ? this.openJsonSettings(filtered) : filtered.map((s) => ({ ...s, hide: this.originalHideMap[s.id] ?? false }));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
createHideMap(settings = []) {
|
|
||||||
const map = settings.reduce((acc, s) => {
|
|
||||||
acc[s.id] = s.hide ?? false;
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, {} );
|
|
||||||
|
|
||||||
return map;
|
|
||||||
},
|
|
||||||
filterSearchSettings(settings, searchKey) {
|
|
||||||
if (!searchKey) {
|
|
||||||
return this.filterCategorySettings();
|
|
||||||
}
|
|
||||||
const searchQuery = searchKey.toLowerCase();
|
|
||||||
|
|
||||||
return settings.filter((setting) => {
|
|
||||||
const id = setting.id?.toLowerCase() || '';
|
|
||||||
|
|
||||||
// filter by id
|
|
||||||
if (id.includes(searchQuery) ) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
let description = this.t(setting.description, this.getDocLinkParams(setting) || {}, true)?.toLowerCase() || '';
|
|
||||||
|
|
||||||
// remove <a> tags from description
|
|
||||||
if (description.includes('<a')) {
|
|
||||||
description = description.replace(/<a[^>]*>(.*?)<\/a>/g, '$1');
|
|
||||||
}
|
|
||||||
// filter by description
|
|
||||||
if (description.includes(searchQuery)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// filter by customized value
|
|
||||||
if (setting.customized === true && setting.data?.value) {
|
|
||||||
const value = setting.data.value?.toLowerCase() || '';
|
|
||||||
|
|
||||||
return value.includes(searchQuery);
|
|
||||||
}
|
|
||||||
|
|
||||||
// filter by json value
|
|
||||||
if (setting.kind === 'json' && setting.json) {
|
|
||||||
try {
|
|
||||||
const json = JSON.parse(setting.json);
|
|
||||||
const jsonString = JSON.stringify(json).toLowerCase();
|
|
||||||
|
|
||||||
return jsonString.includes(searchQuery);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(`${ setting.id }: wrong format`, e); // eslint-disable-line no-console
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// filter by default value
|
|
||||||
if (setting.data?.default) {
|
|
||||||
return setting.data?.default.includes(searchQuery);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
filterCategorySettings() {
|
filterCategorySettings() {
|
||||||
return this.settings.filter((s) => {
|
return this.settings.filter((s) => {
|
||||||
if (!this.getFeatureEnabled(s.featureFlag)) {
|
if (!this.getFeatureEnabled(s.featureFlag)) {
|
||||||
@@ -159,21 +74,25 @@ export default {
|
|||||||
return id ? this.$store.getters['harvester-common/getFeatureEnabled'](id) : true;
|
return id ? this.$store.getters['harvester-common/getFeatureEnabled'](id) : true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
showActionMenu(e, setting) {
|
||||||
|
const actionElement = e.srcElement;
|
||||||
|
|
||||||
|
this.$store.commit(`action-menu/show`, {
|
||||||
|
resources: setting.data,
|
||||||
|
elem: actionElement
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
getSettingOption(id) {
|
getSettingOption(id) {
|
||||||
return HCI_ALLOWED_SETTINGS.find((setting) => setting.id === id);
|
return HCI_ALLOWED_SETTINGS.find((setting) => setting.id === id);
|
||||||
},
|
},
|
||||||
|
|
||||||
openJsonSettings(settings) {
|
|
||||||
return settings.map((s) => s.hide ? { ...s, hide: false } : s);
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleHide(s) {
|
toggleHide(s) {
|
||||||
const setting = this.filteredSettings.find((setting) => setting.id === s.id);
|
this.categorySettings.find((setting) => {
|
||||||
|
if (setting.id === s.id) {
|
||||||
if (setting) {
|
setting.hide = !setting.hide;
|
||||||
setting.hide = !setting.hide;
|
}
|
||||||
this.originalHideMap[setting.id] = setting.hide;
|
});
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async testConnect(buttonDone, value) {
|
async testConnect(buttonDone, value) {
|
||||||
@@ -199,19 +118,6 @@ export default {
|
|||||||
}
|
}
|
||||||
buttonDone(false);
|
buttonDone(false);
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
getDocLinkParams(setting) {
|
|
||||||
const settingConfig = HCI_ALLOWED_SETTINGS[setting.id] || HCI_SINGLE_CLUSTER_ALLOWED_SETTING[setting.id];
|
|
||||||
|
|
||||||
if (settingConfig?.docPath) {
|
|
||||||
const version = this.$store.getters['harvester-common/getServerVersion']();
|
|
||||||
const url = docLink(DOC[settingConfig.docPath], version);
|
|
||||||
|
|
||||||
return { url };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -220,7 +126,7 @@ export default {
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
v-for="(setting, i) in filteredSettings"
|
v-for="(setting, i) in categorySettings"
|
||||||
:key="i"
|
:key="i"
|
||||||
class="advanced-setting mb-20"
|
class="advanced-setting mb-20"
|
||||||
>
|
>
|
||||||
@@ -242,7 +148,7 @@ export default {
|
|||||||
Experimental
|
Experimental
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<h2 v-clean-html="t(setting.description, getDocLinkParams(setting) || {}, true)">
|
<h2 v-clean-html="t(setting.description, {}, true)">
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -250,12 +156,15 @@ export default {
|
|||||||
:id="setting.id"
|
:id="setting.id"
|
||||||
class="action"
|
class="action"
|
||||||
>
|
>
|
||||||
<ActionMenu
|
<button
|
||||||
:resource="setting.data"
|
aria-haspopup="true"
|
||||||
:button-aria-label="t('advancedSettings.edit.label')"
|
aria-expanded="false"
|
||||||
data-testid="action-button"
|
type="button"
|
||||||
button-role="tertiary"
|
class="btn btn-sm role-multi-action actions"
|
||||||
/>
|
@click="showActionMenu($event, setting)"
|
||||||
|
>
|
||||||
|
<i class="icon icon-actions" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div value>
|
<div value>
|
||||||
@@ -312,12 +221,6 @@ export default {
|
|||||||
{{ setting.data.errMessage }}
|
{{ setting.data.errMessage }}
|
||||||
</Banner>
|
</Banner>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
v-if="filteredSettings.length === 0"
|
|
||||||
class="advanced-setting mb-20 no-search-match"
|
|
||||||
>
|
|
||||||
<p> {{ t('harvester.setting.noSearchMatch') }} </p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -368,8 +271,4 @@ export default {
|
|||||||
padding: 2px 10px;
|
padding: 2px 10px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-search-match {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export default {
|
|||||||
{{ t('harvester.upgradePage.upgradeInfo.tip') }}
|
{{ t('harvester.upgradePage.upgradeInfo.tip') }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p class="mb-5">
|
||||||
{{ t('harvester.upgradePage.upgradeInfo.moreNotes') }} <a
|
{{ t('harvester.upgradePage.upgradeInfo.moreNotes') }} <a
|
||||||
:href="releaseVersion"
|
:href="releaseVersion"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|||||||
@@ -174,6 +174,10 @@ export default {
|
|||||||
return driver === LONGHORN_DRIVER || this.allowEmptySnapshotClassNameFeatureEnabled;
|
return driver === LONGHORN_DRIVER || this.allowEmptySnapshotClassNameFeatureEnabled;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isBackupVolumeSnapshotClassNameSelectable(driver) {
|
||||||
|
return driver === LONGHORN_DRIVER;
|
||||||
|
},
|
||||||
|
|
||||||
add() {
|
add() {
|
||||||
this.configArr.push({
|
this.configArr.push({
|
||||||
key: '',
|
key: '',
|
||||||
@@ -243,6 +247,7 @@ export default {
|
|||||||
:options="getVolumeSnapshotOptions(driver.key)"
|
:options="getVolumeSnapshotOptions(driver.key)"
|
||||||
:required="isBackupVolumeSnapshotRequired(driver.key)"
|
:required="isBackupVolumeSnapshotRequired(driver.key)"
|
||||||
:label="t('harvester.setting.csiDriverConfig.backupVolumeSnapshotClassName')"
|
:label="t('harvester.setting.csiDriverConfig.backupVolumeSnapshotClassName')"
|
||||||
|
@selectable="isBackupVolumeSnapshotClassNameSelectable(driver.key)"
|
||||||
@keydown.native.enter.prevent="()=>{}"
|
@keydown.native.enter.prevent="()=>{}"
|
||||||
@update:value="update"
|
@update:value="update"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,294 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { _EDIT } from '@shell/config/query-params';
|
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
||||||
import InfoBox from '@shell/components/InfoBox';
|
|
||||||
import { Banner } from '@components/Banner';
|
|
||||||
import { allHash } from '@shell/utils/promise';
|
|
||||||
import { CSI_DRIVER } from '../../types';
|
|
||||||
import { LONGHORN_DRIVER } from '@shell/config/types';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'CSIOnlineExpandValidation',
|
|
||||||
|
|
||||||
components: {
|
|
||||||
Banner,
|
|
||||||
InfoBox,
|
|
||||||
LabeledSelect,
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
|
||||||
mode: {
|
|
||||||
type: String,
|
|
||||||
default: _EDIT,
|
|
||||||
},
|
|
||||||
value: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
registerBeforeHook: {
|
|
||||||
type: Function,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await allHash({ csiDrivers: this.$store.dispatch(`${ inStore }/findAll`, { type: CSI_DRIVER }) });
|
|
||||||
this.fetchError = null;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch CSI drivers:', error); // eslint-disable-line no-console
|
|
||||||
this.fetchError = this.t(
|
|
||||||
'harvester.setting.csiOnlineExpandValidation.failedToLoadDrivers',
|
|
||||||
{ error: error.message || error },
|
|
||||||
true
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
configArr: [],
|
|
||||||
parseError: null,
|
|
||||||
fetchError: null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
const initValue = this.value.value || this.value.default || '{}';
|
|
||||||
|
|
||||||
this.configArr = this.parseValue(initValue);
|
|
||||||
this.registerBeforeHook?.(this.willSave, 'willSave');
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
allErrors() {
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
if (this.fetchError) {
|
|
||||||
errors.push(this.fetchError);
|
|
||||||
}
|
|
||||||
if (this.parseError) {
|
|
||||||
errors.push(this.parseError);
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
},
|
|
||||||
|
|
||||||
csiDrivers() {
|
|
||||||
if (this.fetchError) return [];
|
|
||||||
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
return this.$store.getters[`${ inStore }/all`](CSI_DRIVER) || [];
|
|
||||||
},
|
|
||||||
|
|
||||||
provisioners() {
|
|
||||||
const usedKeys = this.configArr.map(({ key }) => key);
|
|
||||||
|
|
||||||
return this.csiDrivers
|
|
||||||
.filter(({ name }) => !usedKeys.includes(name))
|
|
||||||
.map(({ name }) => name);
|
|
||||||
},
|
|
||||||
|
|
||||||
provisionerValue() {
|
|
||||||
return [
|
|
||||||
{ label: 'True', value: true },
|
|
||||||
{ label: 'False', value: false },
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
disableAdd() {
|
|
||||||
return this.parseError || this.fetchError || this.configArr.length >= this.csiDrivers.length;
|
|
||||||
},
|
|
||||||
|
|
||||||
disableConfigEditing() {
|
|
||||||
return this.parseError || this.fetchError;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
'value.value'(newVal, oldVal) {
|
|
||||||
if (newVal !== oldVal) {
|
|
||||||
this.configArr = this.parseValue(newVal || '{}');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
_convertToBoolean(value) {
|
|
||||||
if (typeof value === 'boolean') return value;
|
|
||||||
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
const lowerCaseValue = value.toLowerCase();
|
|
||||||
|
|
||||||
if (lowerCaseValue === 'true') return true;
|
|
||||||
if (lowerCaseValue === 'false') return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false; // default to false for any other string or non-boolean type
|
|
||||||
},
|
|
||||||
parseValue(raw) {
|
|
||||||
try {
|
|
||||||
const json = JSON.parse(raw);
|
|
||||||
|
|
||||||
this.parseError = null;
|
|
||||||
|
|
||||||
return Object.entries(json).map(([key, value]) => ({
|
|
||||||
key,
|
|
||||||
value: this._convertToBoolean(value),
|
|
||||||
}));
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[CSIOnlineExpandValidation] JSON Parsing Error:', raw, e); // eslint-disable-line no-console
|
|
||||||
this.parseError = this.t(
|
|
||||||
'harvester.setting.csiOnlineExpandValidation.invalidJsonFormat',
|
|
||||||
{ error: e.message },
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
stringifyConfig() {
|
|
||||||
const obj = {};
|
|
||||||
|
|
||||||
this.configArr.forEach(({ key, value }) => {
|
|
||||||
obj[key] = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.configArr.length ? JSON.stringify(obj) : '';
|
|
||||||
},
|
|
||||||
|
|
||||||
update() {
|
|
||||||
this.value.value = this.stringifyConfig();
|
|
||||||
},
|
|
||||||
|
|
||||||
willSave() {
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
this.configArr.forEach(({ key }) => {
|
|
||||||
if (!key) {
|
|
||||||
errors.push(
|
|
||||||
this.t('validation.required', { key: this.t('harvester.setting.csiOnlineExpandValidation.provisioner') }, true)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.value.value = this.stringifyConfig();
|
|
||||||
|
|
||||||
return errors.length ? Promise.reject(errors) : Promise.resolve();
|
|
||||||
},
|
|
||||||
|
|
||||||
useDefault() {
|
|
||||||
this.configArr = this.parseValue(this.value.default || '{}');
|
|
||||||
this.update();
|
|
||||||
},
|
|
||||||
|
|
||||||
disableEdit(driverKey) {
|
|
||||||
return this.fetchError || driverKey === LONGHORN_DRIVER;
|
|
||||||
},
|
|
||||||
|
|
||||||
add() {
|
|
||||||
if (this.disableConfigEditing) return;
|
|
||||||
|
|
||||||
this.configArr.push({ key: '', value: true });
|
|
||||||
},
|
|
||||||
|
|
||||||
remove(index) {
|
|
||||||
if (this.disableConfigEditing) return;
|
|
||||||
|
|
||||||
this.configArr.splice(index, 1);
|
|
||||||
this.update();
|
|
||||||
},
|
|
||||||
|
|
||||||
onValueChange(idx, newVal) {
|
|
||||||
if (this.disableConfigEditing) return;
|
|
||||||
|
|
||||||
const val = newVal === 'true' ? true : newVal === 'false' ? false : newVal;
|
|
||||||
|
|
||||||
this.configArr[idx].value = val;
|
|
||||||
this.update();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<Banner
|
|
||||||
v-for="(errorMsg, index) in allErrors"
|
|
||||||
:key="index"
|
|
||||||
color="error"
|
|
||||||
>
|
|
||||||
{{ errorMsg }}
|
|
||||||
</Banner>
|
|
||||||
<InfoBox
|
|
||||||
v-for="(driver, idx) in configArr"
|
|
||||||
:key="idx"
|
|
||||||
class="box"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="role-link btn btn-sm remove"
|
|
||||||
type="button"
|
|
||||||
:disabled="disableEdit(driver.key)"
|
|
||||||
@click="remove(idx)"
|
|
||||||
>
|
|
||||||
<i class="icon icon-x" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col span-4">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="driver.key"
|
|
||||||
label-key="harvester.setting.csiOnlineExpandValidation.provisioner"
|
|
||||||
required
|
|
||||||
searchable
|
|
||||||
:mode="mode"
|
|
||||||
:disabled="disableEdit(driver.key)"
|
|
||||||
:options="provisioners"
|
|
||||||
@update:value="update"
|
|
||||||
@keydown.native.enter.prevent
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col span-4">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="driver.value"
|
|
||||||
:value="driver.value.toString()"
|
|
||||||
label-key="harvester.setting.csiOnlineExpandValidation.value"
|
|
||||||
required
|
|
||||||
searchable
|
|
||||||
:mode="mode"
|
|
||||||
:disabled="disableEdit(driver.key)"
|
|
||||||
:options="provisionerValue"
|
|
||||||
@update:value="val => onValueChange(idx, val)"
|
|
||||||
@keydown.native.enter.prevent
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</InfoBox>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="btn btn-sm role-primary"
|
|
||||||
:disabled="disableAdd"
|
|
||||||
@click="add"
|
|
||||||
>
|
|
||||||
{{ t('generic.add') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.box {
|
|
||||||
position: relative;
|
|
||||||
padding-top: 40px;
|
|
||||||
}
|
|
||||||
.remove {
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
right: 10px;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
|
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
|
||||||
@@ -61,7 +62,7 @@ export default {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class="row"
|
class="row"
|
||||||
@input="update"
|
@update:value="update"
|
||||||
>
|
>
|
||||||
<div class="col span-12">
|
<div class="col span-12">
|
||||||
<LabeledInput
|
<LabeledInput
|
||||||
|
|||||||
@@ -1,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 { HCI } from '../../types';
|
||||||
import { DOC } from '../../config/doc-links';
|
import { DOC } from '../../config/doc-links';
|
||||||
import { docLink } from '../../utils/feature-flags';
|
import { docLink } from '../../utils/feature-flags';
|
||||||
import { NETWORK_TYPE } from '../../config/types';
|
|
||||||
|
|
||||||
const { L2VLAN, UNTAGGED } = NETWORK_TYPE;
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterEditStorageNetwork',
|
name: 'HarvesterEditStorageNetwork',
|
||||||
@@ -60,14 +57,11 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
let parsedDefaultValue = {};
|
let parsedDefaultValue = {};
|
||||||
let openVlan = false;
|
let openVlan = false;
|
||||||
let networkType = L2VLAN;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
parsedDefaultValue = JSON.parse(this.value.value);
|
parsedDefaultValue = JSON.parse(this.value.value);
|
||||||
networkType = 'vlan' in parsedDefaultValue ? L2VLAN : UNTAGGED; // backend doesn't provide networkType, so we check if vlan is provided instead
|
|
||||||
openVlan = true;
|
openVlan = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
networkType = L2VLAN;
|
|
||||||
parsedDefaultValue = {
|
parsedDefaultValue = {
|
||||||
vlan: '',
|
vlan: '',
|
||||||
clusterNetwork: '',
|
clusterNetwork: '',
|
||||||
@@ -79,7 +73,6 @@ export default {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
openVlan,
|
openVlan,
|
||||||
networkType,
|
|
||||||
errors: [],
|
errors: [],
|
||||||
exclude,
|
exclude,
|
||||||
parsedDefaultValue,
|
parsedDefaultValue,
|
||||||
@@ -94,35 +87,16 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
showVlan() {
|
|
||||||
return this.networkType === L2VLAN;
|
|
||||||
},
|
|
||||||
networkTypes() {
|
|
||||||
const types = [L2VLAN];
|
|
||||||
|
|
||||||
if (this.untaggedNetworkSettingEnabled) {
|
|
||||||
types.push(UNTAGGED);
|
|
||||||
}
|
|
||||||
|
|
||||||
return types;
|
|
||||||
},
|
|
||||||
storageNetworkExampleLink() {
|
storageNetworkExampleLink() {
|
||||||
const version = this.$store.getters['harvester-common/getServerVersion']();
|
const version = this.$store.getters['harvester-common/getServerVersion']();
|
||||||
|
|
||||||
return docLink(DOC.STORAGE_NETWORK_EXAMPLE, version);
|
return docLink(DOC.STORAGE_NETWORK_EXAMPLE, version);
|
||||||
},
|
},
|
||||||
|
|
||||||
untaggedNetworkSettingEnabled() {
|
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('untaggedNetworkSetting');
|
|
||||||
},
|
|
||||||
|
|
||||||
clusterNetworkOptions() {
|
clusterNetworkOptions() {
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
|
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
|
||||||
// untaggedNetwork filter out mgmt cluster network
|
|
||||||
const clusterNetworksOptions = this.networkType === UNTAGGED ? clusterNetworks.filter((net) => net.id !== 'mgmt') : clusterNetworks;
|
|
||||||
|
|
||||||
return clusterNetworksOptions.map((n) => {
|
return clusterNetworks.map((n) => {
|
||||||
const disabled = !n.isReadyForStorageNetwork;
|
const disabled = !n.isReadyForStorageNetwork;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -134,50 +108,7 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
|
||||||
networkType: {
|
|
||||||
handler(neu) {
|
|
||||||
this.parsedDefaultValue.clusterNetwork = '';
|
|
||||||
|
|
||||||
if (neu === L2VLAN) {
|
|
||||||
this.parsedDefaultValue.vlan = '';
|
|
||||||
} else {
|
|
||||||
delete this.parsedDefaultValue.vlan;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
deep: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
inputVlan(neu) {
|
|
||||||
if (neu === '') {
|
|
||||||
this.parsedDefaultValue.vlan = '';
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const newValue = Number(neu);
|
|
||||||
|
|
||||||
if (newValue > 4094) {
|
|
||||||
this.parsedDefaultValue.vlan = 4094;
|
|
||||||
} else if (newValue < 1) {
|
|
||||||
this.parsedDefaultValue.vlan = 1;
|
|
||||||
} else {
|
|
||||||
this.parsedDefaultValue.vlan = newValue;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
useDefault() {
|
|
||||||
this.openVlan = false;
|
|
||||||
this.networkType = L2VLAN;
|
|
||||||
this.parsedDefaultValue = {
|
|
||||||
vlan: '',
|
|
||||||
clusterNetwork: '',
|
|
||||||
range: '',
|
|
||||||
exclude: []
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
const exclude = this.exclude.filter((ip) => ip);
|
const exclude = this.exclude.filter((ip) => ip);
|
||||||
|
|
||||||
@@ -207,7 +138,7 @@ export default {
|
|||||||
errors.push(this.t('harvester.setting.storageNetwork.range.invalid', null, true));
|
errors.push(this.t('harvester.setting.storageNetwork.range.invalid', null, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.networkType === L2VLAN && !this.parsedDefaultValue.vlan) {
|
if (!this.parsedDefaultValue.vlan) {
|
||||||
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.vlan') }, true));
|
errors.push(this.t('validation.required', { key: this.t('harvester.setting.storageNetwork.vlan') }, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,25 +191,13 @@ export default {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div v-if="openVlan">
|
<div v-if="openVlan">
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="networkType"
|
|
||||||
class="mb-20"
|
|
||||||
:options="networkTypes"
|
|
||||||
:mode="mode"
|
|
||||||
:label="t('harvester.fields.type')"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LabeledInput
|
<LabeledInput
|
||||||
v-if="showVlan"
|
|
||||||
v-model:value.number="parsedDefaultValue.vlan"
|
v-model:value.number="parsedDefaultValue.vlan"
|
||||||
type="number"
|
type="number"
|
||||||
class="mb-20"
|
class="mb-20"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
required
|
required
|
||||||
placeholder="e.g. 1 - 4094"
|
|
||||||
label-key="harvester.setting.storageNetwork.vlan"
|
label-key="harvester.setting.storageNetwork.vlan"
|
||||||
@update:value="inputVlan"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
|
|||||||
@@ -3,9 +3,6 @@ import { NAMESPACE } from '@shell/config/types';
|
|||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
|
||||||
const SELECT_ALL = 'select_all';
|
|
||||||
const UNSELECT_ALL = 'unselect_all';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterBundleNamespaces',
|
name: 'HarvesterBundleNamespaces',
|
||||||
|
|
||||||
@@ -14,74 +11,47 @@ export default {
|
|||||||
mixins: [CreateEditView],
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
async fetch() {
|
async fetch() {
|
||||||
this.loading = true;
|
|
||||||
|
|
||||||
await this.$store.dispatch('harvester/findAll', { type: NAMESPACE });
|
await this.$store.dispatch('harvester/findAll', { type: NAMESPACE });
|
||||||
|
|
||||||
try {
|
|
||||||
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/namespaces?link=supportbundle');
|
|
||||||
const response = await this.$store.dispatch('harvester/request', { url });
|
|
||||||
|
|
||||||
this.defaultNamespaces = response.data || [];
|
|
||||||
} catch (error) {
|
|
||||||
this.defaultNamespaces = [];
|
|
||||||
} finally {
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
let namespaces = [];
|
||||||
const namespacesStr = this.value?.value || this.value?.default || '';
|
const namespacesStr = this.value?.value || this.value?.default || '';
|
||||||
const namespaces = namespacesStr ? namespacesStr.split(',') : [];
|
|
||||||
|
|
||||||
return {
|
if (namespacesStr) {
|
||||||
namespaces,
|
namespaces = namespacesStr.split(',');
|
||||||
defaultNamespaces: [],
|
}
|
||||||
loading: true
|
|
||||||
};
|
return { namespaces };
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
allNamespaces() {
|
|
||||||
return this.$store.getters['harvester/all'](NAMESPACE).map((ns) => ns.id);
|
|
||||||
},
|
|
||||||
|
|
||||||
filteredNamespaces() {
|
|
||||||
const defaultIds = this.defaultNamespaces.map((ns) => ns.id);
|
|
||||||
|
|
||||||
return this.allNamespaces.filter((ns) => !defaultIds.includes(ns));
|
|
||||||
},
|
|
||||||
|
|
||||||
namespaceOptions() {
|
namespaceOptions() {
|
||||||
const allSelected =
|
return this.$store.getters['harvester/all'](NAMESPACE).map((N) => {
|
||||||
this.namespaces.length === this.filteredNamespaces.length &&
|
return {
|
||||||
this.filteredNamespaces.every((ns) => this.namespaces.includes(ns));
|
label: N.id,
|
||||||
|
value: N.id
|
||||||
const controlOption = allSelected ? { label: this.t('harvester.modal.bundle.namespaces.unselectAll'), value: UNSELECT_ALL } : { label: this.t('harvester.modal.bundle.namespaces.selectAll'), value: SELECT_ALL };
|
};
|
||||||
|
});
|
||||||
return [controlOption, ...this.filteredNamespaces];
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
update(selected) {
|
update() {
|
||||||
if (selected.includes(SELECT_ALL)) {
|
const namespaceStr = this.namespaces.join(',');
|
||||||
this.namespaces = [...this.filteredNamespaces];
|
|
||||||
} else if (selected.includes(UNSELECT_ALL)) {
|
|
||||||
this.namespaces = [];
|
|
||||||
} else {
|
|
||||||
this.namespaces = selected.filter((val) => val !== SELECT_ALL && val !== UNSELECT_ALL);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.value.value = this.namespaces.join(',');
|
this.value['value'] = namespaceStr;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
'value.value'(newVal) {
|
'value.value': {
|
||||||
const raw = newVal || this.value.default || '';
|
handler(neu) {
|
||||||
|
if (neu === this.value.default || !neu) {
|
||||||
this.namespaces = raw ? raw.split(',') : [];
|
this.namespaces = [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -92,7 +62,6 @@ export default {
|
|||||||
<div class="col span-12">
|
<div class="col span-12">
|
||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
v-model:value="namespaces"
|
v-model:value="namespaces"
|
||||||
:loading="loading"
|
|
||||||
:multiple="true"
|
:multiple="true"
|
||||||
label-key="nameNsDescription.namespace.label"
|
label-key="nameNsDescription.namespace.label"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
|
|||||||
@@ -1,163 +0,0 @@
|
|||||||
<script>
|
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
||||||
import { RadioGroup } from '@components/Form/Radio';
|
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'HarvesterUpgradeConfig',
|
|
||||||
|
|
||||||
components: {
|
|
||||||
LabeledInput,
|
|
||||||
LabeledSelect,
|
|
||||||
RadioGroup
|
|
||||||
},
|
|
||||||
mixins: [CreateEditView],
|
|
||||||
|
|
||||||
data() {
|
|
||||||
let parseDefaultValue = {};
|
|
||||||
|
|
||||||
try {
|
|
||||||
parseDefaultValue = this.value.value ? JSON.parse(this.value.value) : JSON.parse(this.value.default);
|
|
||||||
} catch (error) {
|
|
||||||
parseDefaultValue = JSON.parse(this.value.default);
|
|
||||||
}
|
|
||||||
parseDefaultValue = this.normalizeValue(parseDefaultValue);
|
|
||||||
|
|
||||||
return {
|
|
||||||
parseDefaultValue,
|
|
||||||
errors: []
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
...mapGetters({ t: 'i18n/t' }),
|
|
||||||
strategyOptions() {
|
|
||||||
return [
|
|
||||||
{ value: 'sequential', label: 'sequential' },
|
|
||||||
{ value: 'skip', label: 'skip' },
|
|
||||||
{ value: 'parallel', label: 'parallel' }
|
|
||||||
];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
this.update();
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
normalizeValue(obj) {
|
|
||||||
if (!obj.imagePreloadOption) {
|
|
||||||
obj.imagePreloadOption = { strategy: { type: 'sequential' } };
|
|
||||||
}
|
|
||||||
if (!obj.imagePreloadOption.strategy) {
|
|
||||||
obj.imagePreloadOption.strategy = { type: 'sequential' };
|
|
||||||
}
|
|
||||||
if (!obj.imagePreloadOption.strategy.type) {
|
|
||||||
obj.imagePreloadOption.strategy.type = 'sequential';
|
|
||||||
}
|
|
||||||
// Only set concurrency if type is 'parallel'
|
|
||||||
if (obj.imagePreloadOption.strategy.type === 'parallel') {
|
|
||||||
if (typeof obj.imagePreloadOption.strategy.concurrency !== 'number') {
|
|
||||||
obj.imagePreloadOption.strategy.concurrency = 0;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
delete obj.imagePreloadOption.strategy.concurrency;
|
|
||||||
}
|
|
||||||
if (typeof obj.restoreVM !== 'boolean') {
|
|
||||||
obj.restoreVM = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return obj;
|
|
||||||
},
|
|
||||||
update() {
|
|
||||||
try {
|
|
||||||
// Clone to avoid mutating the form state
|
|
||||||
const valueToSave = JSON.parse(JSON.stringify(this.parseDefaultValue));
|
|
||||||
|
|
||||||
if (valueToSave.imagePreloadOption && valueToSave.imagePreloadOption.strategy) {
|
|
||||||
if (valueToSave.imagePreloadOption.strategy.type !== 'parallel') {
|
|
||||||
delete valueToSave.imagePreloadOption.strategy.concurrency;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.value['value'] = JSON.stringify(valueToSave, null, 2);
|
|
||||||
this.errors = [];
|
|
||||||
} catch (e) {
|
|
||||||
this.errors = ['Invalid JSON'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
value: {
|
|
||||||
handler(neu) {
|
|
||||||
let parseDefaultValue;
|
|
||||||
|
|
||||||
try {
|
|
||||||
parseDefaultValue = JSON.parse(neu.value);
|
|
||||||
} catch (err) {
|
|
||||||
parseDefaultValue = JSON.parse(this.value.default);
|
|
||||||
}
|
|
||||||
parseDefaultValue = this.normalizeValue(parseDefaultValue);
|
|
||||||
this['parseDefaultValue'] = parseDefaultValue;
|
|
||||||
this.update();
|
|
||||||
},
|
|
||||||
deep: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="row"
|
|
||||||
@input="update"
|
|
||||||
>
|
|
||||||
<div class="col span-12">
|
|
||||||
<label class="mb-5"><b>{{ t('harvester.setting.upgrade.imagePreloadStrategy') }}</b></label>
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="parseDefaultValue.imagePreloadOption.strategy.type"
|
|
||||||
class="mb-20"
|
|
||||||
:mode="mode"
|
|
||||||
:label="t('harvester.setting.upgrade.strategyType')"
|
|
||||||
:options="strategyOptions"
|
|
||||||
@update:value="update"
|
|
||||||
/>
|
|
||||||
<LabeledInput
|
|
||||||
v-if="parseDefaultValue.imagePreloadOption.strategy.type === 'parallel'"
|
|
||||||
v-model:value.number="parseDefaultValue.imagePreloadOption.strategy.concurrency"
|
|
||||||
class="mb-20"
|
|
||||||
:mode="mode"
|
|
||||||
:label="t('harvester.setting.upgrade.concurrency')"
|
|
||||||
min="0"
|
|
||||||
type="number"
|
|
||||||
/>
|
|
||||||
<label class="mb-5"><b>{{ t('harvester.setting.upgrade.restoreVM') }}</b></label>
|
|
||||||
<RadioGroup
|
|
||||||
v-model:value="parseDefaultValue.restoreVM"
|
|
||||||
class="mb-20"
|
|
||||||
name="restoreVM"
|
|
||||||
:options="[true, false]"
|
|
||||||
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
|
||||||
@update:value="update"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
v-if="errors.length"
|
|
||||||
class="error"
|
|
||||||
>
|
|
||||||
{{ errors[0] }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.error {
|
|
||||||
color: #d9534f;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -59,7 +59,7 @@ export default {
|
|||||||
name="model"
|
name="model"
|
||||||
:options="[true,false]"
|
:options="[true,false]"
|
||||||
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||||
@update:value="update"
|
@input="update"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LabeledInput
|
<LabeledInput
|
||||||
|
|||||||
@@ -1,270 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
|
|
||||||
import { RadioGroup } from '@components/Form/Radio';
|
|
||||||
import { Banner } from '@components/Banner';
|
|
||||||
import ArrayList from '@shell/components/form/ArrayList';
|
|
||||||
import { allHash } from '@shell/utils/promise';
|
|
||||||
import { isValidCIDR } from '@shell/utils/validators/cidr';
|
|
||||||
import { NODE } from '@shell/config/types';
|
|
||||||
import { _EDIT } from '@shell/config/query-params';
|
|
||||||
import { HCI } from '../../types';
|
|
||||||
|
|
||||||
const DEFAULT_NETWORK = {
|
|
||||||
clusterNetwork: '',
|
|
||||||
vlan: '',
|
|
||||||
range: '',
|
|
||||||
exclude: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'VMMigrationNetwork',
|
|
||||||
|
|
||||||
components: {
|
|
||||||
LabeledInput,
|
|
||||||
LabeledSelect,
|
|
||||||
RadioGroup,
|
|
||||||
Banner,
|
|
||||||
ArrayList,
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
|
||||||
registerBeforeHook: {
|
|
||||||
type: Function,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
mode: {
|
|
||||||
type: String,
|
|
||||||
default: _EDIT,
|
|
||||||
},
|
|
||||||
value: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({ value: '' }),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await allHash({
|
|
||||||
clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }),
|
|
||||||
vlanStatus: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VLAN_STATUS }),
|
|
||||||
nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }),
|
|
||||||
});
|
|
||||||
|
|
||||||
this.fetchError = null;
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to fetch network data:', e); // eslint-disable-line no-console
|
|
||||||
this.fetchError = this.t('harvester.setting.vmMigrationNetwork.fetchError', { error: e.message || e }, true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
const { parsed, enabled, parseError } = this.parseInitialValue();
|
|
||||||
|
|
||||||
return {
|
|
||||||
enabled,
|
|
||||||
network: { ...DEFAULT_NETWORK, ...parsed },
|
|
||||||
fetchError: null,
|
|
||||||
parseError,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
this.registerBeforeHook?.(this.willSave, 'willSave');
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
allErrors() {
|
|
||||||
return [this.fetchError, this.parseError].filter(Boolean);
|
|
||||||
},
|
|
||||||
|
|
||||||
clusterNetworkOptions() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
const networks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
|
|
||||||
|
|
||||||
return networks.map((net) => ({
|
|
||||||
label: net.isReadyForStorageNetwork ? net.id : `${ net.id } (${ this.t('generic.notReady') })`,
|
|
||||||
value: net.id,
|
|
||||||
disabled: !net.isReadyForStorageNetwork,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
disableEdit() {
|
|
||||||
return !!(this.fetchError || this.parseError);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
parseInitialValue() {
|
|
||||||
let parsed = {};
|
|
||||||
let enabled = false;
|
|
||||||
let parseError = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (typeof this.value.value === 'string' && this.value.value.trim()) {
|
|
||||||
parsed = JSON.parse(this.value.value);
|
|
||||||
enabled = true;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[VMMigrationNetwork] Failed to parse value:', e); // eslint-disable-line no-console
|
|
||||||
parseError = this.t('harvester.setting.vmMigrationNetwork.parseError', { error: e.message }, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Array.isArray(parsed.exclude)) {
|
|
||||||
parsed.exclude = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
parsed, enabled, parseError
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
clearErrors() {
|
|
||||||
this.fetchError = null;
|
|
||||||
this.parseError = null;
|
|
||||||
},
|
|
||||||
|
|
||||||
inputVlan(val) {
|
|
||||||
this.network.vlan = val ? Math.min(4094, Math.max(1, Number(val))) : '';
|
|
||||||
this.update();
|
|
||||||
},
|
|
||||||
|
|
||||||
useDefault() {
|
|
||||||
this.network = { ...DEFAULT_NETWORK };
|
|
||||||
this.value.value = '';
|
|
||||||
this.enabled = false;
|
|
||||||
this.clearErrors();
|
|
||||||
},
|
|
||||||
|
|
||||||
update() {
|
|
||||||
try {
|
|
||||||
this.value.value = this.enabled ? JSON.stringify({
|
|
||||||
...this.network,
|
|
||||||
exclude: (this.network.exclude || []).filter((e) => !!e?.trim()),
|
|
||||||
}) : '';
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to stringify network config:', e); // eslint-disable-line no-console
|
|
||||||
this.value.value = '';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
validateInputs() {
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
if (!this.network.clusterNetwork) {
|
|
||||||
errors.push(this.t('validation.required', { key: this.t('harvester.setting.vmMigrationNetwork.clusterNetwork') }, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.network.range) {
|
|
||||||
errors.push(this.t('validation.required', { key: this.t('harvester.setting.vmMigrationNetwork.range.label') }, true));
|
|
||||||
} else if (!isValidCIDR(this.network.range)) {
|
|
||||||
errors.push(this.t('harvester.setting.vmMigrationNetwork.range.invalid', null, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.network.vlan === '') {
|
|
||||||
errors.push(this.t('validation.required', { key: this.t('harvester.setting.vmMigrationNetwork.vlan') }, true));
|
|
||||||
} else {
|
|
||||||
const vlan = Number(this.network.vlan);
|
|
||||||
|
|
||||||
if (isNaN(vlan) || vlan < 1 || vlan > 4094) {
|
|
||||||
errors.push(this.t('validation.between', {
|
|
||||||
key: this.t('harvester.setting.vmMigrationNetwork.vlan'),
|
|
||||||
min: 1,
|
|
||||||
max: 4094,
|
|
||||||
}, true));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const cidr of this.network.exclude || []) {
|
|
||||||
if (cidr && !isValidCIDR(cidr)) {
|
|
||||||
errors.push(this.t('harvester.setting.storageNetwork.exclude.invalid', { value: cidr }, true));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
},
|
|
||||||
|
|
||||||
async willSave() {
|
|
||||||
if (!this.enabled) {
|
|
||||||
this.useDefault();
|
|
||||||
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.update();
|
|
||||||
|
|
||||||
const errors = this.validateInputs();
|
|
||||||
|
|
||||||
return errors.length ? Promise.reject(errors) : Promise.resolve();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<Banner
|
|
||||||
v-for="(errorMsg, index) in allErrors"
|
|
||||||
:key="index"
|
|
||||||
color="error"
|
|
||||||
>
|
|
||||||
{{ errorMsg }}
|
|
||||||
</Banner>
|
|
||||||
<RadioGroup
|
|
||||||
v-model:value="enabled"
|
|
||||||
class="mb-20"
|
|
||||||
name="enableMigrationNetwork"
|
|
||||||
:options="[true, false]"
|
|
||||||
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
|
||||||
@update:value="update"
|
|
||||||
/>
|
|
||||||
<template v-if="enabled">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="network.clusterNetwork"
|
|
||||||
required
|
|
||||||
label-key="harvester.setting.vmMigrationNetwork.clusterNetwork"
|
|
||||||
class="mb-20"
|
|
||||||
:mode="mode"
|
|
||||||
:options="clusterNetworkOptions"
|
|
||||||
:disabled="disableEdit"
|
|
||||||
@update:value="update"
|
|
||||||
/>
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value.number="network.vlan"
|
|
||||||
required
|
|
||||||
type="number"
|
|
||||||
class="mb-20"
|
|
||||||
:min="1"
|
|
||||||
:max="4094"
|
|
||||||
:mode="mode"
|
|
||||||
placeholder="e.g. 1 - 4094"
|
|
||||||
label-key="harvester.setting.vmMigrationNetwork.vlan"
|
|
||||||
:disabled="disableEdit"
|
|
||||||
@update:value="inputVlan"
|
|
||||||
/>
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="network.range"
|
|
||||||
required
|
|
||||||
class="mb-5"
|
|
||||||
:mode="mode"
|
|
||||||
:placeholder="t('harvester.setting.vmMigrationNetwork.range.placeholder')"
|
|
||||||
label-key="harvester.setting.vmMigrationNetwork.range.label"
|
|
||||||
:disabled="disableEdit"
|
|
||||||
@update:value="update"
|
|
||||||
/>
|
|
||||||
<ArrayList
|
|
||||||
v-model:value="network.exclude"
|
|
||||||
:show-header="true"
|
|
||||||
:default-add-value="''"
|
|
||||||
:mode="mode"
|
|
||||||
:add-disabled="disableEdit"
|
|
||||||
:add-label="t('harvester.setting.vmMigrationNetwork.exclude.addButton')"
|
|
||||||
:value-label="t('harvester.setting.vmMigrationNetwork.exclude.label')"
|
|
||||||
:value-placeholder="t('harvester.setting.storageNetwork.exclude.placeholder')"
|
|
||||||
@update:value="update"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
// suffix of doc link, see utils/feature-flags.js how to get complete doc link
|
// suffix of doc link, see utils/feature-flags.js how to get complete doc link
|
||||||
export const DOC = {
|
export const DOC = {
|
||||||
CONSOLE_URL: `/host/#remote-console`,
|
CONSOLE_URL: `/host/#remote-console`,
|
||||||
RANCHER_INTEGRATION_URL: `/rancher/rancher-integration`,
|
RANCHER_INTEGRATION_URL: `/rancher/rancher-integration`,
|
||||||
KSMTUNED_MODE: `/host/#ksmtuned-mode`,
|
STORAGE_NETWORK_EXAMPLE: `/advanced/storagenetwork#configuration-example`,
|
||||||
UPGRADE_URL: `/upgrade/index`,
|
KSMTUNED_MODE: `/host/#ksmtuned-mode`,
|
||||||
UPGRADE_CONFIG_URL: `/advanced/index#upgrade-config`,
|
UPGRADE_URL: `/upgrade/index`
|
||||||
STORAGE_NETWORK_EXAMPLE: `/advanced/storagenetwork#configuration-example`,
|
|
||||||
SUPPORT_BUNDLE_NAMESPACES: `/advanced/index/#support-bundle-namespaces`,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,68 +1,63 @@
|
|||||||
import semver from 'semver';
|
// https://github.com/harvester/dashboard/releases/tag/v1.3.0
|
||||||
|
const featuresV130 = [
|
||||||
|
'supportHarvesterClusterVersion'
|
||||||
|
];
|
||||||
|
|
||||||
const FEATURE_FLAGS = {
|
// https://github.com/harvester/dashboard/releases/tag/v1.3.1
|
||||||
'v1.3.0': [
|
const featuresV131 = [
|
||||||
'supportHarvesterClusterVersion'
|
...featuresV130,
|
||||||
],
|
'autoRotateRke2CertsSetting',
|
||||||
'v1.3.1': [
|
'supportBundleNodeCollectionTimeoutSetting'
|
||||||
'autoRotateRke2CertsSetting',
|
];
|
||||||
'supportBundleNodeCollectionTimeoutSetting'
|
|
||||||
],
|
// https://github.com/harvester/dashboard/releases/tag/v1.3.2
|
||||||
'v1.3.2': [
|
const featuresV132 = [
|
||||||
'kubeconfigDefaultTokenTTLMinutesSetting',
|
...featuresV131,
|
||||||
'improveMaintenanceMode',
|
'kubeconfigDefaultTokenTTLMinutesSetting',
|
||||||
],
|
'improveMaintenanceMode',
|
||||||
'v1.3.3': [],
|
];
|
||||||
'v1.4.0': [
|
|
||||||
'cpuPinning',
|
// TODO: add v1.3.3 official release note
|
||||||
'usbPassthrough',
|
// https://github.com/harvester/dashboard/releases/tag/v1.3.3-dev-20250105
|
||||||
'volumeEncryption',
|
const featuresV133 = [
|
||||||
'schedulingVMBackup',
|
...featuresV132,
|
||||||
'vmSnapshotQuota',
|
];
|
||||||
'longhornV2LVMSupport',
|
|
||||||
'improveMaintenanceMode',
|
// https://github.com/harvester/dashboard/releases/tag/v1.4.0
|
||||||
'upgradeConfigSetting'
|
const featuresV140 = [
|
||||||
],
|
...featuresV132,
|
||||||
'v1.4.1': [],
|
'cpuPinning',
|
||||||
'v1.4.2': [
|
'usbPassthrough',
|
||||||
'refreshIntervalInSecond',
|
'volumeEncryption',
|
||||||
'allowEmptySnapshotClassName'
|
'schedulingVMBackup',
|
||||||
],
|
'vmSnapshotQuota',
|
||||||
'v1.4.3': [],
|
'longhornV2LVMSupport',
|
||||||
'v1.5.0': [
|
'improveMaintenanceMode',
|
||||||
'tpmPersistentState',
|
];
|
||||||
'efiPersistentState',
|
|
||||||
'untaggedNetworkSetting',
|
// https://github.com/harvester/dashboard/releases/tag/v1.4.1
|
||||||
'skipSingleReplicaDetachedVol',
|
const featuresV141 = [
|
||||||
'thirdPartyStorage',
|
...featuresV140
|
||||||
'liveMigrationProgress'
|
];
|
||||||
],
|
|
||||||
'v1.5.1': [],
|
const featuresV142 = [
|
||||||
'v1.6.0': [
|
...featuresV141,
|
||||||
'vmMachineTypes',
|
'refreshIntervalInSecond',
|
||||||
'customSupportBundle',
|
'allowEmptySnapshotClassName'
|
||||||
'csiOnlineExpandValidation',
|
];
|
||||||
'vmNetworkMigration',
|
|
||||||
'kubeovnVpcSubnet',
|
// TODO: add v1.5.0 official release note
|
||||||
'rancherClusterSetting',
|
const featuresV150 = [
|
||||||
'cpuMemoryHotplug',
|
...featuresV142
|
||||||
'cdiSettings',
|
];
|
||||||
'vmCloneRunStrategy',
|
|
||||||
]
|
export const RELEASE_FEATURES = {
|
||||||
|
'v1.3.0': featuresV130,
|
||||||
|
'v1.3.1': featuresV131,
|
||||||
|
'v1.3.2': featuresV132,
|
||||||
|
'v1.3.3': featuresV133,
|
||||||
|
'v1.4.0': featuresV140,
|
||||||
|
'v1.4.1': featuresV141,
|
||||||
|
'v1.4.2': featuresV142,
|
||||||
|
'v1.5.0': featuresV150
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateFeatureFlags = () => {
|
|
||||||
const versions = [...Object.keys(FEATURE_FLAGS)].filter((version) => semver.valid(version)).sort(semver.compare);
|
|
||||||
|
|
||||||
const generatedFlags = {};
|
|
||||||
|
|
||||||
versions.forEach((version, index) => {
|
|
||||||
const previousVersion = versions[index - 1];
|
|
||||||
|
|
||||||
generatedFlags[version] = previousVersion ? [...generatedFlags[previousVersion], ...FEATURE_FLAGS[version]] : [...FEATURE_FLAGS[version]];
|
|
||||||
});
|
|
||||||
|
|
||||||
return generatedFlags;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const RELEASE_FEATURES = generateFeatureFlags();
|
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import {
|
|||||||
CONFIGURED_PROVIDERS,
|
CONFIGURED_PROVIDERS,
|
||||||
SUB_TYPE,
|
SUB_TYPE,
|
||||||
ADDRESS,
|
ADDRESS,
|
||||||
DESCRIPTION,
|
|
||||||
} from '@shell/config/table-headers';
|
} from '@shell/config/table-headers';
|
||||||
import { IF_HAVE } from '@shell/store/type-map';
|
import { IF_HAVE } from '@shell/store/type-map';
|
||||||
import {
|
import {
|
||||||
@@ -32,9 +31,7 @@ import {
|
|||||||
FINGERPRINT,
|
FINGERPRINT,
|
||||||
IMAGE_PROGRESS,
|
IMAGE_PROGRESS,
|
||||||
SNAPSHOT_TARGET_VOLUME,
|
SNAPSHOT_TARGET_VOLUME,
|
||||||
IMAGE_VIRTUAL_SIZE,
|
IMAGE_VIRTUAL_SIZE
|
||||||
IMAGE_STORAGE_CLASS,
|
|
||||||
HARVESTER_DESCRIPTION
|
|
||||||
} from './table-headers';
|
} from './table-headers';
|
||||||
|
|
||||||
const TEMPLATE = HCI.VM_VERSION;
|
const TEMPLATE = HCI.VM_VERSION;
|
||||||
@@ -224,8 +221,6 @@ export function init($plugin, store) {
|
|||||||
STATE,
|
STATE,
|
||||||
NAME_COL,
|
NAME_COL,
|
||||||
NAMESPACE_COL,
|
NAMESPACE_COL,
|
||||||
HARVESTER_DESCRIPTION,
|
|
||||||
IMAGE_STORAGE_CLASS,
|
|
||||||
IMAGE_PROGRESS,
|
IMAGE_PROGRESS,
|
||||||
IMAGE_DOWNLOAD_SIZE,
|
IMAGE_DOWNLOAD_SIZE,
|
||||||
IMAGE_VIRTUAL_SIZE,
|
IMAGE_VIRTUAL_SIZE,
|
||||||
@@ -258,7 +253,7 @@ export function init($plugin, store) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (isSingleVirtualCluster) {
|
if (isSingleVirtualCluster) {
|
||||||
headers(NAMESPACE, [STATE, NAME_UNLINKED, DESCRIPTION, AGE]);
|
headers(NAMESPACE, [STATE, NAME_UNLINKED, AGE]);
|
||||||
basicType([NAMESPACE]);
|
basicType([NAMESPACE]);
|
||||||
virtualType({
|
virtualType({
|
||||||
labelKey: 'harvester.namespace.label',
|
labelKey: 'harvester.namespace.label',
|
||||||
@@ -424,7 +419,6 @@ export function init($plugin, store) {
|
|||||||
[
|
[
|
||||||
HCI.CLUSTER_NETWORK,
|
HCI.CLUSTER_NETWORK,
|
||||||
HCI.NETWORK_ATTACHMENT,
|
HCI.NETWORK_ATTACHMENT,
|
||||||
HCI.VPC,
|
|
||||||
HCI.LB,
|
HCI.LB,
|
||||||
HCI.IP_POOL,
|
HCI.IP_POOL,
|
||||||
],
|
],
|
||||||
@@ -551,21 +545,6 @@ export function init($plugin, store) {
|
|||||||
exact: false
|
exact: false
|
||||||
});
|
});
|
||||||
|
|
||||||
configureType(HCI.VPC, { hiddenNamespaceGroupButton: true, canYaml: false });
|
|
||||||
|
|
||||||
virtualType({
|
|
||||||
labelKey: 'harvester.vpc.label',
|
|
||||||
name: HCI.VPC,
|
|
||||||
namespaced: true,
|
|
||||||
weight: 187,
|
|
||||||
route: {
|
|
||||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
|
||||||
params: { resource: HCI.VPC }
|
|
||||||
},
|
|
||||||
exact: false,
|
|
||||||
ifHaveType: HCI.VPC,
|
|
||||||
});
|
|
||||||
|
|
||||||
configureType(HCI.SNAPSHOT, {
|
configureType(HCI.SNAPSHOT, {
|
||||||
isCreatable: false,
|
isCreatable: false,
|
||||||
location: {
|
location: {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// TODO: delete this not used variable
|
|
||||||
export const MemoryUnit = [{
|
export const MemoryUnit = [{
|
||||||
label: 'Mi',
|
label: 'Mi',
|
||||||
value: 'Mi'
|
value: 'Mi'
|
||||||
@@ -70,7 +69,6 @@ export const ADD_ONS = {
|
|||||||
RANCHER_MONITORING: 'rancher-monitoring',
|
RANCHER_MONITORING: 'rancher-monitoring',
|
||||||
VM_IMPORT_CONTROLLER: 'vm-import-controller',
|
VM_IMPORT_CONTROLLER: 'vm-import-controller',
|
||||||
LVM_DRIVER: 'lvm.driver.harvesterhci.io',
|
LVM_DRIVER: 'lvm.driver.harvesterhci.io',
|
||||||
KUBEOVN_OPERATOR: 'kubeovn-operator',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CSI_SECRETS = {
|
export const CSI_SECRETS = {
|
||||||
|
|||||||
@@ -7,73 +7,60 @@ export const LABELS_TO_IGNORE_REGEX = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const HCI = {
|
export const HCI = {
|
||||||
CLOUD_INIT: 'harvesterhci.io/cloud-init-template',
|
CLOUD_INIT: 'harvesterhci.io/cloud-init-template',
|
||||||
CURRENT_IP: 'rke2.io/internal-ip',
|
CURRENT_IP: 'rke2.io/internal-ip',
|
||||||
IMAGE_ID: 'harvesterhci.io/imageId',
|
IMAGE_ID: 'harvesterhci.io/imageId',
|
||||||
SSH_NAMES: 'harvesterhci.io/sshNames',
|
SSH_NAMES: 'harvesterhci.io/sshNames',
|
||||||
NETWORK_IPS: 'network.harvesterhci.io/ips',
|
NETWORK_IPS: 'network.harvesterhci.io/ips',
|
||||||
TEMPLATE_VERSION_CUSTOM_NAME: 'template-version.harvesterhci.io/customName',
|
TEMPLATE_VERSION_CUSTOM_NAME: 'template-version.harvesterhci.io/customName',
|
||||||
CREATOR: 'harvesterhci.io/creator',
|
CREATOR: 'harvesterhci.io/creator',
|
||||||
OS: 'harvesterhci.io/os',
|
OS: 'harvesterhci.io/os',
|
||||||
GOLDEN_IMAGE: 'harvesterhci.io/goldenImage',
|
NETWORK_TYPE: 'network.harvesterhci.io/type',
|
||||||
NETWORK_TYPE: 'network.harvesterhci.io/type',
|
VM_NAME: 'harvesterhci.io/vmName',
|
||||||
VM_NAME: 'harvesterhci.io/vmName',
|
VM_NAME_PREFIX: 'harvesterhci.io/vmNamePrefix',
|
||||||
VM_NAME_PREFIX: 'harvesterhci.io/vmNamePrefix',
|
VM_RESERVED_MEMORY: 'harvesterhci.io/reservedMemory',
|
||||||
VM_RESERVED_MEMORY: 'harvesterhci.io/reservedMemory',
|
MAINTENANCE_STATUS: 'harvesterhci.io/maintain-status',
|
||||||
MAINTENANCE_STATUS: 'harvesterhci.io/maintain-status',
|
HOST_CUSTOM_NAME: 'harvesterhci.io/host-custom-name',
|
||||||
HOST_CUSTOM_NAME: 'harvesterhci.io/host-custom-name',
|
HOST_CONSOLE_URL: 'harvesterhci.io/host-console-url',
|
||||||
HOST_CONSOLE_URL: 'harvesterhci.io/host-console-url',
|
RESTORE_NAME: 'restore.harvesterhci.io/name',
|
||||||
RESTORE_NAME: 'restore.harvesterhci.io/name',
|
NODE_ROLE_MASTER: 'node-role.kubernetes.io/master',
|
||||||
NODE_ROLE_MASTER: 'node-role.kubernetes.io/master',
|
NODE_ROLE_CONTROL_PLANE: 'node-role.kubernetes.io/control-plane',
|
||||||
NODE_ROLE_CONTROL_PLANE: 'node-role.kubernetes.io/control-plane',
|
NODE_ROLE_ETCD: 'node-role.harvesterhci.io/witness',
|
||||||
NODE_ROLE_ETCD: 'node-role.harvesterhci.io/witness',
|
PROMOTE_STATUS: 'harvesterhci.io/promote-status',
|
||||||
PROMOTE_STATUS: 'harvesterhci.io/promote-status',
|
MIGRATION_STATE: 'harvesterhci.io/migrationState',
|
||||||
MIGRATION_STATE: 'harvesterhci.io/migrationState',
|
VOLUME_CLAIM_TEMPLATE: 'harvesterhci.io/volumeClaimTemplates',
|
||||||
VOLUME_CLAIM_TEMPLATE: 'harvesterhci.io/volumeClaimTemplates',
|
IMAGE_NAME: 'harvesterhci.io/image-name',
|
||||||
IMAGE_NAME: 'harvesterhci.io/image-name',
|
INIT_IP: 'etcd.rke2.cattle.io/node-address',
|
||||||
INIT_IP: 'etcd.rke2.cattle.io/node-address',
|
NODE_SCHEDULABLE: 'kubevirt.io/schedulable',
|
||||||
NODE_SCHEDULABLE: 'kubevirt.io/schedulable',
|
NETWORK_ROUTE: 'network.harvesterhci.io/route',
|
||||||
NETWORK_ROUTE: 'network.harvesterhci.io/route',
|
MATCHED_NODES: 'network.harvesterhci.io/matched-nodes',
|
||||||
MATCHED_NODES: 'network.harvesterhci.io/matched-nodes',
|
OS_UPGRADE_IMAGE: 'harvesterhci.io/os-upgrade-image',
|
||||||
UPGRADE: 'harvesterhci.io/upgrade',
|
LATEST_UPGRADE: 'harvesterhci.io/latestUpgrade',
|
||||||
OS_UPGRADE_IMAGE: 'harvesterhci.io/os-upgrade-image',
|
UPGRADE_STATE: 'harvesterhci.io/upgradeState',
|
||||||
LATEST_UPGRADE: 'harvesterhci.io/latestUpgrade',
|
REAY_MESSAGE: 'harvesterhci.io/read-message',
|
||||||
UPGRADE_STATE: 'harvesterhci.io/upgradeState',
|
DYNAMIC_SSHKEYS_NAMES: 'harvesterhci.io/dynamic-ssh-key-names',
|
||||||
REAY_MESSAGE: 'harvesterhci.io/read-message',
|
DYNAMIC_SSHKEYS_USERS: 'harvesterhci.io/dynamic-ssh-key-users',
|
||||||
DYNAMIC_SSHKEYS_NAMES: 'harvesterhci.io/dynamic-ssh-key-names',
|
IMAGE_SUFFIX: 'harvesterhci.io/image-type',
|
||||||
DYNAMIC_SSHKEYS_USERS: 'harvesterhci.io/dynamic-ssh-key-users',
|
OS_TYPE: 'harvesterhci.io/os-type',
|
||||||
IMAGE_SUFFIX: 'harvesterhci.io/image-type',
|
STORAGE_PROVISIONER: 'harvesterhci.io/storageProvisioner',
|
||||||
OS_TYPE: 'harvesterhci.io/os-type',
|
HOST_REQUEST: 'management.cattle.io/pod-requests',
|
||||||
STORAGE_PROVISIONER: 'harvesterhci.io/storageProvisioner',
|
STORAGE_CLASS: 'harvesterhci.io/storageClassName',
|
||||||
SKIP_SINGLE_REPLICA_DETACHED_VOL: 'harvesterhci.io/skipSingleReplicaDetachedVol',
|
STORAGE_NETWORK: 'storage-network.settings.harvesterhci.io',
|
||||||
HOST_REQUEST: 'management.cattle.io/pod-requests',
|
ADDON_EXPERIMENTAL: 'addon.harvesterhci.io/experimental',
|
||||||
STORAGE_CLASS: 'harvesterhci.io/storageClassName',
|
VOLUME_ERROR: 'longhorn.io/volume-scheduling-error',
|
||||||
STORAGE_NETWORK: 'storage-network.settings.harvesterhci.io',
|
KVM_AMD_CPU: 'cpu-feature.node.kubevirt.io/svm',
|
||||||
ADDON_EXPERIMENTAL: 'addon.harvesterhci.io/experimental',
|
KVM_INTEL_CPU: 'cpu-feature.node.kubevirt.io/vmx',
|
||||||
VOLUME_ERROR: 'longhorn.io/volume-scheduling-error',
|
NODE_MANUFACTURER: 'manufacturer',
|
||||||
VOLUME_FOR_VM: 'harvesterhci.io/volumeForVirtualMachine',
|
NODE_MODEL: 'model',
|
||||||
KVM_AMD_CPU: 'cpu-feature.node.kubevirt.io/svm',
|
NODE_SERIAL_NUMBER: 'serialNumber',
|
||||||
KVM_INTEL_CPU: 'cpu-feature.node.kubevirt.io/vmx',
|
VM_INSUFFICIENT: 'harvesterhci.io/insufficient-resource-quota',
|
||||||
NODE_MANUFACTURER: 'manufacturer',
|
NODE_NTP_SYNC_STATUS: 'node.harvesterhci.io/ntp-service',
|
||||||
NODE_MODEL: 'model',
|
PARENT_SRIOV: 'harvesterhci.io/parent-sriov-network-device',
|
||||||
NODE_SERIAL_NUMBER: 'serialNumber',
|
PARENT_SRIOV_GPU: 'harvesterhci.io/parentSRIOVGPUDevice',
|
||||||
VM_INSUFFICIENT: 'harvesterhci.io/insufficient-resource-quota',
|
VM_MAINTENANCE_MODE_STRATEGY: 'harvesterhci.io/maintain-mode-strategy',
|
||||||
NODE_NTP_SYNC_STATUS: 'node.harvesterhci.io/ntp-service',
|
NODE_CPU_MANAGER_UPDATE_STATUS: 'harvesterhci.io/cpu-manager-update-status',
|
||||||
PARENT_SRIOV: 'harvesterhci.io/parent-sriov-network-device',
|
CPU_MANAGER: 'cpumanager',
|
||||||
PARENT_SRIOV_GPU: 'harvesterhci.io/parentSRIOVGPUDevice',
|
VM_DEVICE_ALLOCATION_DETAILS: 'harvesterhci.io/deviceAllocationDetails',
|
||||||
VM_MAINTENANCE_MODE_STRATEGY: 'harvesterhci.io/maintain-mode-strategy',
|
SVM_BACKUP_ID: 'harvesterhci.io/svmbackupId',
|
||||||
NODE_CPU_MANAGER_UPDATE_STATUS: 'harvesterhci.io/cpu-manager-update-status',
|
DISABLE_LONGHORN_V2_ENGINE: 'node.longhorn.io/disable-v2-data-engine',
|
||||||
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',
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
export const CLUSTER_NETWORK = 'clusterNetwork';
|
export const CLUSTER_NETWORK = 'clusterNetwork';
|
||||||
export const VPC = 'vpc';
|
|
||||||
|
|||||||
@@ -28,17 +28,12 @@ export const HCI_SETTING = {
|
|||||||
RELEASE_DOWNLOAD_URL: 'release-download-url',
|
RELEASE_DOWNLOAD_URL: 'release-download-url',
|
||||||
CCM_CSI_VERSION: 'harvester-csi-ccm-versions',
|
CCM_CSI_VERSION: 'harvester-csi-ccm-versions',
|
||||||
CSI_DRIVER_CONFIG: 'csi-driver-config',
|
CSI_DRIVER_CONFIG: 'csi-driver-config',
|
||||||
CSI_ONLINE_EXPAND_VALIDATION: 'csi-online-expand-validation',
|
|
||||||
VM_TERMINATION_PERIOD: 'default-vm-termination-grace-period-seconds',
|
VM_TERMINATION_PERIOD: 'default-vm-termination-grace-period-seconds',
|
||||||
NTP_SERVERS: 'ntp-servers',
|
NTP_SERVERS: 'ntp-servers',
|
||||||
AUTO_ROTATE_RKE2_CERTS: 'auto-rotate-rke2-certs',
|
AUTO_ROTATE_RKE2_CERTS: 'auto-rotate-rke2-certs',
|
||||||
KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES: 'kubeconfig-default-token-ttl-minutes',
|
KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES: 'kubeconfig-default-token-ttl-minutes',
|
||||||
LONGHORN_V2_DATA_ENGINE_ENABLED: 'longhorn-v2-data-engine-enabled',
|
LONGHORN_V2_DATA_ENGINE_ENABLED: 'longhorn-v2-data-engine-enabled',
|
||||||
ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO: 'additional-guest-memory-overhead-ratio',
|
ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO: 'additional-guest-memory-overhead-ratio',
|
||||||
UPGRADE_CONFIG: 'upgrade-config',
|
|
||||||
VM_MIGRATION_NETWORK: 'vm-migration-network',
|
|
||||||
RANCHER_CLUSTER: 'rancher-cluster',
|
|
||||||
MAX_HOTPLUG_RATIO: 'max-hotplug-ratio'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HCI_ALLOWED_SETTINGS = {
|
export const HCI_ALLOWED_SETTINGS = {
|
||||||
@@ -57,25 +52,20 @@ export const HCI_ALLOWED_SETTINGS = {
|
|||||||
from: 'import',
|
from: 'import',
|
||||||
featureFlag: 'autoRotateRke2CertsSetting'
|
featureFlag: 'autoRotateRke2CertsSetting'
|
||||||
},
|
},
|
||||||
[HCI_SETTING.CSI_DRIVER_CONFIG]: { kind: 'json', from: 'import' },
|
[HCI_SETTING.CSI_DRIVER_CONFIG]: { kind: 'json', from: 'import' },
|
||||||
[HCI_SETTING.CSI_ONLINE_EXPAND_VALIDATION]: {
|
[HCI_SETTING.SERVER_VERSION]: { readOnly: true },
|
||||||
kind: 'json', from: 'import', featureFlag: 'csiOnlineExpandValidation'
|
[HCI_SETTING.UPGRADE_CHECKER_ENABLED]: { kind: 'boolean' },
|
||||||
},
|
[HCI_SETTING.UPGRADE_CHECKER_URL]: { kind: 'url' },
|
||||||
[HCI_SETTING.SERVER_VERSION]: { readOnly: true },
|
[HCI_SETTING.HTTP_PROXY]: { kind: 'json', from: 'import' },
|
||||||
[HCI_SETTING.UPGRADE_CHECKER_ENABLED]: { kind: 'boolean' },
|
[HCI_SETTING.ADDITIONAL_CA]: {
|
||||||
[HCI_SETTING.UPGRADE_CHECKER_URL]: { kind: 'url' },
|
|
||||||
[HCI_SETTING.HTTP_PROXY]: { kind: 'json', from: 'import' },
|
|
||||||
[HCI_SETTING.ADDITIONAL_CA]: {
|
|
||||||
kind: 'multiline', canReset: true, from: 'import'
|
kind: 'multiline', canReset: true, from: 'import'
|
||||||
},
|
},
|
||||||
[HCI_SETTING.OVERCOMMIT_CONFIG]: { kind: 'json', from: 'import' },
|
[HCI_SETTING.OVERCOMMIT_CONFIG]: { kind: 'json', from: 'import' },
|
||||||
[HCI_SETTING.SUPPORT_BUNDLE_TIMEOUT]: { kind: 'number' },
|
[HCI_SETTING.SUPPORT_BUNDLE_TIMEOUT]: {},
|
||||||
[HCI_SETTING.SUPPORT_BUNDLE_EXPIRATION]: { kind: 'number' },
|
[HCI_SETTING.SUPPORT_BUNDLE_EXPIRATION]: {},
|
||||||
[HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT]: { kind: 'number', featureFlag: 'supportBundleNodeCollectionTimeoutSetting' },
|
[HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT]: { featureFlag: 'supportBundleNodeCollectionTimeoutSetting' },
|
||||||
[HCI_SETTING.SUPPORT_BUNDLE_IMAGE]: { kind: 'json', from: 'import' },
|
[HCI_SETTING.SUPPORT_BUNDLE_IMAGE]: { kind: 'json', from: 'import' },
|
||||||
[HCI_SETTING.STORAGE_NETWORK]: {
|
[HCI_SETTING.STORAGE_NETWORK]: { kind: 'custom', from: 'import' },
|
||||||
kind: 'custom', from: 'import', canReset: true
|
|
||||||
},
|
|
||||||
[HCI_SETTING.VM_FORCE_RESET_POLICY]: { kind: 'json', from: 'import' },
|
[HCI_SETTING.VM_FORCE_RESET_POLICY]: { kind: 'json', from: 'import' },
|
||||||
[HCI_SETTING.SSL_CERTIFICATES]: { kind: 'json', from: 'import' },
|
[HCI_SETTING.SSL_CERTIFICATES]: { kind: 'json', from: 'import' },
|
||||||
[HCI_SETTING.SSL_PARAMETERS]: {
|
[HCI_SETTING.SSL_PARAMETERS]: {
|
||||||
@@ -97,26 +87,13 @@ export const HCI_ALLOWED_SETTINGS = {
|
|||||||
[HCI_SETTING.NTP_SERVERS]: {
|
[HCI_SETTING.NTP_SERVERS]: {
|
||||||
kind: 'json', from: 'import', canReset: true
|
kind: 'json', from: 'import', canReset: true
|
||||||
},
|
},
|
||||||
[HCI_SETTING.KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES]: { kind: 'number', featureFlag: 'kubeconfigDefaultTokenTTLMinutesSetting' },
|
[HCI_SETTING.KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES]: { featureFlag: 'kubeconfigDefaultTokenTTLMinutesSetting' },
|
||||||
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_ENABLED]: {
|
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_ENABLED]: {
|
||||||
kind: 'boolean',
|
kind: 'boolean',
|
||||||
experimental: true,
|
experimental: true,
|
||||||
featureFlag: 'longhornV2LVMSupport'
|
featureFlag: 'longhornV2LVMSupport'
|
||||||
},
|
},
|
||||||
[HCI_SETTING.ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO]: { kind: 'string', from: 'import' },
|
[HCI_SETTING.ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO]: { kind: 'string', from: 'import' },
|
||||||
[HCI_SETTING.UPGRADE_CONFIG]: {
|
|
||||||
kind: 'json',
|
|
||||||
from: 'import',
|
|
||||||
featureFlag: 'upgradeConfigSetting',
|
|
||||||
docPath: 'UPGRADE_CONFIG_URL'
|
|
||||||
},
|
|
||||||
[HCI_SETTING.RANCHER_CLUSTER]: {
|
|
||||||
kind: 'custom', from: 'import', canReset: true, featureFlag: 'rancherClusterSetting'
|
|
||||||
},
|
|
||||||
[HCI_SETTING.MAX_HOTPLUG_RATIO]: { kind: 'number', featureFlag: 'cpuMemoryHotplug' },
|
|
||||||
[HCI_SETTING.VM_MIGRATION_NETWORK]: {
|
|
||||||
kind: 'json', from: 'import', canReset: true, featureFlag: 'vmNetworkMigration',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = {
|
export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Harvester
|
* Harvester
|
||||||
*/
|
*/
|
||||||
import { DESCRIPTION } from '@shell/config/table-headers';
|
|
||||||
|
|
||||||
// image
|
// image
|
||||||
export const IMAGE_DOWNLOAD_SIZE = {
|
export const IMAGE_DOWNLOAD_SIZE = {
|
||||||
@@ -89,45 +88,3 @@ export const MACHINE_POOLS = {
|
|||||||
align: 'center',
|
align: 'center',
|
||||||
width: 100,
|
width: 100,
|
||||||
};
|
};
|
||||||
|
|
||||||
// The STORAGE_CLASS column in VM image list page
|
|
||||||
export const IMAGE_STORAGE_CLASS = {
|
|
||||||
name: 'imageStorageClass',
|
|
||||||
labelKey: 'harvester.tableHeaders.storageClass',
|
|
||||||
sort: 'imageStorageClass',
|
|
||||||
value: 'imageStorageClass',
|
|
||||||
align: 'left',
|
|
||||||
width: 150,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const HARVESTER_DESCRIPTION = {
|
|
||||||
...DESCRIPTION,
|
|
||||||
width: 150,
|
|
||||||
};
|
|
||||||
|
|
||||||
// The CIDR_BLOCK column in VPC list page
|
|
||||||
export const CIDR_BLOCK = {
|
|
||||||
name: 'cidrBlock',
|
|
||||||
labelKey: 'harvester.subnet.cidrBlock.label',
|
|
||||||
sort: 'cidrBlock',
|
|
||||||
value: 'spec.cidrBlock',
|
|
||||||
align: 'left',
|
|
||||||
};
|
|
||||||
|
|
||||||
// The Protocol column in VPC list page
|
|
||||||
export const PROTOCOL = {
|
|
||||||
name: 'protocol',
|
|
||||||
labelKey: 'harvester.subnet.protocol.label',
|
|
||||||
sort: 'protocol',
|
|
||||||
value: 'spec.protocol',
|
|
||||||
align: 'left',
|
|
||||||
};
|
|
||||||
|
|
||||||
// The Provider column in VPC list page
|
|
||||||
export const PROVIDER = {
|
|
||||||
name: 'provider',
|
|
||||||
labelKey: 'harvester.subnet.provider.label',
|
|
||||||
sort: 'provider',
|
|
||||||
value: 'spec.provider',
|
|
||||||
align: 'left',
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -2,24 +2,3 @@ export const BACKUP_TYPE = {
|
|||||||
BACKUP: 'backup',
|
BACKUP: 'backup',
|
||||||
SNAPSHOT: 'snapshot'
|
SNAPSHOT: 'snapshot'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NETWORK_TYPE = {
|
|
||||||
L2VLAN: 'L2VlanNetwork',
|
|
||||||
UNTAGGED: 'UntaggedNetwork',
|
|
||||||
OVERLAY: 'OverlayNetwork',
|
|
||||||
};
|
|
||||||
|
|
||||||
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',
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { formatSi, exponentNeeded, UNITS } from '@shell/utils/units';
|
|||||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
import { LONGHORN, METRIC } from '@shell/config/types';
|
import { LONGHORN, METRIC } from '@shell/config/types';
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import { UNIT_SUFFIX } from '../../utils/unit';
|
|
||||||
import HarvesterCPUUsed from '../../formatters/HarvesterCPUUsed';
|
import HarvesterCPUUsed from '../../formatters/HarvesterCPUUsed';
|
||||||
import HarvesterMemoryUsed from '../../formatters/HarvesterMemoryUsed';
|
import HarvesterMemoryUsed from '../../formatters/HarvesterMemoryUsed';
|
||||||
import HarvesterStorageUsed from '../../formatters/HarvesterStorageUsed';
|
import HarvesterStorageUsed from '../../formatters/HarvesterStorageUsed';
|
||||||
@@ -122,7 +121,7 @@ export default {
|
|||||||
memoryUnits() {
|
memoryUnits() {
|
||||||
const exponent = exponentNeeded(this.memoryTotal, 1024);
|
const exponent = exponentNeeded(this.memoryTotal, 1024);
|
||||||
|
|
||||||
return `${ UNITS[exponent] }${ UNIT_SUFFIX }`;
|
return `${ UNITS[exponent] }iB`;
|
||||||
},
|
},
|
||||||
|
|
||||||
nodeType() {
|
nodeType() {
|
||||||
|
|||||||
@@ -47,26 +47,20 @@ export default {
|
|||||||
STATE,
|
STATE,
|
||||||
NAME,
|
NAME,
|
||||||
{
|
{
|
||||||
name: 'CPU',
|
name: 'vmCPU',
|
||||||
label: 'CPU',
|
labelKey: 'tableHeaders.cpu',
|
||||||
sort: ['displayCPU'],
|
search: false,
|
||||||
value: 'displayCPU',
|
sort: ['spec.template.spec.domain.cpu.cores'],
|
||||||
align: 'center',
|
value: 'spec.template.spec.domain.cpu.cores',
|
||||||
dashIfEmpty: true,
|
width: 120
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Memory',
|
name: 'vmRAM',
|
||||||
value: 'displayMemory',
|
labelKey: 'glance.memory',
|
||||||
sort: ['memorySort'],
|
search: false,
|
||||||
align: 'center',
|
sort: ['memorySort'],
|
||||||
labelKey: 'tableHeaders.memory',
|
value: 'spec.template.spec.domain.resources.limits.memory',
|
||||||
formatter: 'Si',
|
width: 120
|
||||||
formatterOpts: {
|
|
||||||
opts: {
|
|
||||||
increment: 1024, addSuffix: true, maxExponent: 3, minExponent: 3, suffix: 'i',
|
|
||||||
},
|
|
||||||
needParseSi: true
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'ip',
|
name: 'ip',
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import ArrayListGrouped from '@shell/components/form/ArrayListGrouped';
|
|||||||
import Loading from '@shell/components/Loading.vue';
|
import Loading from '@shell/components/Loading.vue';
|
||||||
import SortableTable from '@shell/components/SortableTable';
|
import SortableTable from '@shell/components/SortableTable';
|
||||||
import Banner from '@components/Banner/Banner.vue';
|
import Banner from '@components/Banner/Banner.vue';
|
||||||
import { UNIT_SUFFIX } from '../../utils/unit';
|
|
||||||
import metricPoller from '@shell/mixins/metric-poller';
|
import metricPoller from '@shell/mixins/metric-poller';
|
||||||
import {
|
import {
|
||||||
METRIC, NODE, LONGHORN, POD, EVENT
|
METRIC, NODE, LONGHORN, POD, EVENT
|
||||||
@@ -178,7 +178,7 @@ export default {
|
|||||||
minExponent: 3,
|
minExponent: 3,
|
||||||
maxExponent: 3,
|
maxExponent: 3,
|
||||||
maxPrecision: 2,
|
maxPrecision: 2,
|
||||||
suffix: UNIT_SUFFIX,
|
suffix: 'iB',
|
||||||
};
|
};
|
||||||
|
|
||||||
const longhornDisks = Object.keys(diskStatus).map((key) => {
|
const longhornDisks = Object.keys(diskStatus).map((key) => {
|
||||||
|
|||||||
@@ -125,7 +125,6 @@ export default {
|
|||||||
:resource="value"
|
:resource="value"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:apply-hooks="applyHooks"
|
:apply-hooks="applyHooks"
|
||||||
@error="e=>errors=e"
|
|
||||||
>
|
>
|
||||||
<Tabbed
|
<Tabbed
|
||||||
v-if="spec"
|
v-if="spec"
|
||||||
@@ -172,9 +171,6 @@ export default {
|
|||||||
:cpu="cpu"
|
:cpu="cpu"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:memory="memory"
|
:memory="memory"
|
||||||
:max-cpu="maxCpu"
|
|
||||||
:max-memory="maxMemory"
|
|
||||||
:enable-hot-plug="cpuMemoryHotplugEnabled"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|||||||
@@ -118,10 +118,6 @@ export default {
|
|||||||
imageName() {
|
imageName() {
|
||||||
return this.value?.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_NAME] || '-';
|
return this.value?.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_NAME] || '-';
|
||||||
},
|
},
|
||||||
|
|
||||||
sourceType() {
|
|
||||||
return this.value?.spec?.sourceType;
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -256,16 +252,6 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col span-12">
|
|
||||||
<LabelValue
|
|
||||||
:name="t('harvester.image.source')"
|
|
||||||
:value="sourceType"
|
|
||||||
class="mb-20"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="errorMessage !== '-'"
|
v-if="errorMessage !== '-'"
|
||||||
class="row"
|
class="row"
|
||||||
|
|||||||
@@ -125,7 +125,6 @@ export default {
|
|||||||
:resource="value"
|
:resource="value"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:apply-hooks="applyHooks"
|
:apply-hooks="applyHooks"
|
||||||
@error="e=>errors=e"
|
|
||||||
>
|
>
|
||||||
<Tabbed
|
<Tabbed
|
||||||
v-if="spec"
|
v-if="spec"
|
||||||
@@ -172,9 +171,6 @@ export default {
|
|||||||
:cpu="cpu"
|
:cpu="cpu"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:memory="memory"
|
:memory="memory"
|
||||||
:max-cpu="maxCpu"
|
|
||||||
:max-memory="maxMemory"
|
|
||||||
:enable-hot-plug="cpuMemoryHotplugEnabled"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="row mb-10">
|
<div class="row mb-10">
|
||||||
|
|||||||
+2
-3
@@ -5,7 +5,6 @@ import CreateEditView from '@shell/mixins/create-edit-view';
|
|||||||
import HarvesterIpAddress from '../../../formatters/HarvesterIpAddress';
|
import HarvesterIpAddress from '../../../formatters/HarvesterIpAddress';
|
||||||
import VMConsoleBar from '../../../components/VMConsoleBar';
|
import VMConsoleBar from '../../../components/VMConsoleBar';
|
||||||
import { HCI } from '../../../types';
|
import { HCI } from '../../../types';
|
||||||
import { getVmCPUMemoryValues } from '../../../utils/cpuMemory';
|
|
||||||
|
|
||||||
const UNDEFINED = 'n/a';
|
const UNDEFINED = 'n/a';
|
||||||
|
|
||||||
@@ -92,9 +91,9 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
flavor() {
|
flavor() {
|
||||||
const { cpu, memory } = getVmCPUMemoryValues(this.value);
|
const domain = this.value?.spec?.template?.spec?.domain;
|
||||||
|
|
||||||
return `${ cpu } vCPU , ${ memory } ${ this.t('harvester.virtualMachine.input.memory') }`;
|
return `${ domain.cpu?.cores } vCPU , ${ domain.resources?.limits?.memory } ${ this.t('harvester.virtualMachine.input.memory') }`;
|
||||||
},
|
},
|
||||||
|
|
||||||
kernelRelease() {
|
kernelRelease() {
|
||||||
|
|||||||
-25
@@ -17,13 +17,6 @@ export default {
|
|||||||
default: () => {
|
default: () => {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
},
|
|
||||||
vmimResource: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
default: () => {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -32,12 +25,6 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
liveMigrationProgressEnabled() {
|
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('liveMigrationProgress');
|
|
||||||
},
|
|
||||||
migrationPhase() {
|
|
||||||
return this.vmimResource?.status?.phase || 'N/A';
|
|
||||||
},
|
|
||||||
migrationState() {
|
migrationState() {
|
||||||
return this.localResource?.status?.migrationState;
|
return this.localResource?.status?.migrationState;
|
||||||
},
|
},
|
||||||
@@ -71,18 +58,6 @@ export default {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div
|
|
||||||
v-if="liveMigrationProgressEnabled"
|
|
||||||
class="row mb-20"
|
|
||||||
>
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabelValue
|
|
||||||
:name="t('harvester.virtualMachine.detail.details.phase')"
|
|
||||||
:value="migrationPhase"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-20">
|
<div class="row mb-20">
|
||||||
<div class="col span-6">
|
<div class="col span-6">
|
||||||
<LabelValue
|
<LabelValue
|
||||||
|
|||||||
@@ -27,8 +27,6 @@ import { formatSi } from '@shell/utils/units';
|
|||||||
|
|
||||||
const VM_METRICS_DETAIL_URL = '/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/d/harvester-vm-detail-1/vm-info-detail?orgId=1';
|
const VM_METRICS_DETAIL_URL = '/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/d/harvester-vm-detail-1/vm-info-detail?orgId=1';
|
||||||
|
|
||||||
const VM_MIGRATION_DETAIL_URL = '/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/d/harvester-vm-migration-details-1/harvester-vm-migration-details?orgId=1';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'VMIDetailsPage',
|
name: 'VMIDetailsPage',
|
||||||
|
|
||||||
@@ -65,7 +63,6 @@ export default {
|
|||||||
hasResourceQuotaSchema: false,
|
hasResourceQuotaSchema: false,
|
||||||
switchToCloud: false,
|
switchToCloud: false,
|
||||||
VM_METRICS_DETAIL_URL,
|
VM_METRICS_DETAIL_URL,
|
||||||
VM_MIGRATION_DETAIL_URL,
|
|
||||||
showVmMetrics: false,
|
showVmMetrics: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -81,7 +78,6 @@ export default {
|
|||||||
events: this.$store.dispatch(`${ inStore }/findAll`, { type: EVENT }),
|
events: this.$store.dispatch(`${ inStore }/findAll`, { type: EVENT }),
|
||||||
allSSHs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SSH }),
|
allSSHs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SSH }),
|
||||||
vmis: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMI }),
|
vmis: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMI }),
|
||||||
vmims: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMIM }),
|
|
||||||
restore: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.RESTORE }),
|
restore: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.RESTORE }),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -135,39 +131,6 @@ export default {
|
|||||||
return this.$store.getters[`${ inStore }/all`](EVENT);
|
return this.$store.getters[`${ inStore }/all`](EVENT);
|
||||||
},
|
},
|
||||||
|
|
||||||
vmim() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
const vmimList = this.$store.getters[`${ inStore }/all`](HCI.VMIM) || [];
|
|
||||||
|
|
||||||
const vmiName = this.vmi?.name || '';
|
|
||||||
|
|
||||||
// filter the corresponding vmims by vmim.spec.vmiName and find the latest one by creationTimestamp
|
|
||||||
const vmim = vmimList.filter((VMIM) => VMIM?.spec?.vmiName === vmiName).sort((a, b) => {
|
|
||||||
if (a?.metadata?.creationTimestamp > b?.metadata?.creationTimestamp) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
return vmim.length > 0 && vmim[0] ? vmim[0] : null;
|
|
||||||
},
|
|
||||||
|
|
||||||
migrationEvents() {
|
|
||||||
const migrationVMName = this.vmim?.metadata.name || '';
|
|
||||||
|
|
||||||
if (migrationVMName === '') {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.allEvents.filter((e) => {
|
|
||||||
const { creationTimestamp } = this.value?.metadata || {};
|
|
||||||
const involvedName = e?.involvedObject?.name;
|
|
||||||
|
|
||||||
return involvedName === migrationVMName && e.firstTimestamp >= creationTimestamp;
|
|
||||||
}).sort((a, b) => a.lastTimestamp > b.lastTimestamp);
|
|
||||||
},
|
|
||||||
|
|
||||||
events() {
|
events() {
|
||||||
return this.allEvents.filter((e) => {
|
return this.allEvents.filter((e) => {
|
||||||
const { name, creationTimestamp } = this.value?.metadata || {};
|
const { name, creationTimestamp } = this.value?.metadata || {};
|
||||||
@@ -175,6 +138,7 @@ export default {
|
|||||||
const pvcName = this.value.persistentVolumeClaimName || [];
|
const pvcName = this.value.persistentVolumeClaimName || [];
|
||||||
|
|
||||||
const involvedName = e?.involvedObject?.name;
|
const involvedName = e?.involvedObject?.name;
|
||||||
|
|
||||||
const matchPVC = pvcName.find((name) => name === involvedName);
|
const matchPVC = pvcName.find((name) => name === involvedName);
|
||||||
|
|
||||||
return (involvedName === name || involvedName === podName || matchPVC) && e.firstTimestamp >= creationTimestamp;
|
return (involvedName === name || involvedName === podName || matchPVC) && e.firstTimestamp >= creationTimestamp;
|
||||||
@@ -193,10 +157,6 @@ export default {
|
|||||||
vm: this.value.name
|
vm: this.value.name
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
liveMigrationProgressEnabled() {
|
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('liveMigrationProgress');
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@@ -386,20 +346,6 @@ export default {
|
|||||||
<Migration
|
<Migration
|
||||||
:value="value"
|
:value="value"
|
||||||
:vmi-resource="vmi"
|
:vmi-resource="vmi"
|
||||||
:vmim-resource="vmim"
|
|
||||||
/>
|
|
||||||
<DashboardMetrics
|
|
||||||
v-if="showVmMetrics && liveMigrationProgressEnabled"
|
|
||||||
:detail-url="VM_MIGRATION_DETAIL_URL"
|
|
||||||
graph-height="640px"
|
|
||||||
:has-summary-and-detail="false"
|
|
||||||
:vars="graphVars"
|
|
||||||
class="mb-30"
|
|
||||||
/>
|
|
||||||
<Events
|
|
||||||
v-if="liveMigrationProgressEnabled"
|
|
||||||
:resource="vmi"
|
|
||||||
:events="migrationEvents"
|
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
<script>
|
<script>
|
||||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { runStrategies as runStrategyOptions } from '../config/harvester-map';
|
|
||||||
import { Card } from '@components/Card';
|
import { Card } from '@components/Card';
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import { Checkbox } from '@components/Form/Checkbox';
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
import AsyncButton from '@shell/components/AsyncButton';
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CloneVMModal',
|
name: 'CloneVMModal',
|
||||||
@@ -15,7 +14,7 @@ export default {
|
|||||||
emits: ['close'],
|
emits: ['close'],
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
AsyncButton, Banner, Checkbox, Card, LabeledInput, LabeledSelect
|
AsyncButton, Banner, Checkbox, Card, LabeledInput
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
@@ -27,11 +26,9 @@ export default {
|
|||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
name: '',
|
name: '',
|
||||||
cloneData: true,
|
cloneData: true,
|
||||||
errors: [],
|
errors: []
|
||||||
runStrategy: runStrategyOptions[1],
|
|
||||||
runStrategyOptions
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -41,9 +38,6 @@ export default {
|
|||||||
actionResource() {
|
actionResource() {
|
||||||
return this.resources[0];
|
return this.resources[0];
|
||||||
},
|
},
|
||||||
vmCloneRunStrategyEnabled() {
|
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('vmCloneRunStrategy');
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@@ -64,8 +58,7 @@ export default {
|
|||||||
|
|
||||||
// deep clone
|
// deep clone
|
||||||
try {
|
try {
|
||||||
const payload = this.vmCloneRunStrategyEnabled ? { targetVm: this.name, runStrategy: this.runStrategy } : { targetVm: this.name };
|
const res = await this.actionResource.doAction('clone', { targetVm: this.name }, {}, false);
|
||||||
const res = await this.actionResource.doAction('clone', payload, {}, false);
|
|
||||||
|
|
||||||
if (res._status === 200 || res._status === 204) {
|
if (res._status === 200 || res._status === 204) {
|
||||||
this.$store.dispatch('growl/success', {
|
this.$store.dispatch('growl/success', {
|
||||||
@@ -113,19 +106,6 @@ export default {
|
|||||||
:label="t('harvester.modal.cloneVM.name')"
|
:label="t('harvester.modal.cloneVM.name')"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<LabeledSelect
|
|
||||||
v-if="vmCloneRunStrategyEnabled"
|
|
||||||
v-model:value="runStrategy"
|
|
||||||
label-key="harvester.virtualMachine.runStrategy"
|
|
||||||
:options="runStrategyOptions"
|
|
||||||
:mode="mode"
|
|
||||||
/>
|
|
||||||
<Banner
|
|
||||||
v-for="(err, i) in errors"
|
|
||||||
:key="i"
|
|
||||||
color="error"
|
|
||||||
:label="err"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template
|
<template
|
||||||
@@ -147,6 +127,11 @@ export default {
|
|||||||
@click="create"
|
@click="create"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Banner
|
||||||
|
v-for="(err, i) in errors"
|
||||||
|
:key="i"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</Card>
|
</Card>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,175 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { mapState } from 'vuex';
|
|
||||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
|
||||||
import { alternateLabel } from '@shell/utils/platform';
|
|
||||||
import AsyncButton from '@shell/components/AsyncButton';
|
|
||||||
import { Banner } from '@components/Banner';
|
|
||||||
import { Card } from '@components/Card';
|
|
||||||
import { escapeHtml } from '@shell/utils/string';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @name ConfirmExecutionDialog
|
|
||||||
* @description Dialog component to confirm the related resources before executing the action.
|
|
||||||
*/
|
|
||||||
export default {
|
|
||||||
name: 'ConfirmExecutionDialog',
|
|
||||||
|
|
||||||
emits: ['close'],
|
|
||||||
|
|
||||||
components: {
|
|
||||||
AsyncButton,
|
|
||||||
Banner,
|
|
||||||
Card,
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
|
||||||
/**
|
|
||||||
* @property resources to be deleted.
|
|
||||||
* @type {Resource[]} Array of the resource model's instance
|
|
||||||
*/
|
|
||||||
resources: {
|
|
||||||
type: Array,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return { errors: [] };
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
...mapState('action-menu', ['modalData']),
|
|
||||||
|
|
||||||
warningMessageKey() {
|
|
||||||
return this.modalData.warningMessageKey;
|
|
||||||
},
|
|
||||||
|
|
||||||
names() {
|
|
||||||
return this.resources.map((obj) => obj.nameDisplay).slice(0, 5);
|
|
||||||
},
|
|
||||||
|
|
||||||
resourceNames() {
|
|
||||||
return this.names.reduce((res, name, i) => {
|
|
||||||
if (i >= 5) {
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
res += `<b>${ escapeHtml(name) }</b>`;
|
|
||||||
if (i === this.names.length - 1) {
|
|
||||||
res += this.plusMore;
|
|
||||||
} else {
|
|
||||||
res += i === this.resources.length - 2 ? ' and ' : ', ';
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}, '');
|
|
||||||
},
|
|
||||||
|
|
||||||
plusMore() {
|
|
||||||
const remaining = this.resources.length - this.names.length;
|
|
||||||
|
|
||||||
return this.t('dialog.confirmExecution.andOthers', { count: remaining }, true);
|
|
||||||
},
|
|
||||||
|
|
||||||
type() {
|
|
||||||
const types = new Set(this.resources.reduce((array, each) => {
|
|
||||||
array.push(each.type);
|
|
||||||
|
|
||||||
return array;
|
|
||||||
}, []));
|
|
||||||
|
|
||||||
if (types.size > 1) {
|
|
||||||
return this.t('generic.resource', { count: this.resources.length });
|
|
||||||
}
|
|
||||||
|
|
||||||
const schema = this.resources[0]?.schema;
|
|
||||||
|
|
||||||
if ( !schema ) {
|
|
||||||
return `resource${ this.resources.length === 1 ? '' : 's' }`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.$store.getters['type-map/labelFor'](schema, this.resources.length);
|
|
||||||
},
|
|
||||||
|
|
||||||
protip() {
|
|
||||||
return this.t('dialog.confirmExecution.protip', { alternateLabel });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
escapeHtml,
|
|
||||||
|
|
||||||
close() {
|
|
||||||
this.errors = [];
|
|
||||||
this.$emit('close');
|
|
||||||
},
|
|
||||||
|
|
||||||
async apply(buttonDone) {
|
|
||||||
try {
|
|
||||||
for (const resource of this.resources) {
|
|
||||||
await resource.doActionGrowl(this.modalData.action, {});
|
|
||||||
}
|
|
||||||
buttonDone(true);
|
|
||||||
this.close();
|
|
||||||
} catch (e) {
|
|
||||||
this.errors = exceptionToErrorsArray(e);
|
|
||||||
buttonDone(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Card :show-highlight-border="false">
|
|
||||||
<template #title>
|
|
||||||
<h4 class="text-default-text">
|
|
||||||
{{ t('dialog.confirmExecution.title') }}
|
|
||||||
</h4>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #body>
|
|
||||||
<div class="pl-10 pr-10">
|
|
||||||
<span
|
|
||||||
v-clean-html="t(warningMessageKey, { type, names: resourceNames }, true)"
|
|
||||||
></span>
|
|
||||||
<div class="text-info mt-20">
|
|
||||||
{{ protip }}
|
|
||||||
</div>
|
|
||||||
<Banner
|
|
||||||
v-for="(error, i) in errors"
|
|
||||||
:key="i"
|
|
||||||
:label="error"
|
|
||||||
color="error"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #actions>
|
|
||||||
<div class="actions">
|
|
||||||
<button
|
|
||||||
class="btn role-secondary"
|
|
||||||
@click="close"
|
|
||||||
>
|
|
||||||
{{ t('generic.cancel') }}
|
|
||||||
</button>
|
|
||||||
<AsyncButton
|
|
||||||
mode="apply"
|
|
||||||
class="btn bg-primary ml-10"
|
|
||||||
:disabled="applyDisabled"
|
|
||||||
@click="apply"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang='scss' scoped>
|
|
||||||
.modal-container {
|
|
||||||
max-width: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
width: 100%;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -42,25 +42,8 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
...mapState('action-menu', ['modalData']),
|
...mapState('action-menu', ['modalData']),
|
||||||
|
|
||||||
title() {
|
warningMessageKey() {
|
||||||
return this.modalData.title || 'dialog.promptRemove.title';
|
return this.modalData.warningMessageKey;
|
||||||
},
|
|
||||||
|
|
||||||
formattedType() {
|
|
||||||
return this.type.toLowerCase();
|
|
||||||
},
|
|
||||||
|
|
||||||
warningMessage() {
|
|
||||||
if (this.modalData.warningMessage) return this.modalData.warningMessage;
|
|
||||||
|
|
||||||
const isPlural = this.type.endsWith('s');
|
|
||||||
const thisOrThese = isPlural ? 'these' : 'this';
|
|
||||||
const defaultMessage = this.t('dialog.promptRemove.warningMessage', {
|
|
||||||
type: this.formattedType,
|
|
||||||
thisOrThese,
|
|
||||||
});
|
|
||||||
|
|
||||||
return defaultMessage;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
names() {
|
names() {
|
||||||
@@ -87,12 +70,6 @@ export default {
|
|||||||
return this.resources[0].nameDisplay;
|
return this.resources[0].nameDisplay;
|
||||||
},
|
},
|
||||||
|
|
||||||
needConfirmation() {
|
|
||||||
const { needConfirmation = true } = this.modalData ;
|
|
||||||
|
|
||||||
return needConfirmation === true;
|
|
||||||
},
|
|
||||||
|
|
||||||
plusMore() {
|
plusMore() {
|
||||||
const remaining = this.resources.length - this.names.length;
|
const remaining = this.resources.length - this.names.length;
|
||||||
|
|
||||||
@@ -120,15 +97,11 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
deleteDisabled() {
|
deleteDisabled() {
|
||||||
if (!this.needConfirmation) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.confirmName !== this.nameToMatch;
|
return this.confirmName !== this.nameToMatch;
|
||||||
},
|
},
|
||||||
|
|
||||||
protip() {
|
protip() {
|
||||||
return this.t('dialog.promptRemove.protip', { alternateLabel });
|
return this.t('promptRemove.protip', { alternateLabel });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -164,45 +137,35 @@ export default {
|
|||||||
>
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
<h4 class="text-default-text">
|
<h4 class="text-default-text">
|
||||||
{{ t(title, { type }, true) }}
|
{{ t('promptRemove.title') }}
|
||||||
</h4>
|
</h4>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #body>
|
<template #body>
|
||||||
<div class="pl-10 pr-10">
|
<div class="pl-10 pr-10">
|
||||||
<span
|
<span
|
||||||
v-clean-html="warningMessage"
|
v-clean-html="t(warningMessageKey, { type, names: resourceNames }, true)"
|
||||||
></span>
|
></span>
|
||||||
|
|
||||||
<div
|
<div class="mt-10 mb-10">
|
||||||
v-if="needConfirmation"
|
<span
|
||||||
class="mt-20"
|
v-clean-html="t('promptRemove.confirmName', { nameToMatch: escapeHtml(nameToMatch) }, true)"
|
||||||
>
|
></span>
|
||||||
<div class="mt-10 mb-10">
|
</div>
|
||||||
<span
|
<div class="mb-10">
|
||||||
v-clean-html="t('dialog.promptRemove.confirmName', {
|
<CopyToClipboardText :text="nameToMatch" />
|
||||||
type: formattedType,
|
</div>
|
||||||
nameToMatch: escapeHtml(nameToMatch)
|
<input
|
||||||
}, true)"
|
id="confirm"
|
||||||
></span>
|
v-model="confirmName"
|
||||||
</div>
|
type="text"
|
||||||
<div class="mb-10">
|
/>
|
||||||
<CopyToClipboardText :text="nameToMatch" />
|
<div class="text-info mt-20">
|
||||||
</div>
|
{{ protip }}
|
||||||
<input
|
|
||||||
id="confirm"
|
|
||||||
v-model="confirmName"
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
<div class="text-info mt-20">
|
|
||||||
{{ protip }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<Banner
|
<Banner
|
||||||
v-for="(error, i) in errors"
|
v-for="(error, i) in errors"
|
||||||
:key="i"
|
:key="i"
|
||||||
:label="error"
|
|
||||||
color="error"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -57,10 +57,6 @@ export default {
|
|||||||
if (!!pvc.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_ID]) {
|
if (!!pvc.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_ID]) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// we won't show golden image volume in the hot plug volume modal
|
|
||||||
if (pvc.isGoldenImageVolume) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return !pvc.attachVM;
|
return !pvc.attachVM;
|
||||||
})
|
})
|
||||||
@@ -141,12 +137,6 @@ export default {
|
|||||||
class="mt-20"
|
class="mt-20"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<Banner
|
|
||||||
v-for="(err, i) in errors"
|
|
||||||
:key="i"
|
|
||||||
:label="err"
|
|
||||||
color="error"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #actions>
|
<template #actions>
|
||||||
@@ -166,6 +156,11 @@ export default {
|
|||||||
@click="save"
|
@click="save"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Banner
|
||||||
|
v-for="(err, i) in errors"
|
||||||
|
:key="i"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -103,12 +103,6 @@ export default {
|
|||||||
:label="t('generic.name')"
|
:label="t('generic.name')"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<Banner
|
|
||||||
v-for="(err, i) in errors"
|
|
||||||
:key="i"
|
|
||||||
color="error"
|
|
||||||
:label="err"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #actions>
|
<template #actions>
|
||||||
@@ -127,6 +121,13 @@ export default {
|
|||||||
@click="save"
|
@click="save"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Banner
|
||||||
|
v-for="(err, i) in errors"
|
||||||
|
:key="i"
|
||||||
|
color="error"
|
||||||
|
:label="err"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Card>
|
</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"
|
v-model:value="description"
|
||||||
:label="t('harvester.modal.createTemplate.description')"
|
:label="t('harvester.modal.createTemplate.description')"
|
||||||
/>
|
/>
|
||||||
<Banner
|
|
||||||
v-for="(err, i) in errors"
|
|
||||||
:key="i"
|
|
||||||
color="error"
|
|
||||||
:label="err"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #actions>
|
<template #actions>
|
||||||
@@ -144,6 +138,11 @@ export default {
|
|||||||
@click="save"
|
@click="save"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Banner
|
||||||
|
v-for="(err, i) in errors"
|
||||||
|
:key="i"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -128,12 +128,6 @@ export default {
|
|||||||
<Banner color="warning">
|
<Banner color="warning">
|
||||||
<span>{{ t('harvester.modal.ejectCDROM.warnTip') }}</span>
|
<span>{{ t('harvester.modal.ejectCDROM.warnTip') }}</span>
|
||||||
</Banner>
|
</Banner>
|
||||||
<Banner
|
|
||||||
v-for="(err, i) in errors"
|
|
||||||
:key="i"
|
|
||||||
color="error"
|
|
||||||
:label="err"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #actions>
|
<template #actions>
|
||||||
@@ -153,6 +147,13 @@ export default {
|
|||||||
@click="remove"
|
@click="remove"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Banner
|
||||||
|
v-for="(err, i) in errors"
|
||||||
|
:key="i"
|
||||||
|
color="error"
|
||||||
|
:label="err"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import { LabeledInput } from '@components/Form/LabeledInput';
|
|||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
import { NAMESPACE, STORAGE_CLASS } from '@shell/config/types';
|
import { NAMESPACE, STORAGE_CLASS } from '@shell/config/types';
|
||||||
import { allHash } from '@shell/utils/promise';
|
import { allHash } from '@shell/utils/promise';
|
||||||
import { isInternalStorageClass } from '../utils/storage-class';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterExportImageDialog',
|
name: 'HarvesterExportImageDialog',
|
||||||
@@ -35,14 +34,9 @@ export default {
|
|||||||
|
|
||||||
await allHash(hash);
|
await allHash(hash);
|
||||||
|
|
||||||
const allStorages = this.allStorageClasses;
|
const defaultStorage = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS).find((s) => s.isDefault);
|
||||||
const defaultStorage = allStorages.find((s) => s.isDefault);
|
|
||||||
|
|
||||||
if (this.isLonghornV1Volume) {
|
this['storageClassName'] = defaultStorage?.metadata?.name || 'longhorn';
|
||||||
this['storageClassName'] = defaultStorage?.metadata?.name || 'longhorn';
|
|
||||||
} else {
|
|
||||||
this['storageClassName'] = this.nonLonghornV1StorageClasses[0]?.metadata?.name || '';
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
@@ -60,7 +54,7 @@ export default {
|
|||||||
...mapGetters({ t: 'i18n/t' }),
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
|
||||||
actionResource() {
|
actionResource() {
|
||||||
return this.resources[0] || {};
|
return this.resources[0];
|
||||||
},
|
},
|
||||||
|
|
||||||
namespaces() {
|
namespaces() {
|
||||||
@@ -79,48 +73,24 @@ export default {
|
|||||||
return out;
|
return out;
|
||||||
},
|
},
|
||||||
|
|
||||||
isLonghornV1Volume() {
|
|
||||||
return this.actionResource?.isLonghornV1 === true;
|
|
||||||
},
|
|
||||||
|
|
||||||
disableSave() {
|
disableSave() {
|
||||||
return !(this.name && this.namespace && this.storageClassName);
|
return !(this.name && this.namespace && this.storageClassName);
|
||||||
},
|
},
|
||||||
|
|
||||||
allStorageClasses() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
return this.$store.getters[`${ inStore }/all`](STORAGE_CLASS) || [];
|
|
||||||
},
|
|
||||||
|
|
||||||
nonLonghornV1StorageClasses() {
|
|
||||||
return this.allStorageClasses.filter((s) => s.isLonghornV1 === false) || [];
|
|
||||||
},
|
|
||||||
|
|
||||||
storageClassOptions() {
|
storageClassOptions() {
|
||||||
let storages = this.allStorageClasses;
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const storages = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS);
|
||||||
|
|
||||||
// Volume with non-longhorn v1 sc can't be exported to longhorn v1 sc image
|
const out = storages.filter((s) => !s.parameters?.backingImage).map((s) => {
|
||||||
if (!this.isLonghornV1Volume) {
|
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||||
storages = this.allStorageClasses.filter((s) => s.isLonghornV1 === false);
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = storages.filter((s) => !s.parameters?.backingImage).map((s) => {
|
|
||||||
let label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
|
||||||
const isInternal = isInternalStorageClass(s.metadata?.name);
|
|
||||||
|
|
||||||
if (isInternal) {
|
|
||||||
label += ` (${ this.t('harvester.storage.internal.label') })`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label,
|
label,
|
||||||
value: s.name,
|
value: s.name,
|
||||||
disabled: isInternal,
|
|
||||||
};
|
};
|
||||||
}) || [];
|
}) || [];
|
||||||
|
|
||||||
return options;
|
return out;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -194,12 +164,6 @@ export default {
|
|||||||
class="mt-20"
|
class="mt-20"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<Banner
|
|
||||||
v-for="(err, i) in errors"
|
|
||||||
:key="i"
|
|
||||||
color="error"
|
|
||||||
:label="err"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #actions>
|
<template #actions>
|
||||||
@@ -218,6 +182,11 @@ export default {
|
|||||||
@click="save"
|
@click="save"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Banner
|
||||||
|
v-for="(err, i) in errors"
|
||||||
|
:key="i"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Card>
|
</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')"
|
:placeholder="t('harvester.modal.migration.fields.nodeName.placeholder')"
|
||||||
:options="nodeNameList"
|
:options="nodeNameList"
|
||||||
/>
|
/>
|
||||||
<Banner
|
|
||||||
v-for="(err, i) in errors"
|
|
||||||
:key="i"
|
|
||||||
color="error"
|
|
||||||
:label="err"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template
|
<template
|
||||||
@@ -187,6 +181,12 @@ export default {
|
|||||||
@click="apply"
|
@click="apply"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<Banner
|
||||||
|
v-for="(err, i) in errors"
|
||||||
|
:key="i"
|
||||||
|
color="error"
|
||||||
|
:label="err"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</Card>
|
</Card>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { Banner } from '@components/Banner';
|
|||||||
import AsyncButton from '@shell/components/AsyncButton';
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
import UnitInput from '@shell/components/form/UnitInput';
|
import UnitInput from '@shell/components/form/UnitInput';
|
||||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||||
import { GIBIBYTE } from '../utils/unit';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterVMQuotaDialog',
|
name: 'HarvesterVMQuotaDialog',
|
||||||
@@ -30,7 +29,6 @@ export default {
|
|||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
GIBIBYTE,
|
|
||||||
totalSnapshotSize: '',
|
totalSnapshotSize: '',
|
||||||
errors: []
|
errors: []
|
||||||
};
|
};
|
||||||
@@ -46,6 +44,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.totalSnapshotSize = '';
|
this.totalSnapshotSize = '';
|
||||||
this.$emit('close');
|
this.$emit('close');
|
||||||
@@ -93,15 +92,9 @@ export default {
|
|||||||
:input-exponent="3"
|
:input-exponent="3"
|
||||||
:increment="1024"
|
:increment="1024"
|
||||||
:output-modifier="true"
|
:output-modifier="true"
|
||||||
:suffix="GIBIBYTE"
|
suffix="GiB"
|
||||||
class="mb-20"
|
class="mb-20"
|
||||||
/>
|
/>
|
||||||
<Banner
|
|
||||||
v-for="(err, i) in errors"
|
|
||||||
:key="i"
|
|
||||||
color="error"
|
|
||||||
:label="err"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #actions>
|
<template #actions>
|
||||||
@@ -115,6 +108,12 @@ export default {
|
|||||||
</button>
|
</button>
|
||||||
<AsyncButton @click="save" />
|
<AsyncButton @click="save" />
|
||||||
</div>
|
</div>
|
||||||
|
<Banner
|
||||||
|
v-for="(err, i) in errors"
|
||||||
|
:key="i"
|
||||||
|
color="error"
|
||||||
|
:label="err"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -136,12 +136,6 @@ export default {
|
|||||||
:options="backupOption"
|
:options="backupOption"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<Banner
|
|
||||||
v-for="(err, i) in errors"
|
|
||||||
:key="i"
|
|
||||||
color="error"
|
|
||||||
:label="err"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #actions>
|
<template #actions>
|
||||||
@@ -160,6 +154,11 @@ export default {
|
|||||||
@click="saveRestore"
|
@click="saveRestore"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Banner
|
||||||
|
v-for="(err, i) in errors"
|
||||||
|
:key="i"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script>
|
<script>
|
||||||
import { NAMESPACE } from '@shell/config/types';
|
|
||||||
import { randomStr } from '@shell/utils/string';
|
import { randomStr } from '@shell/utils/string';
|
||||||
import { exceptionToErrorsArray, stringify } from '@shell/utils/error';
|
import { exceptionToErrorsArray, stringify } from '@shell/utils/error';
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
@@ -7,15 +6,7 @@ import AsyncButton from '@shell/components/AsyncButton';
|
|||||||
import GraphCircle from '@shell/components/graph/Circle';
|
import GraphCircle from '@shell/components/graph/Circle';
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import AppModal from '@shell/components/AppModal';
|
import AppModal from '@shell/components/AppModal';
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
||||||
import UnitInput from '@shell/components/form/UnitInput';
|
|
||||||
import { HCI } from '../types';
|
import { HCI } from '../types';
|
||||||
import { HCI_SETTING } from '../config/settings';
|
|
||||||
import { DOC } from '../config/doc-links';
|
|
||||||
import { docLink } from '../utils/feature-flags';
|
|
||||||
|
|
||||||
const SELECT_ALL = 'select_all';
|
|
||||||
const UNSELECT_ALL = 'unselect_all';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SupportBundle',
|
name: 'SupportBundle',
|
||||||
@@ -26,36 +17,14 @@ export default {
|
|||||||
AsyncButton,
|
AsyncButton,
|
||||||
Banner,
|
Banner,
|
||||||
AppModal,
|
AppModal,
|
||||||
LabeledSelect,
|
|
||||||
UnitInput
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
await this.$store.dispatch('harvester/findAll', { type: NAMESPACE });
|
|
||||||
|
|
||||||
try {
|
|
||||||
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/namespaces?link=supportbundle');
|
|
||||||
const response = await this.$store.dispatch('harvester/request', { url });
|
|
||||||
|
|
||||||
this.defaultNamespaces = response.data || [];
|
|
||||||
} catch (error) {
|
|
||||||
this.defaultNamespaces = [];
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isOpen: false,
|
url: '',
|
||||||
errors: [],
|
description: '',
|
||||||
version: '',
|
errors: [],
|
||||||
clusterName: '',
|
isOpen: false,
|
||||||
url: '',
|
|
||||||
description: '',
|
|
||||||
namespaces: [],
|
|
||||||
defaultNamespaces: [],
|
|
||||||
timeout: '',
|
|
||||||
expiration: '',
|
|
||||||
nodeTimeout: '',
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -70,51 +39,23 @@ export default {
|
|||||||
|
|
||||||
percentage() {
|
percentage() {
|
||||||
return this.$store.getters['harvester-common/getBundlePercentage'];
|
return this.$store.getters['harvester-common/getBundlePercentage'];
|
||||||
},
|
}
|
||||||
|
|
||||||
availableNamespaces() {
|
|
||||||
const allNamespaces = this.$store.getters['harvester/all'](NAMESPACE).map((ns) => ns.id);
|
|
||||||
const defaultNamespacesIds = this.defaultNamespaces.map((ns) => ns.id);
|
|
||||||
|
|
||||||
return allNamespaces.filter((ns) => !defaultNamespacesIds.includes(ns) || this.namespaces.includes(ns));
|
|
||||||
},
|
|
||||||
|
|
||||||
namespaceOptions() {
|
|
||||||
if (this.availableNamespaces.length === 0) return [];
|
|
||||||
|
|
||||||
const allSelected = this.namespaces.length === this.availableNamespaces.length &&
|
|
||||||
this.availableNamespaces.every((ns) => this.namespaces.includes(ns));
|
|
||||||
|
|
||||||
const controlOption = allSelected ? { label: this.t('harvester.modal.bundle.namespaces.unselectAll'), value: UNSELECT_ALL } : { label: this.t('harvester.modal.bundle.namespaces.selectAll'), value: SELECT_ALL };
|
|
||||||
|
|
||||||
return [controlOption, ...this.availableNamespaces];
|
|
||||||
},
|
|
||||||
|
|
||||||
docLink() {
|
|
||||||
const version = this.$store.getters['harvester-common/getServerVersion']();
|
|
||||||
|
|
||||||
return docLink(DOC.SUPPORT_BUNDLE_NAMESPACES, version);
|
|
||||||
},
|
|
||||||
|
|
||||||
customSupportBundleFeatureEnabled() {
|
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('customSupportBundle');
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
isShowBundleModal: {
|
isShowBundleModal: {
|
||||||
immediate: true,
|
|
||||||
handler(show) {
|
handler(show) {
|
||||||
this.isOpen = show;
|
if (show) {
|
||||||
}
|
this.$nextTick(() => {
|
||||||
},
|
this.isOpen = true;
|
||||||
|
});
|
||||||
isOpen(newVal) {
|
} else {
|
||||||
if (newVal) {
|
this.isOpen = false;
|
||||||
this.loadDefaultSettings();
|
this.url = '';
|
||||||
} else {
|
this.description = '';
|
||||||
this.resetForm();
|
}
|
||||||
}
|
},
|
||||||
|
immediate: true
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -124,93 +65,33 @@ export default {
|
|||||||
close() {
|
close() {
|
||||||
this.isOpen = false;
|
this.isOpen = false;
|
||||||
this.$store.commit('harvester-common/toggleBundleModal', false);
|
this.$store.commit('harvester-common/toggleBundleModal', false);
|
||||||
},
|
this.backUpName = '';
|
||||||
|
|
||||||
loadDefaultSettings() {
|
|
||||||
const cluster = this.$store.getters['currentCluster'];
|
|
||||||
const versionSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SERVER_VERSION);
|
|
||||||
const namespacesSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_NAMESPACES);
|
|
||||||
const timeoutSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_TIMEOUT);
|
|
||||||
const expirationSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_EXPIRATION);
|
|
||||||
const nodeTimeoutSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT);
|
|
||||||
|
|
||||||
this.version = versionSetting?.currentVersion || '';
|
|
||||||
this.clusterName = cluster?.id || '';
|
|
||||||
this.namespaces = (namespacesSetting?.value ?? namespacesSetting?.default ?? '').split(',').map((ns) => ns.trim()).filter((ns) => ns);
|
|
||||||
this.timeout = timeoutSetting?.value ?? timeoutSetting?.default ?? '';
|
|
||||||
this.expiration = expirationSetting?.value ?? expirationSetting?.default ?? '';
|
|
||||||
this.nodeTimeout = nodeTimeoutSetting?.value ?? nodeTimeoutSetting?.default ?? '';
|
|
||||||
this.url = '';
|
|
||||||
this.description = '';
|
|
||||||
this.errors = [];
|
|
||||||
},
|
|
||||||
|
|
||||||
resetForm() {
|
|
||||||
this.url = '';
|
|
||||||
this.description = '';
|
|
||||||
this.namespaces = [];
|
|
||||||
this.timeout = '';
|
|
||||||
this.expiration = '';
|
|
||||||
this.nodeTimeout = '';
|
|
||||||
this.errors = [];
|
|
||||||
},
|
|
||||||
|
|
||||||
updateNamespaces(selected) {
|
|
||||||
if (selected.includes(SELECT_ALL)) {
|
|
||||||
this.namespaces = [...this.availableNamespaces];
|
|
||||||
} else if (selected.includes(UNSELECT_ALL)) {
|
|
||||||
this.namespaces = [];
|
|
||||||
} else {
|
|
||||||
this.namespaces = selected.filter((val) => val !== SELECT_ALL && val !== UNSELECT_ALL);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
updateNumberValue(field, value) {
|
|
||||||
if (value === '' || value === null || isNaN(value)) {
|
|
||||||
this[field] = '';
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const num = Number(value);
|
|
||||||
const isValid = Number.isInteger(num) && num >= 0;
|
|
||||||
|
|
||||||
this[field] = isValid ? String(num) : '';
|
|
||||||
},
|
|
||||||
|
|
||||||
onKeyDown(e) {
|
|
||||||
if (['e', 'E', '+', '-', '.'].includes(e.key)) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async save(buttonCb) {
|
async save(buttonCb) {
|
||||||
this.errors = [];
|
this.errors = [];
|
||||||
|
|
||||||
const name = `bundle-${ this.clusterName }-${ this.version }-${ randomStr(5).toLowerCase() }`;
|
const name = `bundle-${ randomStr(5).toLowerCase() }`;
|
||||||
const namespace = 'harvester-system';
|
const namespace = 'harvester-system';
|
||||||
|
|
||||||
const spec = {
|
|
||||||
description: this.description.trim(),
|
|
||||||
...(this.url.trim() && { issueURL: this.url.trim() }),
|
|
||||||
...(this.namespaces.length > 0 && { extraCollectionNamespaces: this.namespaces }),
|
|
||||||
...(this.timeout !== '' && { timeout: Number(this.timeout) }),
|
|
||||||
...(this.expiration !== '' && { expiration: Number(this.expiration) }),
|
|
||||||
...(this.nodeTimeout !== '' && { nodeTimeout: Number(this.nodeTimeout) }),
|
|
||||||
};
|
|
||||||
|
|
||||||
const bundleCrd = {
|
const bundleCrd = {
|
||||||
apiVersion: 'harvesterhci.io/v1beta1',
|
apiVersion: 'harvesterhci.io/v1beta1',
|
||||||
type: HCI.SUPPORT_BUNDLE,
|
type: HCI.SUPPORT_BUNDLE,
|
||||||
kind: 'SupportBundle',
|
kind: 'SupportBundle',
|
||||||
metadata: { name, namespace },
|
metadata: {
|
||||||
spec,
|
name,
|
||||||
|
namespace
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
issueURL: this.url,
|
||||||
|
description: this.description
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
const bundleValue = await this.$store.dispatch(`${ inStore }/create`, bundleCrd);
|
||||||
const bundleValue = await this.$store.dispatch(`${ inStore }/create`, bundleCrd);
|
|
||||||
|
|
||||||
|
try {
|
||||||
await bundleValue.save();
|
await bundleValue.save();
|
||||||
|
|
||||||
this.$store.commit('harvester-common/setLatestBundleId', `${ namespace }/${ name }`, { root: true });
|
this.$store.commit('harvester-common/setLatestBundleId', `${ namespace }/${ name }`, { root: true });
|
||||||
@@ -237,110 +118,69 @@ export default {
|
|||||||
@close="close"
|
@close="close"
|
||||||
>
|
>
|
||||||
<div class="p-20">
|
<div class="p-20">
|
||||||
<h2>{{ t('harvester.modal.bundle.title') }}</h2>
|
<h2>
|
||||||
<div class="content">
|
{{ t('harvester.modal.bundle.title') }}
|
||||||
<div
|
</h2>
|
||||||
v-if="bundlePending"
|
|
||||||
class="circle mb-20"
|
<div
|
||||||
>
|
v-if="!bundlePending"
|
||||||
|
class="content"
|
||||||
|
>
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="url"
|
||||||
|
:label="t('harvester.modal.bundle.url')"
|
||||||
|
class="mb-20"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="description"
|
||||||
|
:label="t('harvester.modal.bundle.description')"
|
||||||
|
type="multiline"
|
||||||
|
:min-height="120"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="content"
|
||||||
|
>
|
||||||
|
<div class="circle">
|
||||||
<GraphCircle
|
<GraphCircle
|
||||||
primary-stroke-color="green"
|
primary-stroke-color="green"
|
||||||
secondary-stroke-color="lightgrey"
|
secondary-stroke-color="white"
|
||||||
:stroke-width="6"
|
:stroke-width="6"
|
||||||
:percentage="percentage"
|
:percentage="percentage"
|
||||||
:show-text="true"
|
:show-text="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<template v-else>
|
</div>
|
||||||
<p
|
|
||||||
v-if="customSupportBundleFeatureEnabled"
|
|
||||||
v-clean-html="t('harvester.modal.bundle.tip', { doc: docLink }, true)"
|
|
||||||
class="mb-20"
|
|
||||||
/>
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="url"
|
|
||||||
:label="t('harvester.modal.bundle.url')"
|
|
||||||
class="mb-10"
|
|
||||||
/>
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="description"
|
|
||||||
required
|
|
||||||
:label="t('harvester.modal.bundle.description')"
|
|
||||||
type="multiline"
|
|
||||||
:min-height="80"
|
|
||||||
class="mb-10"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<template v-if="customSupportBundleFeatureEnabled">
|
<div
|
||||||
<LabeledSelect
|
v-for="(err, idx) in errors"
|
||||||
v-model:value="namespaces"
|
:key="idx"
|
||||||
:label="t('harvester.modal.bundle.namespaces.label')"
|
>
|
||||||
:clearable="true"
|
<Banner
|
||||||
:multiple="true"
|
color="error"
|
||||||
:options="namespaceOptions"
|
:label="stringify(err)"
|
||||||
class="mb-10 label-select"
|
/>
|
||||||
:tooltip="t('harvester.modal.bundle.namespaces.tooltip', _, true)"
|
</div>
|
||||||
@update:value="updateNamespaces"
|
|
||||||
/>
|
<div class="footer mt-20">
|
||||||
<UnitInput
|
<button
|
||||||
v-model:value="timeout"
|
class="btn btn-sm role-secondary mr-10"
|
||||||
:label="t('harvester.modal.bundle.timeout.label')"
|
@click="close"
|
||||||
class="mb-10"
|
|
||||||
type="number"
|
|
||||||
:min="0"
|
|
||||||
:tooltip="t('harvester.modal.bundle.timeout.tooltip', _, true)"
|
|
||||||
:suffix="timeout > 1 ? 'Minutes' : 'Minute'"
|
|
||||||
@keydown="onKeyDown"
|
|
||||||
@update:value="val => updateNumberValue('timeout', val)"
|
|
||||||
/>
|
|
||||||
<UnitInput
|
|
||||||
v-model:value="expiration"
|
|
||||||
:label="t('harvester.modal.bundle.expiration.label')"
|
|
||||||
class="mb-10"
|
|
||||||
type="number"
|
|
||||||
:min="0"
|
|
||||||
:tooltip="t('harvester.modal.bundle.expiration.tooltip', _, true)"
|
|
||||||
:suffix="expiration > 1 ? 'Minutes' : 'Minute'"
|
|
||||||
@keydown="onKeyDown"
|
|
||||||
@update:value="val => updateNumberValue('expiration', val)"
|
|
||||||
/>
|
|
||||||
<UnitInput
|
|
||||||
v-model:value="nodeTimeout"
|
|
||||||
:label="t('harvester.modal.bundle.nodeTimeout.label')"
|
|
||||||
class="mb-10"
|
|
||||||
type="number"
|
|
||||||
:min="0"
|
|
||||||
:tooltip="t('harvester.modal.bundle.nodeTimeout.tooltip', _, true)"
|
|
||||||
:suffix="nodeTimeout > 1 ? 'Minutes' : 'Minute'"
|
|
||||||
@keydown="onKeyDown"
|
|
||||||
@update:value="val => updateNumberValue('nodeTimeout', val)"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
<div
|
|
||||||
v-for="(err, idx) in errors"
|
|
||||||
:key="idx"
|
|
||||||
>
|
>
|
||||||
<Banner
|
{{ t('generic.close') }}
|
||||||
color="error"
|
</button>
|
||||||
:label="stringify(err)"
|
|
||||||
/>
|
<AsyncButton
|
||||||
</div>
|
type="submit"
|
||||||
<div class="footer mt-20">
|
mode="generate"
|
||||||
<button
|
class="btn btn-sm bg-primary"
|
||||||
class="btn btn-sm role-secondary mr-10"
|
:disabled="bundlePending"
|
||||||
@click="close"
|
@click="save"
|
||||||
>
|
/>
|
||||||
{{ t('generic.close') }}
|
|
||||||
</button>
|
|
||||||
<AsyncButton
|
|
||||||
type="submit"
|
|
||||||
mode="generate"
|
|
||||||
class="btn btn-sm bg-primary"
|
|
||||||
:disabled="bundlePending"
|
|
||||||
@click="save"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</app-modal>
|
</app-modal>
|
||||||
@@ -354,10 +194,6 @@ export default {
|
|||||||
max-height: 100vh;
|
max-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.labeled-select.taggable ::v-deep(.vs__selected-options .vs__selected.vs__selected > button) {
|
|
||||||
margin: 0 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bundle {
|
.bundle {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
@@ -368,8 +204,10 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
|
height: 218px;
|
||||||
|
|
||||||
.circle {
|
.circle {
|
||||||
padding: 10px 0;
|
padding-top: 20px;
|
||||||
height: 160px;
|
height: 160px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,12 +90,6 @@ export default {
|
|||||||
v-clean-html="t('harvester.virtualMachine.unplug.title', { name: diskName })"
|
v-clean-html="t('harvester.virtualMachine.unplug.title', { name: diskName })"
|
||||||
class="text-default-text"
|
class="text-default-text"
|
||||||
/>
|
/>
|
||||||
<Banner
|
|
||||||
v-for="(err, i) in errors"
|
|
||||||
:key="i"
|
|
||||||
color="error"
|
|
||||||
:label="err"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #actions>
|
<template #actions>
|
||||||
@@ -117,6 +111,13 @@ export default {
|
|||||||
@click="save"
|
@click="save"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Banner
|
||||||
|
v-for="(err, i) in errors"
|
||||||
|
:key="i"
|
||||||
|
color="error"
|
||||||
|
:label="err"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -132,12 +132,6 @@ export default {
|
|||||||
:label="t('generic.name')"
|
:label="t('generic.name')"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<Banner
|
|
||||||
v-for="(err, i) in errors"
|
|
||||||
:key="i"
|
|
||||||
color="error"
|
|
||||||
:label="err"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #actions>
|
<template #actions>
|
||||||
@@ -156,6 +150,11 @@ export default {
|
|||||||
@click="save"
|
@click="save"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Banner
|
||||||
|
v-for="(err, i) in errors"
|
||||||
|
:key="i"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -73,10 +73,8 @@ export default {
|
|||||||
|
|
||||||
data.spec.clusterNetwork = this.clusterNetwork;
|
data.spec.clusterNetwork = this.clusterNetwork;
|
||||||
|
|
||||||
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl'](`v1/harvester/${ HCI.VLAN_CONFIG }s/${ data.id }`);
|
|
||||||
|
|
||||||
await this.$store.dispatch('harvester/request', {
|
await this.$store.dispatch('harvester/request', {
|
||||||
url,
|
url: `/v1/harvester/${ HCI.VLAN_CONFIG }s/${ data.id }`,
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
@@ -109,12 +107,6 @@ export default {
|
|||||||
:placeholder="t('harvester.harvesterVlanConfigMigrateDialog.targetClusterNetwork.placeholder')"
|
:placeholder="t('harvester.harvesterVlanConfigMigrateDialog.targetClusterNetwork.placeholder')"
|
||||||
:options="clusterNetworks"
|
:options="clusterNetworks"
|
||||||
/>
|
/>
|
||||||
<Banner
|
|
||||||
v-for="(err, i) in errors"
|
|
||||||
:key="i"
|
|
||||||
color="error"
|
|
||||||
:label="err"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #actions>
|
<template #actions>
|
||||||
@@ -133,6 +125,11 @@ export default {
|
|||||||
@click="apply"
|
@click="apply"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Banner
|
||||||
|
v-for="(err, i) in errors"
|
||||||
|
:key="i"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -70,16 +70,16 @@ export default {
|
|||||||
v-clean-html="t('harvester.modal.restart.tip')"
|
v-clean-html="t('harvester.modal.restart.tip')"
|
||||||
class="pl-10 pr-10"
|
class="pl-10 pr-10"
|
||||||
/>
|
/>
|
||||||
<Banner
|
|
||||||
v-for="(err, i) in errors"
|
|
||||||
:key="i"
|
|
||||||
color="error"
|
|
||||||
:label="err"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<div class="bottom">
|
<div class="bottom">
|
||||||
|
<Banner
|
||||||
|
v-for="(err, i) in errors"
|
||||||
|
:key="i"
|
||||||
|
color="error"
|
||||||
|
:label="err"
|
||||||
|
/>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<button
|
<button
|
||||||
class="btn role-secondary mr-10"
|
class="btn role-secondary mr-10"
|
||||||
|
|||||||
@@ -145,12 +145,6 @@ export default {
|
|||||||
class="mt-20"
|
class="mt-20"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<Banner
|
|
||||||
v-for="(err, i) in errors"
|
|
||||||
:key="i"
|
|
||||||
color="error"
|
|
||||||
:label="err"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #actions>
|
<template #actions>
|
||||||
@@ -168,6 +162,10 @@ export default {
|
|||||||
@click="save"
|
@click="save"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<Banner
|
||||||
|
v-for="(err, i) in errors"
|
||||||
|
:key="i"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -79,12 +79,6 @@ export default {
|
|||||||
:label="t('harvester.modal.snapshot.name')"
|
:label="t('harvester.modal.snapshot.name')"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<Banner
|
|
||||||
v-for="(err, i) in errors"
|
|
||||||
:key="i"
|
|
||||||
color="error"
|
|
||||||
:label="err"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #actions>
|
<template #actions>
|
||||||
@@ -102,6 +96,12 @@ export default {
|
|||||||
@click="save"
|
@click="save"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<Banner
|
||||||
|
v-for="(err, i) in errors"
|
||||||
|
:key="i"
|
||||||
|
color="error"
|
||||||
|
:label="err"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -98,12 +98,6 @@ export default {
|
|||||||
:label="t('harvester.modal.volumeClone.name')"
|
:label="t('harvester.modal.volumeClone.name')"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<Banner
|
|
||||||
v-for="(err, i) in errors"
|
|
||||||
:key="i"
|
|
||||||
color="error"
|
|
||||||
:label="err"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #actions>
|
<template #actions>
|
||||||
@@ -121,6 +115,10 @@ export default {
|
|||||||
@click="save"
|
@click="save"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<Banner
|
||||||
|
v-for="(err, i) in errors"
|
||||||
|
:key="i"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ export default {
|
|||||||
:mode="mode"
|
:mode="mode"
|
||||||
:errors="errors"
|
:errors="errors"
|
||||||
@finish="save"
|
@finish="save"
|
||||||
@error="e=>errors=e"
|
|
||||||
>
|
>
|
||||||
<component
|
<component
|
||||||
:is="currentComponent"
|
:is="currentComponent"
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { Banner } from '@components/Banner';
|
|||||||
|
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
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 {
|
export default {
|
||||||
name: 'EditAddonNvidiaDriverToolkit',
|
name: 'EditAddonNvidiaDriverToolkit',
|
||||||
@@ -122,7 +122,7 @@ export default {
|
|||||||
>
|
>
|
||||||
<div class="col span-6">
|
<div class="col span-6">
|
||||||
<LabeledInput
|
<LabeledInput
|
||||||
v-model:value="valuesContentJson.image.repository"
|
v-model:value="valuesContentJson.image.repo"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:required="true"
|
:required="true"
|
||||||
label-key="harvester.addons.nvidiaDriverToolkit.image.repository"
|
label-key="harvester.addons.nvidiaDriverToolkit.image.repository"
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ export default {
|
|||||||
:apply-hooks="applyHooks"
|
:apply-hooks="applyHooks"
|
||||||
@finish="save"
|
@finish="save"
|
||||||
@cancel="done"
|
@cancel="done"
|
||||||
@error="e=>errors=e"
|
|
||||||
>
|
>
|
||||||
<NameNsDescription
|
<NameNsDescription
|
||||||
:value="value"
|
:value="value"
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import Loading from '@shell/components/Loading.vue';
|
|||||||
import MessageLink from '@shell/components/MessageLink';
|
import MessageLink from '@shell/components/MessageLink';
|
||||||
import { ADD_ONS } from '@pkg/harvester/config/harvester-map';
|
import { ADD_ONS } from '@pkg/harvester/config/harvester-map';
|
||||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '@pkg/harvester/config/harvester';
|
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 { _EDIT } from '@shell/config/query-params';
|
||||||
import { sortBy } from '@shell/utils/sort';
|
import { sortBy } from '@shell/utils/sort';
|
||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
@@ -185,7 +185,7 @@ export default {
|
|||||||
minExponent: 3,
|
minExponent: 3,
|
||||||
maxExponent: 3,
|
maxExponent: 3,
|
||||||
maxPrecision: 2,
|
maxPrecision: 2,
|
||||||
suffix: UNIT_SUFFIX,
|
suffix: 'iB',
|
||||||
};
|
};
|
||||||
|
|
||||||
const longhornDisks = Object.keys(diskStatus).map((key) => {
|
const longhornDisks = Object.keys(diskStatus).map((key) => {
|
||||||
@@ -457,7 +457,7 @@ export default {
|
|||||||
const devPath = d.spec?.devPath;
|
const devPath = d.spec?.devPath;
|
||||||
const deviceType = d.status?.deviceStatus?.details?.deviceType;
|
const deviceType = d.status?.deviceStatus?.details?.deviceType;
|
||||||
const sizeBytes = d.status?.deviceStatus?.capacity?.sizeBytes;
|
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 parentDevice = d.status?.deviceStatus?.parentDevice;
|
||||||
const isChildAdded = this.newDisks.find((newDisk) => newDisk.blockDevice?.status?.deviceStatus?.parentDevice === devPath);
|
const isChildAdded = this.newDisks.find((newDisk) => newDisk.blockDevice?.status?.deviceStatus?.parentDevice === devPath);
|
||||||
const name = d.displayName;
|
const name = d.displayName;
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ export default {
|
|||||||
:errors="errors"
|
:errors="errors"
|
||||||
:apply-hooks="applyHooks"
|
:apply-hooks="applyHooks"
|
||||||
@finish="save"
|
@finish="save"
|
||||||
@error="e=>errors=e"
|
|
||||||
>
|
>
|
||||||
<div class="header mb-20">
|
<div class="header mb-20">
|
||||||
<FileSelector
|
<FileSelector
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ export default {
|
|||||||
:validation-passed="true"
|
:validation-passed="true"
|
||||||
:errors="errors"
|
:errors="errors"
|
||||||
:can-yaml="true"
|
:can-yaml="true"
|
||||||
@error="e=>errors=e"
|
@error="e=>errors = e"
|
||||||
@finish="save"
|
@finish="save"
|
||||||
@cancel="done"
|
@cancel="done"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ export default {
|
|||||||
:resource="value"
|
:resource="value"
|
||||||
:subtypes="[]"
|
:subtypes="[]"
|
||||||
:cancel-event="true"
|
:cancel-event="true"
|
||||||
@error="e=>errors=e"
|
@error="e=>errors = e"
|
||||||
@finish="save"
|
@finish="save"
|
||||||
@cancel="done"
|
@cancel="done"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -6,13 +6,11 @@ import { LabeledInput } from '@components/Form/LabeledInput';
|
|||||||
import { RadioGroup } from '@components/Form/Radio';
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
|
||||||
import { HCI as HCI_LABELS_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
import { HCI as HCI_LABELS_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
import { allHash } from '@shell/utils/promise';
|
import { allHash } from '@shell/utils/promise';
|
||||||
import { HCI } from '../types';
|
import { HCI } from '../types';
|
||||||
import { NETWORK_TYPE } from '../config/types';
|
|
||||||
|
|
||||||
const { L2VLAN, UNTAGGED, OVERLAY } = NETWORK_TYPE;
|
|
||||||
|
|
||||||
const AUTO = 'auto';
|
const AUTO = 'auto';
|
||||||
const MANUAL = 'manual';
|
const MANUAL = 'manual';
|
||||||
@@ -43,11 +41,14 @@ export default {
|
|||||||
|
|
||||||
data() {
|
data() {
|
||||||
const config = JSON.parse(this.value.spec.config);
|
const config = JSON.parse(this.value.spec.config);
|
||||||
|
|
||||||
const annotations = this.value?.metadata?.annotations || {};
|
const annotations = this.value?.metadata?.annotations || {};
|
||||||
const layer3Network = JSON.parse(annotations[HCI_LABELS_ANNOTATIONS.NETWORK_ROUTE] || '{}');
|
const layer3Network = JSON.parse(annotations[HCI_LABELS_ANNOTATIONS.NETWORK_ROUTE] || '{}');
|
||||||
|
|
||||||
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 {
|
return {
|
||||||
config,
|
config,
|
||||||
@@ -74,30 +75,6 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
clusterBridge: {
|
|
||||||
get() {
|
|
||||||
if (!this.config.bridge) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove -br suffix if exists
|
|
||||||
return this.config?.bridge?.endsWith('-br') ? this.config.bridge.slice(0, -3) : '';
|
|
||||||
},
|
|
||||||
|
|
||||||
set(neu) {
|
|
||||||
if (neu === '') {
|
|
||||||
this.config.bridge = '';
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!neu.endsWith('-br')) {
|
|
||||||
this.config.bridge = `${ neu }-br`;
|
|
||||||
} else {
|
|
||||||
this.config.bridge = neu;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modeOptions() {
|
modeOptions() {
|
||||||
return [{
|
return [{
|
||||||
label: this.t('harvester.network.layer3Network.mode.auto'),
|
label: this.t('harvester.network.layer3Network.mode.auto'),
|
||||||
@@ -108,14 +85,6 @@ export default {
|
|||||||
}];
|
}];
|
||||||
},
|
},
|
||||||
|
|
||||||
kubeovnVpcSubnetSupport() {
|
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('kubeovnVpcSubnet');
|
|
||||||
},
|
|
||||||
|
|
||||||
longhornV2LVMSupport() {
|
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('longhornV2LVMSupport');
|
|
||||||
},
|
|
||||||
|
|
||||||
clusterNetworkOptions() {
|
clusterNetworkOptions() {
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
|
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
|
||||||
@@ -131,55 +100,16 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
networkTypes() {
|
networkType() {
|
||||||
const types = [L2VLAN, UNTAGGED];
|
return ['L2VlanNetwork', 'UntaggedNetwork'];
|
||||||
|
|
||||||
if (this.kubeovnVpcSubnetSupport) {
|
|
||||||
types.push(OVERLAY);
|
|
||||||
}
|
|
||||||
|
|
||||||
return types;
|
|
||||||
},
|
|
||||||
|
|
||||||
isL2VlanNetwork() {
|
|
||||||
if (this.isView) {
|
|
||||||
return this.value.vlanType === L2VLAN;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.type === L2VLAN;
|
|
||||||
},
|
|
||||||
|
|
||||||
isOverlayNetwork() {
|
|
||||||
if (this.isView) {
|
|
||||||
return this.value.vlanType === OVERLAY;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.type === OVERLAY;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
isUntaggedNetwork() {
|
isUntaggedNetwork() {
|
||||||
if (this.isView) {
|
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) {
|
|
||||||
this.config.type = 'kube-ovn';
|
|
||||||
this.config.provider = `${ this.value.metadata.name }.${ this.value.metadata.namespace }.ovn`;
|
|
||||||
this.config.server_socket = '/run/openvswitch/kube-ovn-daemon.sock';
|
|
||||||
} else {
|
|
||||||
this.config.type = 'bridge';
|
|
||||||
this.config.promiscMode = true;
|
|
||||||
this.config.ipam = {};
|
|
||||||
this.config.bridge = '';
|
|
||||||
delete this.config.provider;
|
|
||||||
delete this.config.server_socket;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -187,33 +117,32 @@ export default {
|
|||||||
async saveNetwork(buttonCb) {
|
async saveNetwork(buttonCb) {
|
||||||
const errors = [];
|
const errors = [];
|
||||||
|
|
||||||
if (this.isL2VlanNetwork || this.isUntaggedNetwork) {
|
if (!this.config.vlan && !this.isUntaggedNetwork) {
|
||||||
if (!this.config.vlan && !this.isUntaggedNetwork) {
|
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('tableHeaders.networkVlan') }));
|
||||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('tableHeaders.networkVlan') }));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.config.bridge) {
|
|
||||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.clusterNetwork.label') }));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.layer3Network.mode === MANUAL) {
|
|
||||||
if (!this.layer3Network.gateway) {
|
|
||||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.gateway.label') }));
|
|
||||||
}
|
|
||||||
if (!this.layer3Network.cidr) {
|
|
||||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.cidr.label') }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errors.length > 0) {
|
|
||||||
buttonCb(false);
|
|
||||||
this.errors = errors;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
this.value.setAnnotation(HCI_LABELS_ANNOTATIONS.NETWORK_ROUTE, JSON.stringify(this.layer3Network));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.config.bridge) {
|
||||||
|
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.clusterNetwork.label') }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.layer3Network.mode === MANUAL) {
|
||||||
|
if (!this.layer3Network.gateway) {
|
||||||
|
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.gateway.label') }));
|
||||||
|
}
|
||||||
|
if (!this.layer3Network.cidr) {
|
||||||
|
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.cidr.label') }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
buttonCb(false);
|
||||||
|
this.errors = errors;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.value.setAnnotation(HCI_LABELS_ANNOTATIONS.NETWORK_ROUTE, JSON.stringify(this.layer3Network));
|
||||||
|
|
||||||
await this.save(buttonCb);
|
await this.save(buttonCb);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -237,19 +166,14 @@ export default {
|
|||||||
updateBeforeSave() {
|
updateBeforeSave() {
|
||||||
this.config.name = this.value.metadata.name;
|
this.config.name = this.value.metadata.name;
|
||||||
|
|
||||||
if (this.isOverlayNetwork) {
|
|
||||||
this.config.provider = `${ this.value.metadata.name }.${ this.value.metadata.namespace }.ovn`;
|
|
||||||
delete this.config.bridge;
|
|
||||||
delete this.config.promiscMode;
|
|
||||||
delete this.config.vlan;
|
|
||||||
delete this.config.ipam;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isUntaggedNetwork) {
|
if (this.isUntaggedNetwork) {
|
||||||
delete this.config.vlan;
|
delete this.config.vlan;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.value.spec.config = JSON.stringify({ ...this.config });
|
this.value.spec.config = JSON.stringify({
|
||||||
|
...this.config,
|
||||||
|
bridge: `${ this.config.bridge }-br`,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -263,7 +187,6 @@ export default {
|
|||||||
:errors="errors"
|
:errors="errors"
|
||||||
:apply-hooks="applyHooks"
|
:apply-hooks="applyHooks"
|
||||||
@finish="saveNetwork"
|
@finish="saveNetwork"
|
||||||
@error="e=>errors=e"
|
|
||||||
>
|
>
|
||||||
<NameNsDescription
|
<NameNsDescription
|
||||||
ref="nd"
|
ref="nd"
|
||||||
@@ -285,15 +208,14 @@ export default {
|
|||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
v-model:value="type"
|
v-model:value="type"
|
||||||
class="mb-20"
|
class="mb-20"
|
||||||
:options="networkTypes"
|
:options="networkType"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:disabled="isEdit"
|
|
||||||
:label="t('harvester.fields.type')"
|
:label="t('harvester.fields.type')"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LabeledInput
|
<LabeledInput
|
||||||
v-if="isL2VlanNetwork"
|
v-if="!isUntaggedNetwork"
|
||||||
v-model:value.number="config.vlan"
|
v-model:value.number="config.vlan"
|
||||||
class="mb-20"
|
class="mb-20"
|
||||||
required
|
required
|
||||||
@@ -303,20 +225,25 @@ export default {
|
|||||||
:mode="mode"
|
:mode="mode"
|
||||||
@update:value="input"
|
@update:value="input"
|
||||||
/>
|
/>
|
||||||
<LabeledSelect
|
|
||||||
v-if="!isOverlayNetwork"
|
<div class="row">
|
||||||
v-model:value="clusterBridge"
|
<div
|
||||||
class="mb-20"
|
class="col span-12"
|
||||||
:label="t('harvester.network.clusterNetwork.label')"
|
>
|
||||||
required
|
<LabeledSelect
|
||||||
:disabled="isEdit"
|
v-model:value="config.bridge"
|
||||||
:options="clusterNetworkOptions"
|
class="mb-20"
|
||||||
:mode="mode"
|
:label="t('harvester.network.clusterNetwork.label')"
|
||||||
:placeholder="t('harvester.network.clusterNetwork.selectPlaceholder')"
|
required
|
||||||
/>
|
:options="clusterNetworkOptions"
|
||||||
|
:mode="mode"
|
||||||
|
:placeholder="t('harvester.network.clusterNetwork.selectPlaceholder')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
v-if="isL2VlanNetwork"
|
v-if="!isUntaggedNetwork"
|
||||||
name="layer3Network"
|
name="layer3Network"
|
||||||
:label="t('harvester.network.tabs.layer3Network')"
|
:label="t('harvester.network.tabs.layer3Network')"
|
||||||
:weight="98"
|
:weight="98"
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ export default {
|
|||||||
:apply-hooks="applyHooks"
|
:apply-hooks="applyHooks"
|
||||||
:validation-passed="canSave"
|
:validation-passed="canSave"
|
||||||
@finish="save"
|
@finish="save"
|
||||||
@error="e=>errors=e"
|
@error="e=>errors = e"
|
||||||
>
|
>
|
||||||
<div class="banner">
|
<div class="banner">
|
||||||
<Banner
|
<Banner
|
||||||
@@ -233,7 +233,7 @@ export default {
|
|||||||
:disabled="isEdit || isView"
|
:disabled="isEdit || isView"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
row
|
row
|
||||||
@update:value="onTypeChange"
|
@input="onTypeChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-30">
|
<div class="row mb-30">
|
||||||
|
|||||||
@@ -84,10 +84,6 @@ export default {
|
|||||||
this.value['_type'] = TYPES.OPAQUE;
|
this.value['_type'] = TYPES.OPAQUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( this.realMode === _CLONE) {
|
|
||||||
this.value['_type'] = this.liveValue._type;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isCloud,
|
isCloud,
|
||||||
nodeDrivers: null,
|
nodeDrivers: null,
|
||||||
@@ -238,7 +234,7 @@ export default {
|
|||||||
:resource="value"
|
:resource="value"
|
||||||
:errors="errors"
|
:errors="errors"
|
||||||
@finish="saveSecret"
|
@finish="saveSecret"
|
||||||
@error="e=>errors=e"
|
@error="e=>errors = e"
|
||||||
>
|
>
|
||||||
<NameNsDescription
|
<NameNsDescription
|
||||||
:value="value"
|
:value="value"
|
||||||
|
|||||||
@@ -4,10 +4,8 @@ import { RadioGroup } from '@components/Form/Radio';
|
|||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
import { TextAreaAutoGrow } from '@components/Form/TextArea';
|
import { TextAreaAutoGrow } from '@components/Form/TextArea';
|
||||||
import UnitInput from '@shell/components/form/UnitInput';
|
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
import { DOC } from '../config/doc-links';
|
|
||||||
import { docLink } from '../utils/feature-flags';
|
|
||||||
|
|
||||||
import { HCI_ALLOWED_SETTINGS, HCI_SINGLE_CLUSTER_ALLOWED_SETTING, HCI_SETTING } from '../config/settings';
|
import { HCI_ALLOWED_SETTINGS, HCI_SINGLE_CLUSTER_ALLOWED_SETTING, HCI_SETTING } from '../config/settings';
|
||||||
|
|
||||||
@@ -17,8 +15,7 @@ export default {
|
|||||||
LabeledInput,
|
LabeledInput,
|
||||||
LabeledSelect,
|
LabeledSelect,
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
TextAreaAutoGrow,
|
TextAreaAutoGrow
|
||||||
UnitInput
|
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [CreateEditView],
|
mixins: [CreateEditView],
|
||||||
@@ -60,13 +57,7 @@ export default {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
setting,
|
setting,
|
||||||
description: isHarvester ? t(
|
description: isHarvester ? t(`advancedSettings.descriptions.harv-${ this.value.id }`) : t(`advancedSettings.descriptions.${ this.value.id }`),
|
||||||
`advancedSettings.descriptions.harv-${ this.value.id }`,
|
|
||||||
this.getDocLinkParams()
|
|
||||||
) : t(
|
|
||||||
`advancedSettings.descriptions.${ this.value.id }`,
|
|
||||||
this.getDocLinkParams()
|
|
||||||
),
|
|
||||||
editHelp: t(`advancedSettings.editHelp.${ this.value.id }`),
|
editHelp: t(`advancedSettings.editHelp.${ this.value.id }`),
|
||||||
enumOptions,
|
enumOptions,
|
||||||
canReset,
|
canReset,
|
||||||
@@ -137,13 +128,6 @@ export default {
|
|||||||
await this.clusterRegistrationUrlTip();
|
await this.clusterRegistrationUrlTip();
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove any leading zeros (e.g. '0123' becomes 123)
|
|
||||||
if (this.setting.kind === 'number' && this.value.value) {
|
|
||||||
const num = Number(this.value.value);
|
|
||||||
|
|
||||||
this.value.value = isNaN(num) ? 0 : `${ num }`;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.save(done);
|
this.save(done);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -179,18 +163,6 @@ export default {
|
|||||||
if (typeof this.$refs.settingComp?.useDefault === 'function') {
|
if (typeof this.$refs.settingComp?.useDefault === 'function') {
|
||||||
this.$refs.settingComp.useDefault();
|
this.$refs.settingComp.useDefault();
|
||||||
}
|
}
|
||||||
},
|
|
||||||
getDocLinkParams() {
|
|
||||||
const setting = HCI_ALLOWED_SETTINGS[this.value.id] || HCI_SINGLE_CLUSTER_ALLOWED_SETTING[this.value.id];
|
|
||||||
|
|
||||||
if (setting?.docPath) {
|
|
||||||
const version = this.$store.getters['harvester-common/getServerVersion']();
|
|
||||||
const url = docLink(DOC[setting.docPath], version);
|
|
||||||
|
|
||||||
return { url };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -205,7 +177,7 @@ export default {
|
|||||||
:subtypes="[]"
|
:subtypes="[]"
|
||||||
:can-yaml="false"
|
:can-yaml="false"
|
||||||
:cancel-event="true"
|
:cancel-event="true"
|
||||||
@error="e=>errors=e"
|
@error="e => (errors = e)"
|
||||||
@finish="saveSettings"
|
@finish="saveSettings"
|
||||||
@cancel="done"
|
@cancel="done"
|
||||||
>
|
>
|
||||||
@@ -266,13 +238,6 @@ export default {
|
|||||||
:min-height="254"
|
:min-height="254"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="setting.kind === 'number'">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="value.value"
|
|
||||||
:label="t('advancedSettings.edit.value')"
|
|
||||||
type="number"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<LabeledInput
|
<LabeledInput
|
||||||
v-model:value="value.value"
|
v-model:value="value.value"
|
||||||
|
|||||||
@@ -1,321 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
|
||||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
|
||||||
import ArrayList from '@shell/components/form/ArrayList';
|
|
||||||
import { Checkbox } from '@components/Form/Checkbox';
|
|
||||||
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
|
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
||||||
import { VOLUME_SNAPSHOT_CLASS, HCI } from '../../types';
|
|
||||||
import { HCI_SETTING } from '../../config/settings';
|
|
||||||
import { allHash } from '@shell/utils/promise';
|
|
||||||
import { _EDIT, _CREATE } from '@shell/config/query-params';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'CDISettings',
|
|
||||||
|
|
||||||
props: {
|
|
||||||
value: {
|
|
||||||
type: Object,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
mode: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
provisioner: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
components: {
|
|
||||||
ArrayList,
|
|
||||||
Checkbox,
|
|
||||||
LabeledInput,
|
|
||||||
LabeledSelect,
|
|
||||||
},
|
|
||||||
|
|
||||||
emits: ['update:cdiSettings'],
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
const hash = {
|
|
||||||
volumeSnapshotClasses: this.$store.dispatch(`${ this.inStore }/findAll`, { type: VOLUME_SNAPSHOT_CLASS }),
|
|
||||||
csiDriverConfigSetting: this.$store.dispatch(`${ this.inStore }/find`, { type: HCI.SETTING, id: HCI_SETTING.CSI_DRIVER_CONFIG }),
|
|
||||||
};
|
|
||||||
|
|
||||||
await allHash(hash);
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
cdiSettings: {
|
|
||||||
volumeModeAccessModes: [],
|
|
||||||
volumeSnapshotClass: null,
|
|
||||||
cloneStrategy: null,
|
|
||||||
filesystemOverhead: null,
|
|
||||||
},
|
|
||||||
defaultAddValue: { volumeMode: null, accessModes: [] },
|
|
||||||
noneOption: { label: 'None', value: '' },
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
if (this.mode === _CREATE ) {
|
|
||||||
this.setDefaultVolumeSnapshotClass();
|
|
||||||
} else {
|
|
||||||
this.initCDISettingsFromAnnotations();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
inStore() {
|
|
||||||
return this.$store.getters['currentProduct'].inStore;
|
|
||||||
},
|
|
||||||
|
|
||||||
allVolumeModeOptions() {
|
|
||||||
return Object.values(VOLUME_MODE).map((mode) => ({ label: mode, value: mode }));
|
|
||||||
},
|
|
||||||
|
|
||||||
selectedVolumeModes() {
|
|
||||||
return this.cdiSettings.volumeModeAccessModes
|
|
||||||
.map((item) => item.volumeMode)
|
|
||||||
.filter(Boolean);
|
|
||||||
},
|
|
||||||
|
|
||||||
volumeModeOptions() {
|
|
||||||
return [this.noneOption, ...this.allVolumeModeOptions].filter( (option) => !this.selectedVolumeModes.includes(option.value) );
|
|
||||||
},
|
|
||||||
|
|
||||||
allVolumeModesSelected() {
|
|
||||||
return this.selectedVolumeModes.length === this.allVolumeModeOptions.length;
|
|
||||||
},
|
|
||||||
|
|
||||||
accessModeOptions() {
|
|
||||||
return [
|
|
||||||
{ label: 'ReadWriteOnce', value: 'ReadWriteOnce' },
|
|
||||||
{ label: 'ReadOnlyMany', value: 'ReadOnlyMany' },
|
|
||||||
{ label: 'ReadWriteMany', value: 'ReadWriteMany' },
|
|
||||||
{ label: 'ReadWriteOncePod', value: 'ReadWriteOncePod' },
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
cloneStrategyOptions() {
|
|
||||||
return [
|
|
||||||
this.noneOption,
|
|
||||||
{ label: this.t('harvester.storage.cdiSettings.cloneStrategy.copy'), value: 'copy' },
|
|
||||||
{ label: this.t('harvester.storage.cdiSettings.cloneStrategy.snapshot'), value: 'snapshot' },
|
|
||||||
{ label: this.t('harvester.storage.cdiSettings.cloneStrategy.csiClone'), value: 'csi-clone' },
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
volumeSnapshotClassOptions() {
|
|
||||||
const allClasses = this.$store.getters[`${ this.inStore }/all`](VOLUME_SNAPSHOT_CLASS) || [];
|
|
||||||
const filtered = allClasses.filter((c) => c.driver === this.provisioner);
|
|
||||||
|
|
||||||
return [this.noneOption, ...filtered.map((c) => ({ label: c.name, value: c.name }))];
|
|
||||||
},
|
|
||||||
|
|
||||||
isFilesystemOverheadValid() {
|
|
||||||
const val = this.cdiSettings.filesystemOverhead;
|
|
||||||
|
|
||||||
if (val === null || val === '') return true;
|
|
||||||
const regex = /^(0(\.\d{1,3})?|1(\.0{1,3})?)$/;
|
|
||||||
|
|
||||||
return regex.test(val);
|
|
||||||
},
|
|
||||||
|
|
||||||
isCustomClass() {
|
|
||||||
return this.mode === _CREATE || this.mode === _EDIT;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
provisioner() {
|
|
||||||
this.resetCdiSettings();
|
|
||||||
this.$nextTick(this.setDefaultVolumeSnapshotClass);
|
|
||||||
},
|
|
||||||
|
|
||||||
cdiSettings: {
|
|
||||||
handler(val) {
|
|
||||||
this.$emit('update:cdiSettings', val);
|
|
||||||
},
|
|
||||||
deep: true,
|
|
||||||
immediate: true,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
setDefaultVolumeSnapshotClass() {
|
|
||||||
try {
|
|
||||||
const setting = this.$store.getters[`${ this.inStore }/byId`](HCI.SETTING, HCI_SETTING.CSI_DRIVER_CONFIG);
|
|
||||||
const config = JSON.parse(setting?.value || '{}');
|
|
||||||
const defaultClass = config?.[this.provisioner]?.volumeSnapshotClassName || null;
|
|
||||||
|
|
||||||
const allClasses = this.$store.getters[`${ this.inStore }/all`](VOLUME_SNAPSHOT_CLASS) || [];
|
|
||||||
const matched = allClasses.find((cls) => cls.name === defaultClass && cls.driver === this.provisioner);
|
|
||||||
|
|
||||||
this.cdiSettings.volumeSnapshotClass = matched?.name || null;
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to parse CSI config:', e); // eslint-disable-line no-console
|
|
||||||
this.cdiSettings.volumeSnapshotClass = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
initCDISettingsFromAnnotations() {
|
|
||||||
const annotations = this.value?.metadata?.annotations || {};
|
|
||||||
|
|
||||||
let volumeModeAccessModes = [];
|
|
||||||
const rawVolumeMode = annotations[HCI_ANNOTATIONS.VOLUME_MODE_ACCESS_MODES];
|
|
||||||
|
|
||||||
if (rawVolumeMode) {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(rawVolumeMode);
|
|
||||||
|
|
||||||
volumeModeAccessModes = Object.entries(parsed).map(([volumeMode, accessModes]) => ({
|
|
||||||
volumeMode,
|
|
||||||
accessModes: Array.isArray(accessModes) ? accessModes : [],
|
|
||||||
}));
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to parse annotation:', e); // eslint-disable-line no-console
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (volumeModeAccessModes.length) {
|
|
||||||
this.cdiSettings.volumeModeAccessModes = volumeModeAccessModes;
|
|
||||||
}
|
|
||||||
if (annotations[HCI_ANNOTATIONS.VOLUME_SNAPSHOT_CLASS]) {
|
|
||||||
this.cdiSettings.volumeSnapshotClass = annotations[HCI_ANNOTATIONS.VOLUME_SNAPSHOT_CLASS];
|
|
||||||
}
|
|
||||||
if (annotations[HCI_ANNOTATIONS.CLONE_STRATEGY]) {
|
|
||||||
this.cdiSettings.cloneStrategy = annotations[HCI_ANNOTATIONS.CLONE_STRATEGY];
|
|
||||||
}
|
|
||||||
if (annotations[HCI_ANNOTATIONS.FILESYSTEM_OVERHEAD]) {
|
|
||||||
this.cdiSettings.filesystemOverhead = annotations[HCI_ANNOTATIONS.FILESYSTEM_OVERHEAD];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
resetCdiSettings() {
|
|
||||||
this.cdiSettings.volumeModeAccessModes = [this.defaultAddValue];
|
|
||||||
this.cdiSettings.volumeSnapshotClass = null;
|
|
||||||
this.cdiSettings.cloneStrategy = null;
|
|
||||||
this.cdiSettings.filesystemOverhead = null;
|
|
||||||
},
|
|
||||||
|
|
||||||
validateFilesystemOverhead(e) {
|
|
||||||
const val = e.target.value;
|
|
||||||
|
|
||||||
this.cdiSettings.filesystemOverhead = val;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<ArrayList
|
|
||||||
v-model:value="cdiSettings.volumeModeAccessModes"
|
|
||||||
:initial-empty-row="true"
|
|
||||||
:show-header="true"
|
|
||||||
:mode="mode"
|
|
||||||
:title="t('harvester.storage.cdiSettings.volumeModeAccessModes.label')"
|
|
||||||
:add-label="t('harvester.storage.cdiSettings.volumeModeAccessModes.add')"
|
|
||||||
:default-add-value="defaultAddValue"
|
|
||||||
:protip="t('harvester.storage.cdiSettings.volumeModeAccessModes.tooltip')"
|
|
||||||
:add-disabled="allVolumeModesSelected"
|
|
||||||
>
|
|
||||||
<template #column-headers>
|
|
||||||
<div class="column-headers">
|
|
||||||
<div
|
|
||||||
class="row"
|
|
||||||
:class="{ custom: isCustomClass }"
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
class="col span-3 value text-label mb-10"
|
|
||||||
for="volumeMode"
|
|
||||||
>
|
|
||||||
{{ t('harvester.storage.cdiSettings.volumeModeAccessModes.volumeMode') }}
|
|
||||||
</label>
|
|
||||||
<label
|
|
||||||
class="col span-9 value text-label mb-10"
|
|
||||||
for="accessModes"
|
|
||||||
>
|
|
||||||
{{ t('harvester.storage.cdiSettings.volumeModeAccessModes.accessModes.label') }}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #columns="{ row }">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col span-3">
|
|
||||||
<LabeledSelect
|
|
||||||
id="volumeMode"
|
|
||||||
v-model:value="row.value.volumeMode"
|
|
||||||
:mode="mode"
|
|
||||||
:options="volumeModeOptions"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
id="accessModes"
|
|
||||||
class="col span-9"
|
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
v-for="opt in accessModeOptions"
|
|
||||||
:key="opt.value"
|
|
||||||
v-model:value="row.value.accessModes"
|
|
||||||
:value-when-true="opt.value"
|
|
||||||
:label="opt.label"
|
|
||||||
type="checkbox"
|
|
||||||
:mode="mode"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</ArrayList>
|
|
||||||
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="cdiSettings.volumeSnapshotClass"
|
|
||||||
class="select mt-20 mb-20"
|
|
||||||
:label="t('harvester.storage.cdiSettings.volumeSnapshotClass.label')"
|
|
||||||
:tooltip="t('harvester.storage.cdiSettings.volumeSnapshotClass.tooltip')"
|
|
||||||
:mode="mode"
|
|
||||||
:options="volumeSnapshotClassOptions"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="cdiSettings.cloneStrategy"
|
|
||||||
class="select mb-20"
|
|
||||||
:label="t('harvester.storage.cdiSettings.cloneStrategy.label')"
|
|
||||||
:tooltip="t('harvester.storage.cdiSettings.cloneStrategy.tooltip')"
|
|
||||||
:mode="mode"
|
|
||||||
:options="cloneStrategyOptions"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="cdiSettings.filesystemOverhead"
|
|
||||||
class="select mb-20"
|
|
||||||
:label="t('harvester.storage.cdiSettings.fileSystemOverhead.label')"
|
|
||||||
:tooltip="t('harvester.storage.cdiSettings.fileSystemOverhead.tooltip')"
|
|
||||||
:mode="mode"
|
|
||||||
type="number"
|
|
||||||
:min="0"
|
|
||||||
:max="1"
|
|
||||||
:step="0.001"
|
|
||||||
:placeholder="t('harvester.storage.cdiSettings.fileSystemOverhead.placeholder')"
|
|
||||||
:status="isFilesystemOverheadValid ? null : 'error'"
|
|
||||||
@input="validateFilesystemOverhead"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.column-headers .row.custom {
|
|
||||||
max-width: calc(100% - 75px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.row {
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select {
|
|
||||||
max-width: 480px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
<script>
|
<script>
|
||||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
import CruResource from '@shell/components/CruResource';
|
import CruResource from '@shell/components/CruResource';
|
||||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||||
@@ -7,9 +6,11 @@ import ArrayList from '@shell/components/form/ArrayList';
|
|||||||
import Tab from '@shell/components/Tabbed/Tab';
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
import Tabbed from '@shell/components/Tabbed';
|
import Tabbed from '@shell/components/Tabbed';
|
||||||
import { RadioGroup } from '@components/Form/Radio';
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
||||||
|
import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
import Loading from '@shell/components/Loading';
|
import Loading from '@shell/components/Loading';
|
||||||
|
|
||||||
import { _CREATE, _VIEW } from '@shell/config/query-params';
|
import { _CREATE, _VIEW } from '@shell/config/query-params';
|
||||||
import { mapFeature, UNSUPPORTED_STORAGE_DRIVERS } from '@shell/store/features';
|
import { mapFeature, UNSUPPORTED_STORAGE_DRIVERS } from '@shell/store/features';
|
||||||
import {
|
import {
|
||||||
@@ -21,11 +22,11 @@ import { CSI_DRIVER } from '../../types';
|
|||||||
import Tags from '../../components/DiskTags';
|
import Tags from '../../components/DiskTags';
|
||||||
import { DATA_ENGINE_V1, DATA_ENGINE_V2 } from '../../models/harvester/persistentvolumeclaim';
|
import { DATA_ENGINE_V1, DATA_ENGINE_V2 } from '../../models/harvester/persistentvolumeclaim';
|
||||||
import { LVM_DRIVER } from '../../models/harvester/storage.k8s.io.storageclass';
|
import { LVM_DRIVER } from '../../models/harvester/storage.k8s.io.storageclass';
|
||||||
import CDISettings from './CDISettings';
|
|
||||||
|
const LONGHORN_V2_DATA_ENGINE = 'longhorn-system/v2-data-engine';
|
||||||
|
|
||||||
export const LVM_TOPOLOGY_LABEL = 'topology.lvm.csi/node';
|
export const LVM_TOPOLOGY_LABEL = 'topology.lvm.csi/node';
|
||||||
|
|
||||||
const LONGHORN_V2_DATA_ENGINE = 'longhorn-system/v2-data-engine';
|
|
||||||
const VOLUME_BINDING_MODE_IMMEDIATE = 'Immediate';
|
const VOLUME_BINDING_MODE_IMMEDIATE = 'Immediate';
|
||||||
const VOLUME_BINDING_MODE_WAIT = 'WaitForFirstConsumer';
|
const VOLUME_BINDING_MODE_WAIT = 'WaitForFirstConsumer';
|
||||||
|
|
||||||
@@ -45,7 +46,6 @@ export default {
|
|||||||
Tabbed,
|
Tabbed,
|
||||||
Loading,
|
Loading,
|
||||||
Tags,
|
Tags,
|
||||||
CDISettings,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [CreateEditView],
|
mixins: [CreateEditView],
|
||||||
@@ -118,7 +118,6 @@ export default {
|
|||||||
key: '',
|
key: '',
|
||||||
values: [],
|
values: [],
|
||||||
},
|
},
|
||||||
cdiSettings: {},
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -126,10 +125,10 @@ export default {
|
|||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
const hash = {
|
const hash = {
|
||||||
namespaces: this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE }),
|
namespaces: this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE }),
|
||||||
storages: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }),
|
storages: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }),
|
||||||
longhornNodes: this.$store.dispatch(`${ inStore }/findAll`, { type: LONGHORN.NODES }),
|
longhornNodes: this.$store.dispatch(`${ inStore }/findAll`, { type: LONGHORN.NODES }),
|
||||||
csiDrivers: this.$store.dispatch(`${ inStore }/findAll`, { type: CSI_DRIVER }),
|
csiDrivers: this.$store.dispatch(`${ inStore }/findAll`, { type: CSI_DRIVER })
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.value.longhornV2LVMSupport) {
|
if (this.value.longhornV2LVMSupport) {
|
||||||
@@ -199,14 +198,6 @@ export default {
|
|||||||
|
|
||||||
return v2DataEngine.value === 'true' ? DATA_ENGINE_V2 : DATA_ENGINE_V1;
|
return v2DataEngine.value === 'true' ? DATA_ENGINE_V2 : DATA_ENGINE_V1;
|
||||||
},
|
},
|
||||||
|
|
||||||
isCDISettingsFeatureEnabled() {
|
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('cdiSettings');
|
|
||||||
},
|
|
||||||
|
|
||||||
shouldShowCDISettingsTab() {
|
|
||||||
return this.isCDISettingsFeatureEnabled && this.provisioner !== `${ LONGHORN_DRIVER }_${ DATA_ENGINE_V1 }`;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
@@ -259,10 +250,6 @@ export default {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.formatAllowedTopoloties();
|
this.formatAllowedTopoloties();
|
||||||
|
|
||||||
if (this.shouldShowCDISettingsTab) {
|
|
||||||
this.formatCDISettings();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
formatAllowedTopoloties() {
|
formatAllowedTopoloties() {
|
||||||
@@ -284,42 +271,6 @@ export default {
|
|||||||
if (matchLabelExpressions.length > 0) {
|
if (matchLabelExpressions.length > 0) {
|
||||||
this.value.allowedTopologies = [{ matchLabelExpressions: [...matchLabelExpressions, ...lvmMatchExpression] }];
|
this.value.allowedTopologies = [{ matchLabelExpressions: [...matchLabelExpressions, ...lvmMatchExpression] }];
|
||||||
}
|
}
|
||||||
},
|
|
||||||
formatCDISettings() {
|
|
||||||
const annotations = this.value.metadata.annotations || {};
|
|
||||||
const volumeModeAccessModes = {};
|
|
||||||
|
|
||||||
this.cdiSettings.volumeModeAccessModes.forEach((setting) => {
|
|
||||||
if (setting.volumeMode && Array.isArray(setting.accessModes) && setting.accessModes.length > 0) {
|
|
||||||
volumeModeAccessModes[setting.volumeMode] = setting.accessModes;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (Object.keys(volumeModeAccessModes).length > 0) {
|
|
||||||
annotations[HCI_ANNOTATIONS.VOLUME_MODE_ACCESS_MODES] = JSON.stringify(volumeModeAccessModes);
|
|
||||||
} else {
|
|
||||||
delete annotations[HCI_ANNOTATIONS.VOLUME_MODE_ACCESS_MODES];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.cdiSettings.volumeSnapshotClass) {
|
|
||||||
annotations[HCI_ANNOTATIONS.VOLUME_SNAPSHOT_CLASS] = this.cdiSettings.volumeSnapshotClass;
|
|
||||||
} else {
|
|
||||||
delete annotations[HCI_ANNOTATIONS.VOLUME_SNAPSHOT_CLASS];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.cdiSettings.cloneStrategy) {
|
|
||||||
annotations[HCI_ANNOTATIONS.CLONE_STRATEGY] = this.cdiSettings.cloneStrategy;
|
|
||||||
} else {
|
|
||||||
delete annotations[HCI_ANNOTATIONS.CLONE_STRATEGY];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.cdiSettings.filesystemOverhead) {
|
|
||||||
annotations[HCI_ANNOTATIONS.FILESYSTEM_OVERHEAD] = this.cdiSettings.filesystemOverhead;
|
|
||||||
} else {
|
|
||||||
delete annotations[HCI_ANNOTATIONS.FILESYSTEM_OVERHEAD];
|
|
||||||
}
|
|
||||||
|
|
||||||
this.value.metadata.annotations = annotations;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -336,7 +287,7 @@ export default {
|
|||||||
:validation-passed="true"
|
:validation-passed="true"
|
||||||
:apply-hooks="applyHooks"
|
:apply-hooks="applyHooks"
|
||||||
:errors="errors"
|
:errors="errors"
|
||||||
@error="e=>errors=e"
|
@error="e=>errors = e"
|
||||||
@finish="save"
|
@finish="save"
|
||||||
@cancel="done"
|
@cancel="done"
|
||||||
>
|
>
|
||||||
@@ -454,19 +405,6 @@ export default {
|
|||||||
</template>
|
</template>
|
||||||
</ArrayList>
|
</ArrayList>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
|
||||||
v-if="shouldShowCDISettingsTab"
|
|
||||||
name="cdiSettings"
|
|
||||||
:label="t('harvester.storage.cdiSettings.label')"
|
|
||||||
:weight="-2"
|
|
||||||
>
|
|
||||||
<CDISettings
|
|
||||||
v-model:cdi-settings="cdiSettings"
|
|
||||||
:value="value"
|
|
||||||
:mode="mode"
|
|
||||||
:provisioner="value.provisioner"
|
|
||||||
/>
|
|
||||||
</Tab>
|
|
||||||
</Tabbed>
|
</Tabbed>
|
||||||
</CruResource>
|
</CruResource>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export default {
|
|||||||
diskSelector: null,
|
diskSelector: null,
|
||||||
nodeSelector: null,
|
nodeSelector: null,
|
||||||
encrypted: 'false',
|
encrypted: 'false',
|
||||||
migratable: this.value.thirdPartyStorageFeatureEnabled ? 'true' : 'false',
|
migratable: 'false',
|
||||||
dataEngine: DATA_ENGINE_V2
|
dataEngine: DATA_ENGINE_V2
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -318,7 +318,7 @@ export default {
|
|||||||
:label="t('harvester.storage.parameters.migratable.label')"
|
:label="t('harvester.storage.parameters.migratable.label')"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:options="migratableOptions"
|
:options="migratableOptions"
|
||||||
:disabled="!value.thirdPartyStorageFeatureEnabled"
|
:disabled="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mt-20">
|
<div class="row mt-20">
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import { VM_IMAGE_FILE_FORMAT } from '../validators/vm-image';
|
|||||||
import { OS } from '../mixins/harvester-vm';
|
import { OS } from '../mixins/harvester-vm';
|
||||||
import { HCI } from '../types';
|
import { HCI } from '../types';
|
||||||
import { LVM_DRIVER } from '../models/harvester/storage.k8s.io.storageclass';
|
import { LVM_DRIVER } from '../models/harvester/storage.k8s.io.storageclass';
|
||||||
import { isInternalStorageClass } from '../utils/storage-class';
|
|
||||||
|
|
||||||
const ENCRYPT = 'encrypt';
|
const ENCRYPT = 'encrypt';
|
||||||
const DECRYPT = 'decrypt';
|
const DECRYPT = 'decrypt';
|
||||||
@@ -25,7 +24,6 @@ const CLONE = 'clone';
|
|||||||
const DOWNLOAD = 'download';
|
const DOWNLOAD = 'download';
|
||||||
const UPLOAD = 'upload';
|
const UPLOAD = 'upload';
|
||||||
const rawORqcow2 = 'raw_qcow2';
|
const rawORqcow2 = 'raw_qcow2';
|
||||||
const LONGHORN = 'longhorn';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'EditImage',
|
name: 'EditImage',
|
||||||
@@ -63,10 +61,10 @@ export default {
|
|||||||
storageClasses: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }),
|
storageClasses: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }),
|
||||||
});
|
});
|
||||||
|
|
||||||
this['storageClassName'] = this.storageClassName || this.defaultStorageClassName();
|
const defaultStorage = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS).find((s) => s.isDefault);
|
||||||
this.images = this.$store.getters[`${ inStore }/all`](HCI.IMAGE);
|
|
||||||
|
|
||||||
this.storages = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS);
|
this['storageClassName'] = this.storageClassName || defaultStorage?.metadata?.name || 'longhorn';
|
||||||
|
this.images = this.$store.getters[`${ inStore }/all`](HCI.IMAGE);
|
||||||
|
|
||||||
const { securityParameters } = this.value.spec;
|
const { securityParameters } = this.value.spec;
|
||||||
|
|
||||||
@@ -102,16 +100,14 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
selectedImage: null,
|
selectedImage: null,
|
||||||
storageClasses: [],
|
images: [],
|
||||||
images: [],
|
url: this.value.spec.url,
|
||||||
url: this.value.spec.url,
|
files: [],
|
||||||
files: [],
|
resource: '',
|
||||||
resource: '',
|
headers: {},
|
||||||
headers: {},
|
fileUrl: '',
|
||||||
fileUrl: '',
|
file: '',
|
||||||
file: '',
|
|
||||||
HCI_ANNOTATIONS,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -148,39 +144,18 @@ export default {
|
|||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
encryptedStorageClasses() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
const storages = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS);
|
|
||||||
|
|
||||||
return storages.filter((s) => s.isEncrypted);
|
|
||||||
},
|
|
||||||
|
|
||||||
nonEncryptedStorageClasses() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
const storages = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS);
|
|
||||||
|
|
||||||
return storages.filter((s) => !s.isEncrypted);
|
|
||||||
},
|
|
||||||
|
|
||||||
storageClassOptions() {
|
storageClassOptions() {
|
||||||
const storages = this.value.spec?.securityParameters?.cryptoOperation === ENCRYPT ? this.encryptedStorageClasses : this.nonEncryptedStorageClasses;
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
const filteredStorages = this.value.thirdPartyStorageFeatureEnabled ? storages.filter((s) => !s.parameters?.backingImage) : storages
|
const storages = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS);
|
||||||
.filter((s) => !s.parameters?.backingImage && s.provisioner !== LVM_DRIVER) ;
|
|
||||||
|
|
||||||
return filteredStorages
|
return storages
|
||||||
|
.filter((s) => !s.parameters?.backingImage && s.provisioner !== LVM_DRIVER) // Lvm storage is not supported.
|
||||||
.map((s) => {
|
.map((s) => {
|
||||||
let label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||||
let disabled = false;
|
|
||||||
|
|
||||||
if (isInternalStorageClass(s.name)) {
|
|
||||||
label += ` (${ this.t('harvester.storage.internal.label') })`;
|
|
||||||
disabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label,
|
label,
|
||||||
value: s.name,
|
value: s.name,
|
||||||
disabled
|
|
||||||
};
|
};
|
||||||
}) || [];
|
}) || [];
|
||||||
},
|
},
|
||||||
@@ -192,9 +167,6 @@ export default {
|
|||||||
|
|
||||||
set(nue) {
|
set(nue) {
|
||||||
this.value.metadata.annotations[HCI_ANNOTATIONS.STORAGE_CLASS] = nue;
|
this.value.metadata.annotations[HCI_ANNOTATIONS.STORAGE_CLASS] = nue;
|
||||||
if (this.value.thirdPartyStorageFeatureEnabled) {
|
|
||||||
this.value.spec.targetStorageClassName = nue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
sourceImageOptions() {
|
sourceImageOptions() {
|
||||||
@@ -267,20 +239,6 @@ export default {
|
|||||||
this.$refs.file.value = null;
|
this.$refs.file.value = null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'value.spec.securityParameters.cryptoOperation'() {
|
|
||||||
if (this.value.spec?.securityParameters?.cryptoOperation === ENCRYPT) {
|
|
||||||
this.storageClassName = this.encryptedStorageClasses[0]?.name || '';
|
|
||||||
} else { // URL / FILE / DECRYPT should use default storage class
|
|
||||||
this.storageClassName = this.defaultStorageClassName();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'storageClassName'(neu) {
|
|
||||||
const storageClass = this.storages.find((s) => s.id === neu);
|
|
||||||
|
|
||||||
if (storageClass && this.value.thirdPartyStorageFeatureEnabled) {
|
|
||||||
this.value.spec.backend = storageClass.isLonghornV1 ? 'backingimage' : 'cdi';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@@ -427,18 +385,6 @@ export default {
|
|||||||
return str.toLowerCase().includes(os.value.toLowerCase()) ? os.value : false;
|
return str.toLowerCase().includes(os.value.toLowerCase()) ? os.value : false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
|
||||||
|
|
||||||
defaultStorageClassName() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
const defaultStorage = this.$store.getters[`${ inStore }/all`](STORAGE_CLASS).find((s) => s.isDefault);
|
|
||||||
|
|
||||||
if (!defaultStorage) {
|
|
||||||
return LONGHORN;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if default sc is encrypted, use longhorn as default
|
|
||||||
return defaultStorage.isEncrypted ? LONGHORN : defaultStorage?.metadata?.name;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -604,8 +550,6 @@ export default {
|
|||||||
:pad-left="false"
|
:pad-left="false"
|
||||||
:read-allowed="false"
|
:read-allowed="false"
|
||||||
:value-can-be-empty="true"
|
:value-can-be-empty="true"
|
||||||
:toggle-filter="true"
|
|
||||||
:protected-keys="[HCI_ANNOTATIONS.IMAGE_DISPLAY_NAME]"
|
|
||||||
@focusKey="focusKey"
|
@focusKey="focusKey"
|
||||||
@update:value="value.setLabels($event)"
|
@update:value="value.setLabels($event)"
|
||||||
>
|
>
|
||||||
@@ -622,7 +566,7 @@ export default {
|
|||||||
<input
|
<input
|
||||||
v-else
|
v-else
|
||||||
v-model="row[valueName]"
|
v-model="row[valueName]"
|
||||||
:disabled="isView || row[keyName] === HCI_ANNOTATIONS.IMAGE_DISPLAY_NAME"
|
:disabled="isView"
|
||||||
:type="'text'"
|
:type="'text'"
|
||||||
:placeholder="t('keyValue.valuePlaceholder')"
|
:placeholder="t('keyValue.valuePlaceholder')"
|
||||||
autocorrect="off"
|
autocorrect="off"
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ import { Checkbox } from '@components/Form/Checkbox';
|
|||||||
import CruResource from '@shell/components/CruResource';
|
import CruResource from '@shell/components/CruResource';
|
||||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
import { Banner } from '@components/Banner';
|
import Labels from '@shell/components/form/Labels';
|
||||||
import KeyValue from '@shell/components/form/KeyValue';
|
import KeyValue from '@shell/components/form/KeyValue';
|
||||||
import NodeScheduling from '@shell/components/form/NodeScheduling';
|
import NodeScheduling from '@shell/components/form/NodeScheduling';
|
||||||
import PodAffinity from '@shell/components/form/PodAffinity';
|
import PodAffinity from '@shell/components/form/PodAffinity';
|
||||||
import UnitInput from '@shell/components/form/UnitInput';
|
import UnitInput from '@shell/components/form/UnitInput';
|
||||||
|
|
||||||
import { randomStr } from '@shell/utils/string';
|
import { randomStr } from '@shell/utils/string';
|
||||||
import { _CONFIG, _EDIT, _VIEW } from '@shell/config/query-params';
|
import { _CONFIG, _EDIT, _VIEW } from '@shell/config/query-params';
|
||||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
@@ -46,7 +47,7 @@ export default {
|
|||||||
PodAffinity,
|
PodAffinity,
|
||||||
Reserved,
|
Reserved,
|
||||||
UnitInput,
|
UnitInput,
|
||||||
Banner,
|
Labels,
|
||||||
KeyValue,
|
KeyValue,
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -189,16 +190,6 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.value.spec['templateId'] = `${ namespace }/${ name }`;
|
this.value.spec['templateId'] = `${ namespace }/${ name }`;
|
||||||
|
|
||||||
// inherit labels and annotations so the VM gets them when created from the template
|
|
||||||
this.value.spec.vm.metadata.labels = {
|
|
||||||
...this.value.spec.vm.metadata.labels,
|
|
||||||
...this.value.metadata.labels
|
|
||||||
};
|
|
||||||
this.value.spec.vm.metadata.annotations = {
|
|
||||||
...this.value.spec.vm.metadata.annotations,
|
|
||||||
...this.value.metadata.annotations
|
|
||||||
};
|
|
||||||
const res = await this.value.save();
|
const res = await this.value.save();
|
||||||
|
|
||||||
await this.saveSecret(res);
|
await this.saveSecret(res);
|
||||||
@@ -229,7 +220,6 @@ export default {
|
|||||||
:errors="errors"
|
:errors="errors"
|
||||||
:apply-hooks="applyHooks"
|
:apply-hooks="applyHooks"
|
||||||
@finish="saveVMT"
|
@finish="saveVMT"
|
||||||
@error="e=>errors=e"
|
|
||||||
>
|
>
|
||||||
<NameNsDescription
|
<NameNsDescription
|
||||||
:value="templateValue"
|
:value="templateValue"
|
||||||
@@ -259,9 +249,6 @@ export default {
|
|||||||
<CpuMemory
|
<CpuMemory
|
||||||
:cpu="cpu"
|
:cpu="cpu"
|
||||||
:memory="memory"
|
:memory="memory"
|
||||||
:max-cpu="maxCpu"
|
|
||||||
:max-memory="maxMemory"
|
|
||||||
:enable-hot-plug="cpuMemoryHotplugEnabled"
|
|
||||||
:disabled="isConfig"
|
:disabled="isConfig"
|
||||||
@updateCpuMemory="updateCpuMemory"
|
@updateCpuMemory="updateCpuMemory"
|
||||||
/>
|
/>
|
||||||
@@ -334,64 +321,34 @@ export default {
|
|||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab
|
<Tab
|
||||||
name="labels"
|
:name="t('generic.labels')"
|
||||||
:label="t('generic.labels')"
|
:label="t('harvester.tab.instanceLabel')"
|
||||||
:weight="-9"
|
:weight="-5"
|
||||||
>
|
>
|
||||||
<Banner color="info">
|
<Labels
|
||||||
<t k="harvester.virtualMachine.labels.banner" />
|
:default-container-class="'labels-and-annotations-container'"
|
||||||
</Banner>
|
:value="value"
|
||||||
<KeyValue
|
|
||||||
key="labels"
|
|
||||||
:value="value.labels"
|
|
||||||
:add-label="t('labels.addLabel')"
|
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:read-allowed="false"
|
:display-side-by-side="false"
|
||||||
:value-can-be-empty="true"
|
:show-annotations="false"
|
||||||
@update:value="value.setLabels($event)"
|
:show-label-title="false"
|
||||||
/>
|
>
|
||||||
|
<template #labels="{toggler}">
|
||||||
|
<KeyValue
|
||||||
|
key="labels"
|
||||||
|
:value="value.instanceLabels"
|
||||||
|
:protected-keys="value.systemLabels || []"
|
||||||
|
:toggle-filter="toggler"
|
||||||
|
:add-label="t('labels.addLabel')"
|
||||||
|
:mode="mode"
|
||||||
|
:read-allowed="false"
|
||||||
|
:value-can-be-empty="true"
|
||||||
|
@input="value.setInstanceLabels($event)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Labels>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab
|
|
||||||
name="instanceLabel"
|
|
||||||
:label="t('harvester.tab.instanceLabel')"
|
|
||||||
:weight="-10"
|
|
||||||
>
|
|
||||||
<Banner color="info">
|
|
||||||
<t k="harvester.virtualMachine.instanceLabels.banner" />
|
|
||||||
</Banner>
|
|
||||||
<KeyValue
|
|
||||||
key="instance-labels"
|
|
||||||
:value="value.instanceLabels"
|
|
||||||
:protected-keys="value.systemLabels || []"
|
|
||||||
:toggle-filter="toggler"
|
|
||||||
:add-label="t('labels.addLabel')"
|
|
||||||
:mode="mode"
|
|
||||||
:read-allowed="false"
|
|
||||||
:value-can-be-empty="true"
|
|
||||||
@update:value="value.setInstanceLabels($event)"
|
|
||||||
/>
|
|
||||||
</Tab>
|
|
||||||
<Tab
|
|
||||||
name="annotations"
|
|
||||||
:label="t('harvester.tab.annotations')"
|
|
||||||
:weight="-11"
|
|
||||||
>
|
|
||||||
<Banner color="info">
|
|
||||||
<t k="harvester.virtualMachine.annotations.banner" />
|
|
||||||
</Banner>
|
|
||||||
<KeyValue
|
|
||||||
key="annotations"
|
|
||||||
:value="value.annotations"
|
|
||||||
:protected-keys="value.systemAnnotations || []"
|
|
||||||
:toggle-filter="true"
|
|
||||||
:add-label="t('labels.addAnnotation')"
|
|
||||||
:mode="mode"
|
|
||||||
:read-allowed="false"
|
|
||||||
:value-can-be-empty="true"
|
|
||||||
@update:value="value.setAnnotations($event)"
|
|
||||||
/>
|
|
||||||
</Tab>
|
|
||||||
<Tab
|
<Tab
|
||||||
name="advanced"
|
name="advanced"
|
||||||
:label="t('harvester.tab.advanced')"
|
:label="t('harvester.tab.advanced')"
|
||||||
@@ -477,7 +434,7 @@ export default {
|
|||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
v-if="value.cpuPinningFeatureEnabled"
|
v-if="value.cpuPinningFeatureEnabled"
|
||||||
v-model:value="cpuPinning"
|
v-model="cpuPinning"
|
||||||
class="check"
|
class="check"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
tooltip-key="harvester.virtualMachine.cpuPinning.tooltip"
|
tooltip-key="harvester.virtualMachine.cpuPinning.tooltip"
|
||||||
@@ -508,29 +465,11 @@ export default {
|
|||||||
:mode="mode"
|
:mode="mode"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Checkbox
|
|
||||||
v-if="value.tpmPersistentStateFeatureEnabled && tpmEnabled"
|
|
||||||
v-model:value="tpmPersistentStateEnabled"
|
|
||||||
class="check"
|
|
||||||
type="checkbox"
|
|
||||||
:label="t('harvester.virtualMachine.advancedOptions.tpmPersistentState')"
|
|
||||||
:mode="mode"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
v-model:value="efiEnabled"
|
v-model:value="efiEnabled"
|
||||||
class="check"
|
class="check"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:label="t('harvester.virtualMachine.advancedOptions.efiEnabled')"
|
:label="t('harvester.virtualMachine.efiEnabled')"
|
||||||
:mode="mode"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Checkbox
|
|
||||||
v-if="value.efiPersistentStateFeatureEnabled && efiEnabled"
|
|
||||||
v-model:value="efiPersistentStateEnabled"
|
|
||||||
class="check"
|
|
||||||
type="checkbox"
|
|
||||||
:label="t('harvester.virtualMachine.advancedOptions.efiPersistentState')"
|
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -539,7 +478,7 @@ export default {
|
|||||||
v-model:value="secureBoot"
|
v-model:value="secureBoot"
|
||||||
class="check"
|
class="check"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:label="t('harvester.virtualMachine.advancedOptions.secureBoot')"
|
:label="t('harvester.virtualMachine.secureBoot')"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|||||||
@@ -21,9 +21,6 @@ import { InterfaceOption, VOLUME_DATA_SOURCE_KIND } from '../config/harvester-ma
|
|||||||
import { HCI, VOLUME_SNAPSHOT } from '../types';
|
import { HCI, VOLUME_SNAPSHOT } from '../types';
|
||||||
import { LVM_DRIVER } from '../models/harvester/storage.k8s.io.storageclass';
|
import { LVM_DRIVER } from '../models/harvester/storage.k8s.io.storageclass';
|
||||||
import { DATA_ENGINE_V2 } from '../models/harvester/persistentvolumeclaim';
|
import { DATA_ENGINE_V2 } from '../models/harvester/persistentvolumeclaim';
|
||||||
import { GIBIBYTE } from '../utils/unit';
|
|
||||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
|
||||||
import { isInternalStorageClass } from '../utils/storage-class';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterVolume',
|
name: 'HarvesterVolume',
|
||||||
@@ -75,8 +72,7 @@ export default {
|
|||||||
|
|
||||||
data() {
|
data() {
|
||||||
if (this.mode === _CREATE) {
|
if (this.mode === _CREATE) {
|
||||||
// default volumeMode to Block
|
this.value.spec.volumeMode = 'Block';
|
||||||
this.value.spec.volumeMode = VOLUME_MODE.BLOCK;
|
|
||||||
this.value.spec.accessModes = ['ReadWriteMany'];
|
this.value.spec.accessModes = ['ReadWriteMany'];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +86,6 @@ export default {
|
|||||||
imageId,
|
imageId,
|
||||||
snapshots: [],
|
snapshots: [],
|
||||||
images: [],
|
images: [],
|
||||||
GIBIBYTE
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -129,10 +124,6 @@ export default {
|
|||||||
return InterfaceOption;
|
return InterfaceOption;
|
||||||
},
|
},
|
||||||
|
|
||||||
volumeModeOptions() {
|
|
||||||
return Object.values(VOLUME_MODE);
|
|
||||||
},
|
|
||||||
|
|
||||||
imageOption() {
|
imageOption() {
|
||||||
return sortBy(
|
return sortBy(
|
||||||
this.images
|
this.images
|
||||||
@@ -186,39 +177,13 @@ export default {
|
|||||||
return this.$store.getters[`${ inStore }/all`](STORAGE_CLASS);
|
return this.$store.getters[`${ inStore }/all`](STORAGE_CLASS);
|
||||||
},
|
},
|
||||||
|
|
||||||
isLonghornStorageClass() {
|
|
||||||
const selectedSC = this.storageClasses.find((sc) => sc.name === this.value?.spec?.storageClassName) || {};
|
|
||||||
|
|
||||||
return selectedSC && selectedSC.isLonghorn;
|
|
||||||
},
|
|
||||||
|
|
||||||
showVolumeMode() {
|
|
||||||
// we won't let user choose volumeMode if source = vmimage
|
|
||||||
if (!this.value.thirdPartyStorageFeatureEnabled || this.isVMImage || !!this.value?.spec?.storageClassName === false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isLonghornStorageClass) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
storageClassOptions() {
|
storageClassOptions() {
|
||||||
return this.storageClasses.filter((s) => !s.parameters?.backingImage).map((s) => {
|
return this.storageClasses.filter((s) => !s.parameters?.backingImage).map((s) => {
|
||||||
let label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||||
let disabled = false;
|
|
||||||
|
|
||||||
if (isInternalStorageClass(s.name)) {
|
|
||||||
label += ` (${ this.t('harvester.storage.internal.label') })`;
|
|
||||||
disabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label,
|
label,
|
||||||
value: s.name,
|
value: s.name,
|
||||||
disabled
|
|
||||||
};
|
};
|
||||||
}) || [];
|
}) || [];
|
||||||
},
|
},
|
||||||
@@ -270,59 +235,19 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
|
||||||
'source'(neu) {
|
|
||||||
if (neu === 'url') {
|
|
||||||
this.setBlockVolumeMode();
|
|
||||||
this.deleteVolumeForVmAnnotation();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'value.spec.storageClassName'() {
|
|
||||||
if (this.isLonghornStorageClass) {
|
|
||||||
this.setBlockVolumeMode();
|
|
||||||
this.deleteVolumeForVmAnnotation();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'value.spec.volumeMode'(neu) {
|
|
||||||
if (neu === VOLUME_MODE.FILE_SYSTEM) {
|
|
||||||
this.setVolumeForVmAnnotation();
|
|
||||||
} else if (neu === VOLUME_MODE.BLOCK ) {
|
|
||||||
this.deleteVolumeForVmAnnotation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
setBlockVolumeMode() {
|
|
||||||
this.value.spec.volumeMode = VOLUME_MODE.BLOCK;
|
|
||||||
},
|
|
||||||
|
|
||||||
setVolumeForVmAnnotation() {
|
|
||||||
this.value.setAnnotation(HCI_ANNOTATIONS.VOLUME_FOR_VM, 'true');
|
|
||||||
},
|
|
||||||
|
|
||||||
deleteVolumeForVmAnnotation() {
|
|
||||||
if (this.value?.metadata?.annotations?.[HCI_ANNOTATIONS.VOLUME_FOR_VM]) {
|
|
||||||
delete this.value.metadata.annotations[HCI_ANNOTATIONS.VOLUME_FOR_VM];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getAccessMode() {
|
getAccessMode() {
|
||||||
if (!this.longhornV2LVMSupport) {
|
if (!this.longhornV2LVMSupport) {
|
||||||
return ['ReadWriteMany'];
|
return ['ReadWriteMany'];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.value?.spec?.accessModes && this.value?.spec?.accessModes?.length > 0) {
|
|
||||||
return this.value.spec.accessModes;
|
|
||||||
}
|
|
||||||
|
|
||||||
const storageClassName = this.value.spec.storageClassName;
|
const storageClassName = this.value.spec.storageClassName;
|
||||||
const storageClass = this.storageClasses.find((sc) => sc.name === storageClassName);
|
const storageClass = this.storageClasses.find((sc) => sc.name === storageClassName);
|
||||||
|
|
||||||
let readWriteOnce = this.value.isLvm || (!this.value.thirdPartyStorageFeatureEnabled && this.value.isLonghornV2);
|
let readWriteOnce = this.value.isLvm || this.value.isLonghornV2;
|
||||||
|
|
||||||
if (storageClass) {
|
if (storageClass) {
|
||||||
readWriteOnce = storageClass.provisioner === LVM_DRIVER || (!this.value.thirdPartyStorageFeatureEnabled && storageClass.parameters?.dataEngine === DATA_ENGINE_V2);
|
readWriteOnce = storageClass.provisioner === LVM_DRIVER || storageClass.parameters?.dataEngine === DATA_ENGINE_V2;
|
||||||
}
|
}
|
||||||
|
|
||||||
return readWriteOnce ? ['ReadWriteOnce'] : ['ReadWriteMany'];
|
return readWriteOnce ? ['ReadWriteOnce'] : ['ReadWriteMany'];
|
||||||
@@ -363,7 +288,7 @@ export default {
|
|||||||
const imageSize = Math.max(imageResource?.status?.size, imageResource?.status?.virtualSize);
|
const imageSize = Math.max(imageResource?.status?.size, imageResource?.status?.virtualSize);
|
||||||
|
|
||||||
if (imageSize) {
|
if (imageSize) {
|
||||||
this.storage = `${ Math.ceil(imageSize / 1024 / 1024 / 1024) }${ GIBIBYTE }`;
|
this.storage = `${ Math.ceil(imageSize / 1024 / 1024 / 1024) }Gi`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.update();
|
this.update();
|
||||||
@@ -386,12 +311,10 @@ export default {
|
|||||||
:generate-yaml="generateYaml"
|
:generate-yaml="generateYaml"
|
||||||
:apply-hooks="applyHooks"
|
:apply-hooks="applyHooks"
|
||||||
@finish="save"
|
@finish="save"
|
||||||
@error="e=>errors=e"
|
|
||||||
>
|
>
|
||||||
<NameNsDescription
|
<NameNsDescription
|
||||||
:value="value"
|
:value="value"
|
||||||
:namespaced="true"
|
:namespaced="true"
|
||||||
:name-required="false"
|
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
@update:value="$emit('update:value', $event)"
|
@update:value="$emit('update:value', $event)"
|
||||||
/>
|
/>
|
||||||
@@ -434,7 +357,7 @@ export default {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
v-if="isBlank"
|
v-if="source === 'blank'"
|
||||||
v-model:value="value.spec.storageClassName"
|
v-model:value="value.spec.storageClassName"
|
||||||
:options="storageClassOptions"
|
:options="storageClassOptions"
|
||||||
:label="t('harvester.storage.storageClass.label')"
|
:label="t('harvester.storage.storageClass.label')"
|
||||||
@@ -444,18 +367,6 @@ export default {
|
|||||||
@update:value="update"
|
@update:value="update"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LabeledSelect
|
|
||||||
v-if="showVolumeMode"
|
|
||||||
v-model:value="value.spec.volumeMode"
|
|
||||||
:label="t('harvester.volume.volumeMode')"
|
|
||||||
:options="volumeModeOptions"
|
|
||||||
required
|
|
||||||
:disabled="!isCreate"
|
|
||||||
:mode="mode"
|
|
||||||
class="mb-20"
|
|
||||||
@update:value="update"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<UnitInput
|
<UnitInput
|
||||||
v-model:value="storage"
|
v-model:value="storage"
|
||||||
:label="t('harvester.volume.size')"
|
:label="t('harvester.volume.size')"
|
||||||
@@ -466,7 +377,6 @@ export default {
|
|||||||
:disabled="value?.isLonghornV2 && isEdit"
|
:disabled="value?.isLonghornV2 && isEdit"
|
||||||
required
|
required
|
||||||
class="mb-20"
|
class="mb-20"
|
||||||
:suffix="GIBIBYTE"
|
|
||||||
@update:value="update"
|
@update:value="update"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ export default {
|
|||||||
:errors="errors"
|
:errors="errors"
|
||||||
:apply-hooks="applyHooks"
|
:apply-hooks="applyHooks"
|
||||||
@finish="save"
|
@finish="save"
|
||||||
@error="e=>errors=e"
|
|
||||||
>
|
>
|
||||||
<NameNsDescription
|
<NameNsDescription
|
||||||
:value="value"
|
:value="value"
|
||||||
|
|||||||
@@ -1,323 +0,0 @@
|
|||||||
<script>
|
|
||||||
import CruResource from '@shell/components/CruResource';
|
|
||||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
||||||
import Tab from '@shell/components/Tabbed/Tab';
|
|
||||||
import { NETWORK_ATTACHMENT } from '@shell/config/types';
|
|
||||||
import Loading from '@shell/components/Loading';
|
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
|
||||||
import { RadioGroup } from '@components/Form/Radio';
|
|
||||||
import { NETWORK_PROTOCOL, NETWORK_TYPE } from '@pkg/harvester/config/types';
|
|
||||||
import { set } from '@shell/utils/object';
|
|
||||||
import ArrayList from '@shell/components/form/ArrayList';
|
|
||||||
import { allHash } from '@shell/utils/promise';
|
|
||||||
import { HCI } from '../../types';
|
|
||||||
import ResourceTabs from '@shell/components/form/ResourceTabs/index';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'EditSubnet',
|
|
||||||
|
|
||||||
emits: ['update:value'],
|
|
||||||
|
|
||||||
components: {
|
|
||||||
CruResource,
|
|
||||||
LabeledInput,
|
|
||||||
LabeledSelect,
|
|
||||||
NameNsDescription,
|
|
||||||
Tab,
|
|
||||||
RadioGroup,
|
|
||||||
ArrayList,
|
|
||||||
ResourceTabs,
|
|
||||||
Loading,
|
|
||||||
},
|
|
||||||
|
|
||||||
mixins: [CreateEditView],
|
|
||||||
|
|
||||||
inheritAttrs: false,
|
|
||||||
|
|
||||||
created() {
|
|
||||||
const vpc = this.$route.query.vpc || '';
|
|
||||||
|
|
||||||
set(this.value, 'spec', this.value.spec || {
|
|
||||||
cidrBlock: '',
|
|
||||||
protocol: NETWORK_PROTOCOL.IPv4,
|
|
||||||
provider: '',
|
|
||||||
vpc,
|
|
||||||
gatewayIP: '',
|
|
||||||
excludeIps: [],
|
|
||||||
private: false
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
const hash = {
|
|
||||||
vpc: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VPC }),
|
|
||||||
nad: this.$store.dispatch(`${ inStore }/findAll`, { type: NETWORK_ATTACHMENT }),
|
|
||||||
};
|
|
||||||
|
|
||||||
await allHash(hash);
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
showAllowSubnets() {
|
|
||||||
return this.value?.spec?.private === true;
|
|
||||||
},
|
|
||||||
|
|
||||||
doneLocationOverride() {
|
|
||||||
return this.value.doneOverride;
|
|
||||||
},
|
|
||||||
|
|
||||||
allowSubnetTooltip() {
|
|
||||||
return this.t('harvester.subnet.allowSubnet.tooltip', null, true);
|
|
||||||
},
|
|
||||||
|
|
||||||
excludeIPsTooltip() {
|
|
||||||
return this.t('harvester.subnet.excludeIPs.tooltip', null, true);
|
|
||||||
},
|
|
||||||
|
|
||||||
protocolOptions() {
|
|
||||||
return Object.values(NETWORK_PROTOCOL);
|
|
||||||
},
|
|
||||||
|
|
||||||
provider: {
|
|
||||||
get() {
|
|
||||||
const raw = this.value.spec.provider;
|
|
||||||
|
|
||||||
if (!raw) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
const vmNet = raw.split('.')[0] || '';
|
|
||||||
const ns = raw.split('.')[1] || '';
|
|
||||||
|
|
||||||
return `${ ns }/${ vmNet }`;
|
|
||||||
},
|
|
||||||
|
|
||||||
set(value) {
|
|
||||||
const ns = value.split('/')[0] || '';
|
|
||||||
const vmNet = value.split('/')[1] || '';
|
|
||||||
const provider = `${ vmNet }.${ ns }.ovn`;
|
|
||||||
|
|
||||||
set(this.value, 'spec.provider', provider);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
providerOptions() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
const vmNets = this.$store.getters[`${ inStore }/all`](NETWORK_ATTACHMENT) || [];
|
|
||||||
|
|
||||||
return vmNets.filter((net) => net.vlanType === NETWORK_TYPE.OVERLAY).map((n) => ({
|
|
||||||
label: n.id,
|
|
||||||
value: n.id,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
vpcOptions() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
const vpcs = this.$store.getters[`${ inStore }/all`](HCI.VPC) || [];
|
|
||||||
|
|
||||||
return vpcs.map((n) => ({
|
|
||||||
label: n.id,
|
|
||||||
value: n.id,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
async saveSubnet(buttonCb) {
|
|
||||||
const errors = [];
|
|
||||||
const name = this.value?.metadata?.name;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!name) {
|
|
||||||
errors.push(this.t('validation.required', { key: this.t('generic.name') }, true));
|
|
||||||
} else if (!this.value?.spec?.cidrBlock) {
|
|
||||||
errors.push(this.t('validation.required', { key: this.t('harvester.subnet.cidrBlock.label') }, true));
|
|
||||||
} else if (!this.value?.spec?.provider) {
|
|
||||||
errors.push(this.t('validation.required', { key: this.t('harvester.subnet.provider.label') }, true));
|
|
||||||
} else if (this.value.spec.excludeIps.includes('')) {
|
|
||||||
errors.push(this.t('harvester.validation.subnet.excludeIps'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errors.length > 0) {
|
|
||||||
buttonCb(false);
|
|
||||||
this.errors = errors;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
await this.value.save();
|
|
||||||
buttonCb(true);
|
|
||||||
this.done();
|
|
||||||
} catch (e) {
|
|
||||||
this.errors = [e];
|
|
||||||
buttonCb(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Loading v-if="$fetchState.pending" />
|
|
||||||
<CruResource
|
|
||||||
v-else
|
|
||||||
:done-route="doneRoute"
|
|
||||||
:resource="value"
|
|
||||||
:mode="mode"
|
|
||||||
:apply-hooks="applyHooks"
|
|
||||||
:errors="errors"
|
|
||||||
@finish="saveSubnet"
|
|
||||||
@error="e=>errors=e"
|
|
||||||
>
|
|
||||||
<NameNsDescription
|
|
||||||
:value="value"
|
|
||||||
:mode="mode"
|
|
||||||
:namespaced="false"
|
|
||||||
@update:value="$emit('update:value', $event)"
|
|
||||||
/>
|
|
||||||
<ResourceTabs
|
|
||||||
class="mt-15"
|
|
||||||
:need-events="false"
|
|
||||||
:need-related="false"
|
|
||||||
:mode="mode"
|
|
||||||
:side-tabs="true"
|
|
||||||
>
|
|
||||||
<Tab
|
|
||||||
name="Basic"
|
|
||||||
:label="t('generic.basic')"
|
|
||||||
:weight="-1"
|
|
||||||
class="bordered-table"
|
|
||||||
>
|
|
||||||
<div class="row mt-10">
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="value.spec.cidrBlock"
|
|
||||||
class="mb-20"
|
|
||||||
required
|
|
||||||
:placeholder="t('harvester.subnet.cidrBlock.placeholder')"
|
|
||||||
:label="t('harvester.subnet.cidrBlock.label')"
|
|
||||||
:mode="mode"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="value.spec.protocol"
|
|
||||||
:label="t('harvester.subnet.protocol.label')"
|
|
||||||
:options="protocolOptions"
|
|
||||||
required
|
|
||||||
:mode="mode"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mt-10">
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="provider"
|
|
||||||
:label="t('harvester.subnet.provider.label')"
|
|
||||||
:options="providerOptions"
|
|
||||||
:tooltip="t('harvester.subnet.provider.tooltip')"
|
|
||||||
required
|
|
||||||
:mode="mode"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="value.spec.vpc"
|
|
||||||
:label="t('harvester.subnet.vpc.label')"
|
|
||||||
:options="vpcOptions"
|
|
||||||
required
|
|
||||||
:disabled="true"
|
|
||||||
:mode="mode"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mt-20">
|
|
||||||
<div class="col span-6">
|
|
||||||
<LabeledInput
|
|
||||||
v-model:value="value.spec.gateway"
|
|
||||||
class="mb-20"
|
|
||||||
:placeholder="t('harvester.subnet.gateway.placeholder')"
|
|
||||||
:label="t('harvester.subnet.gateway.label')"
|
|
||||||
:mode="mode"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mt-20">
|
|
||||||
<div class="col span-6">
|
|
||||||
<RadioGroup
|
|
||||||
v-model:value="value.spec.private"
|
|
||||||
name="enabled"
|
|
||||||
:options="[true, false]"
|
|
||||||
:label="t('harvester.subnet.private.label')"
|
|
||||||
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
|
||||||
:mode="mode"
|
|
||||||
tooltip-key="harvester.subnet.private.tooltip"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ArrayList
|
|
||||||
v-if="showAllowSubnets"
|
|
||||||
v-model:value="value.spec.allowSubnets"
|
|
||||||
:show-header="true"
|
|
||||||
class="mt-20"
|
|
||||||
:mode="mode"
|
|
||||||
:add-label="t('harvester.subnet.allowSubnet.addSubnet')"
|
|
||||||
>
|
|
||||||
<template #column-headers>
|
|
||||||
<div class="box">
|
|
||||||
<h3 class="key">
|
|
||||||
{{ t('harvester.subnet.allowSubnet.label') }}
|
|
||||||
<i
|
|
||||||
v-clean-tooltip="{content: allowSubnetTooltip, triggers: ['hover', 'touch', 'focus'] }"
|
|
||||||
v-stripped-aria-label="allowSubnetTooltip"
|
|
||||||
class="icon icon-info"
|
|
||||||
tabindex="0"
|
|
||||||
/>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #columns="scope">
|
|
||||||
<div class="key">
|
|
||||||
<input
|
|
||||||
v-model="scope.row.value"
|
|
||||||
:placeholder="t('harvester.subnet.allowSubnet.placeholder')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</ArrayList>
|
|
||||||
<ArrayList
|
|
||||||
v-model:value="value.spec.excludeIps"
|
|
||||||
:show-header="true"
|
|
||||||
class="mt-20"
|
|
||||||
:mode="mode"
|
|
||||||
:add-label="t('harvester.setting.storageNetwork.exclude.addIp')"
|
|
||||||
>
|
|
||||||
<template #column-headers>
|
|
||||||
<div class="box">
|
|
||||||
<h3 class="key">
|
|
||||||
{{ t('harvester.setting.storageNetwork.exclude.label') }}
|
|
||||||
<i
|
|
||||||
v-clean-tooltip="{content: excludeIPsTooltip, triggers: ['hover', 'touch', 'focus'] }"
|
|
||||||
v-stripped-aria-label="excludeIPsTooltip"
|
|
||||||
class="icon icon-info"
|
|
||||||
tabindex="0"
|
|
||||||
/>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #columns="scope">
|
|
||||||
<div class="key">
|
|
||||||
<input
|
|
||||||
v-model="scope.row.value"
|
|
||||||
:placeholder="t('harvester.setting.storageNetwork.exclude.placeholder')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</ArrayList>
|
|
||||||
</Tab>
|
|
||||||
</ResourceTabs>
|
|
||||||
</CruResource>
|
|
||||||
</template>
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
<script>
|
|
||||||
import debounce from 'lodash/debounce';
|
|
||||||
import { _EDIT, _VIEW } from '@shell/config/query-params';
|
|
||||||
import { removeAt } from '@shell/utils/array';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'StaticRoutes',
|
|
||||||
|
|
||||||
emits: ['update:value'],
|
|
||||||
|
|
||||||
props: {
|
|
||||||
value: {
|
|
||||||
type: Array,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
mode: {
|
|
||||||
type: String,
|
|
||||||
default: _EDIT,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
const rows = (this.value || []).map((row) => {
|
|
||||||
return {
|
|
||||||
cidr: row.cidr || '',
|
|
||||||
nextHopIP: row.nextHopIP || '',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return { rows };
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
isView() {
|
|
||||||
return this.mode === _VIEW;
|
|
||||||
},
|
|
||||||
|
|
||||||
showAdd() {
|
|
||||||
return !this.isView;
|
|
||||||
},
|
|
||||||
|
|
||||||
showRemove() {
|
|
||||||
return !this.isView;
|
|
||||||
},
|
|
||||||
nextHopIPTooltip() {
|
|
||||||
return this.t('harvester.vpc.staticRoutes.nextHopIP.tooltip');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
this.queueUpdate = debounce(this.update, 100);
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
add() {
|
|
||||||
this.rows.push({
|
|
||||||
cidr: '',
|
|
||||||
nextHopIP: '',
|
|
||||||
});
|
|
||||||
this.queueUpdate();
|
|
||||||
},
|
|
||||||
|
|
||||||
remove(idx) {
|
|
||||||
removeAt(this.rows, idx);
|
|
||||||
this.queueUpdate();
|
|
||||||
},
|
|
||||||
|
|
||||||
update() {
|
|
||||||
if (this.isView) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$emit('update:value', this.rows);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
v-if="rows.length"
|
|
||||||
class="static-route-row"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-for="(row, idx) in rows"
|
|
||||||
:key="idx"
|
|
||||||
>
|
|
||||||
<div class="pool-headers cidr">
|
|
||||||
<span class="pool-cidr">
|
|
||||||
<t k="harvester.vpc.staticRoutes.cidr.label" />
|
|
||||||
</span>
|
|
||||||
<span class="pool-nextHopIP">
|
|
||||||
<t k="harvester.vpc.staticRoutes.nextHopIP.label" />
|
|
||||||
<i
|
|
||||||
v-clean-tooltip="{content: nextHopIPTooltip, triggers: ['hover', 'touch', 'focus'] }"
|
|
||||||
v-stripped-aria-label="nextHopIPTooltip"
|
|
||||||
class="icon icon-info"
|
|
||||||
tabindex="0"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="pool-row cidr">
|
|
||||||
<div class="pool-cidr">
|
|
||||||
<span v-if="isView">
|
|
||||||
{{ row.cidr }}
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
v-else
|
|
||||||
v-model="row.cidr"
|
|
||||||
type="text"
|
|
||||||
:placeholder="t('harvester.vpc.staticRoutes.cidr.placeholder')"
|
|
||||||
@input="queueUpdate"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="pool-nextHopIP">
|
|
||||||
<span v-if="isView">
|
|
||||||
{{ row.nextHopIP }}
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
v-else
|
|
||||||
v-model="row.nextHopIP"
|
|
||||||
type="text"
|
|
||||||
:placeholder="t('harvester.vpc.staticRoutes.nextHopIP.placeholder')"
|
|
||||||
@input="queueUpdate"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
v-if="showRemove"
|
|
||||||
type="button"
|
|
||||||
class="btn role-link pl-0"
|
|
||||||
@click="remove(idx)"
|
|
||||||
>
|
|
||||||
<t k="generic.remove" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
v-if="showAdd"
|
|
||||||
type="button"
|
|
||||||
class="btn role-tertiary add"
|
|
||||||
@click="add()"
|
|
||||||
>
|
|
||||||
<t k="generic.add" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.pool-headers, .pool-row {
|
|
||||||
display: grid;
|
|
||||||
grid-column-gap: $column-gutter;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
&.cidr {
|
|
||||||
grid-template-columns: 40%+$column-gutter 40%+$column-gutter 15%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@@ -1,187 +0,0 @@
|
|||||||
<script>
|
|
||||||
import debounce from 'lodash/debounce';
|
|
||||||
import { _EDIT, _VIEW } from '@shell/config/query-params';
|
|
||||||
import { removeAt } from '@shell/utils/array';
|
|
||||||
import { HCI } from '../../types';
|
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'VpcPeerings',
|
|
||||||
|
|
||||||
emits: ['update:value'],
|
|
||||||
|
|
||||||
components: { LabeledSelect },
|
|
||||||
|
|
||||||
props: {
|
|
||||||
value: {
|
|
||||||
type: Array,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
mode: {
|
|
||||||
type: String,
|
|
||||||
default: _EDIT,
|
|
||||||
},
|
|
||||||
vpc: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
await this.$store.dispatch('harvester/findAll', { type: HCI.VPC });
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
const rows = (this.value || []).map((row) => {
|
|
||||||
return {
|
|
||||||
localConnectIP: row.localConnectIP || '',
|
|
||||||
remoteVpc: row.remoteVpc || '',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return { rows };
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
isView() {
|
|
||||||
return this.mode === _VIEW;
|
|
||||||
},
|
|
||||||
|
|
||||||
isEdit() {
|
|
||||||
return this.mode === _EDIT;
|
|
||||||
},
|
|
||||||
|
|
||||||
showAdd() {
|
|
||||||
return !this.isView;
|
|
||||||
},
|
|
||||||
|
|
||||||
showRemove() {
|
|
||||||
return !this.isView;
|
|
||||||
},
|
|
||||||
|
|
||||||
remoteVpcOptions() {
|
|
||||||
const allVpcs = this.$store.getters['harvester/all'](HCI.VPC) || [];
|
|
||||||
|
|
||||||
// filter self vpc if editing
|
|
||||||
const vpcs = this.isEdit ? allVpcs.filter((v) => v.id !== this.vpc.id) : allVpcs;
|
|
||||||
|
|
||||||
return vpcs.map((n) => ({
|
|
||||||
label: n.id,
|
|
||||||
value: n.id,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
localConnectIPTooltip() {
|
|
||||||
return this.t('harvester.vpc.vpcPeerings.localConnectIP.tooltip');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
this.queueUpdate = debounce(this.update, 100);
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
add() {
|
|
||||||
this.rows.push({
|
|
||||||
localConnectIP: '',
|
|
||||||
remoteVpc: '',
|
|
||||||
});
|
|
||||||
this.queueUpdate();
|
|
||||||
},
|
|
||||||
|
|
||||||
remove(idx) {
|
|
||||||
removeAt(this.rows, idx);
|
|
||||||
this.queueUpdate();
|
|
||||||
},
|
|
||||||
|
|
||||||
update() {
|
|
||||||
if (this.isView) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.$emit('update:value', this.rows);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
v-if="rows.length"
|
|
||||||
class="static-route-row"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-for="(row, idx) in rows"
|
|
||||||
:key="idx"
|
|
||||||
>
|
|
||||||
<div class="pool-headers localConnectIP">
|
|
||||||
<span class="pool-localConnectIP">
|
|
||||||
<t k="harvester.vpc.vpcPeerings.localConnectIP.label" />
|
|
||||||
<i
|
|
||||||
v-clean-tooltip="{content: localConnectIPTooltip, triggers: ['hover', 'touch', 'focus'] }"
|
|
||||||
v-stripped-aria-label="localConnectPTooltip"
|
|
||||||
class="icon icon-info"
|
|
||||||
tabindex="0"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<span class="pool-remoteVpc">
|
|
||||||
<t k="harvester.vpc.vpcPeerings.remoteVpc.label" />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="pool-row localConnectIP">
|
|
||||||
<div class="pool-localConnectIP">
|
|
||||||
<span v-if="isView">
|
|
||||||
{{ row.localConnectIP }}
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
v-else
|
|
||||||
v-model="row.localConnectIP"
|
|
||||||
type="text"
|
|
||||||
:placeholder="t('harvester.vpc.vpcPeerings.localConnectIP.placeholder')"
|
|
||||||
@input="queueUpdate"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="pool-remoteVpc">
|
|
||||||
<span v-if="isView">
|
|
||||||
{{ row.remoteVpc }}
|
|
||||||
</span>
|
|
||||||
<LabeledSelect
|
|
||||||
v-model:value="row.remoteVpc"
|
|
||||||
:options="remoteVpcOptions"
|
|
||||||
:mode="mode"
|
|
||||||
@update:value="queueUpdate"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
v-if="showRemove"
|
|
||||||
type="button"
|
|
||||||
class="btn role-link pl-0"
|
|
||||||
@click="remove(idx)"
|
|
||||||
>
|
|
||||||
<t k="generic.remove" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
v-if="showAdd"
|
|
||||||
type="button"
|
|
||||||
class="btn role-tertiary add"
|
|
||||||
@click="add()"
|
|
||||||
>
|
|
||||||
<t k="generic.add" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.pool-headers, .pool-row {
|
|
||||||
display: grid;
|
|
||||||
grid-column-gap: $column-gutter;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
&.localConnectIP {
|
|
||||||
grid-template-columns: 40%+$column-gutter 40%+$column-gutter 15%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
<script>
|
|
||||||
import CruResource from '@shell/components/CruResource';
|
|
||||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
|
||||||
import Tab from '@shell/components/Tabbed/Tab';
|
|
||||||
import Loading from '@shell/components/Loading';
|
|
||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
|
||||||
import ResourceTabs from '@shell/components/form/ResourceTabs/index';
|
|
||||||
import StaticRoutes from './StaticRoutes';
|
|
||||||
import VpcPeerings from './VpcPeerings';
|
|
||||||
import { set } from '@shell/utils/object';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'EditVPC',
|
|
||||||
|
|
||||||
emits: ['update:value'],
|
|
||||||
|
|
||||||
components: {
|
|
||||||
CruResource,
|
|
||||||
NameNsDescription,
|
|
||||||
Tab,
|
|
||||||
StaticRoutes,
|
|
||||||
ResourceTabs,
|
|
||||||
Loading,
|
|
||||||
VpcPeerings
|
|
||||||
},
|
|
||||||
|
|
||||||
mixins: [CreateEditView],
|
|
||||||
|
|
||||||
inheritAttrs: false,
|
|
||||||
|
|
||||||
data() {
|
|
||||||
set(this.value, 'spec', this.value.spec || {
|
|
||||||
staticRoutes: [],
|
|
||||||
vpcPeerings: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
return { staticRoutes: [] };
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
async saveVpc(buttonCb) {
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
const name = this.value?.metadata?.name;
|
|
||||||
|
|
||||||
if (!name) {
|
|
||||||
errors.push(this.t('validation.required', { key: this.t('generic.name') }, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errors.length > 0) {
|
|
||||||
buttonCb(false);
|
|
||||||
this.errors = errors;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
await this.value.save();
|
|
||||||
buttonCb(true);
|
|
||||||
this.done();
|
|
||||||
} catch (e) {
|
|
||||||
this.errors = [e];
|
|
||||||
buttonCb(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Loading v-if="$fetchState.pending" />
|
|
||||||
<CruResource
|
|
||||||
v-else
|
|
||||||
:done-route="doneRoute"
|
|
||||||
:resource="value"
|
|
||||||
:mode="mode"
|
|
||||||
:apply-hooks="applyHooks"
|
|
||||||
:errors="errors"
|
|
||||||
@finish="saveVpc"
|
|
||||||
@error="e=>errors=e"
|
|
||||||
>
|
|
||||||
<NameNsDescription
|
|
||||||
:value="value"
|
|
||||||
:mode="mode"
|
|
||||||
:namespaced="false"
|
|
||||||
@update:value="$emit('update:value', $event)"
|
|
||||||
/>
|
|
||||||
<ResourceTabs
|
|
||||||
class="mt-15"
|
|
||||||
:mode="mode"
|
|
||||||
:side-tabs="true"
|
|
||||||
>
|
|
||||||
<Tab
|
|
||||||
name="staticRoutes"
|
|
||||||
:label="t('harvester.vpc.staticRoutes.label')"
|
|
||||||
:weight="-1"
|
|
||||||
class="bordered-table"
|
|
||||||
>
|
|
||||||
<StaticRoutes
|
|
||||||
v-model:value="value.spec.staticRoutes"
|
|
||||||
class="col span-12"
|
|
||||||
:mode="mode"
|
|
||||||
/>
|
|
||||||
</Tab>
|
|
||||||
<Tab
|
|
||||||
name="vpcPeerings"
|
|
||||||
:label="t('harvester.vpc.vpcPeerings.label')"
|
|
||||||
:weight="-2"
|
|
||||||
class="bordered-table"
|
|
||||||
>
|
|
||||||
<VpcPeerings
|
|
||||||
v-model:value="value.spec.vpcPeerings"
|
|
||||||
class="col span-12"
|
|
||||||
:mode="mode"
|
|
||||||
:vpc="value"
|
|
||||||
/>
|
|
||||||
</Tab>
|
|
||||||
</ResourceTabs>
|
|
||||||
</CruResource>
|
|
||||||
</template>
|
|
||||||
-10
@@ -20,10 +20,6 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: 'create'
|
default: 'create'
|
||||||
},
|
},
|
||||||
osType: {
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
value: {
|
value: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
@@ -68,12 +64,6 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
osType(neu) {
|
|
||||||
if (neu === 'windows') {
|
|
||||||
this.id = '';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
value(neu) {
|
value(neu) {
|
||||||
this.yamlScript = neu;
|
this.yamlScript = neu;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -28,10 +28,6 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
osType: {
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
userScript: {
|
userScript: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
@@ -207,7 +203,6 @@ export default {
|
|||||||
<DataTemplate
|
<DataTemplate
|
||||||
ref="userTemplate"
|
ref="userTemplate"
|
||||||
:value="userScript"
|
:value="userScript"
|
||||||
:os-type="osType"
|
|
||||||
type="user"
|
type="user"
|
||||||
:view-code="viewCode"
|
:view-code="viewCode"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
@@ -224,7 +219,6 @@ export default {
|
|||||||
ref="networkTemplate"
|
ref="networkTemplate"
|
||||||
:value="networkScript"
|
:value="networkScript"
|
||||||
type="network"
|
type="network"
|
||||||
:os-type="osType"
|
|
||||||
:view-code="viewCode"
|
:view-code="viewCode"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:config-id="configNetworkId"
|
:config-id="configNetworkId"
|
||||||
|
|||||||
@@ -1,45 +1,23 @@
|
|||||||
<script>
|
<script>
|
||||||
import UnitInput from '@shell/components/form/UnitInput';
|
import UnitInput from '@shell/components/form/UnitInput';
|
||||||
import InputOrDisplay from '@shell/components/InputOrDisplay';
|
import InputOrDisplay from '@shell/components/InputOrDisplay';
|
||||||
import { GIBIBYTE } from '../../utils/unit';
|
|
||||||
import { Checkbox } from '@components/Form/Checkbox';
|
|
||||||
import { _VIEW } from '@shell/config/query-params';
|
|
||||||
import { HCI } from '../../types';
|
|
||||||
import { allHash } from '@shell/utils/promise';
|
|
||||||
import { HCI_SETTING } from '../../config/settings';
|
|
||||||
|
|
||||||
const DEFAULT_HOT_PLUG_TIMES = 4;
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterEditCpuMemory',
|
name: 'HarvesterEditCpuMemory',
|
||||||
|
|
||||||
emits: ['updateCpuMemory'],
|
emits: ['updateCpuMemory'],
|
||||||
|
|
||||||
components: {
|
components: { UnitInput, InputOrDisplay },
|
||||||
UnitInput, InputOrDisplay, Checkbox
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
cpu: {
|
cpu: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
maxCpu: {
|
|
||||||
type: Number,
|
|
||||||
default: null
|
|
||||||
},
|
|
||||||
memory: {
|
memory: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
maxMemory: {
|
|
||||||
type: String,
|
|
||||||
default: null
|
|
||||||
},
|
|
||||||
enableHotPlug: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
mode: {
|
mode: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'create',
|
default: 'create',
|
||||||
@@ -50,59 +28,20 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async fetch() {
|
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
||||||
|
|
||||||
const res = await allHash({ settings: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SETTING }) });
|
|
||||||
|
|
||||||
this.settings = res.settings || [];
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
GIBIBYTE,
|
localCpu: this.cpu,
|
||||||
localCpu: this.cpu,
|
localMemory: this.memory
|
||||||
localMemory: this.memory,
|
|
||||||
maxLocalCpu: this.maxCpu,
|
|
||||||
maxLocalMemory: this.maxMemory,
|
|
||||||
localEnableHotPlug: this.enableHotPlug,
|
|
||||||
settings: []
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
isView() {
|
cupDisplay() {
|
||||||
return this.mode === _VIEW;
|
|
||||||
},
|
|
||||||
cpuDisplay() {
|
|
||||||
return `${ this.localCpu } C`;
|
return `${ this.localCpu } C`;
|
||||||
},
|
},
|
||||||
|
|
||||||
maxCpuDisplay() {
|
|
||||||
return `${ this.maxLocalCpu } C`;
|
|
||||||
},
|
|
||||||
|
|
||||||
memoryDisplay() {
|
memoryDisplay() {
|
||||||
return `${ this.localMemory }`;
|
return `${ this.localMemory }`;
|
||||||
},
|
|
||||||
|
|
||||||
maxMemoryDisplay() {
|
|
||||||
return `${ this.maxLocalMemory }`;
|
|
||||||
},
|
|
||||||
|
|
||||||
cpuMemoryHotplugTooltip() {
|
|
||||||
return this.t('harvester.virtualMachine.hotplug.tooltip', { hotPlugTimes: this.maxHotplugRatio });
|
|
||||||
},
|
|
||||||
|
|
||||||
isCPUMemoryHotPlugFeatureEnabled() {
|
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('cpuMemoryHotplug');
|
|
||||||
},
|
|
||||||
|
|
||||||
maxHotplugRatio() {
|
|
||||||
const maxHotPlugRatioSetting = this.settings.find((s) => s.id === HCI_SETTING.MAX_HOTPLUG_RATIO);
|
|
||||||
const maxPlugRatio = maxHotPlugRatioSetting?.value ? parseInt(maxHotPlugRatioSetting.value, 10) : DEFAULT_HOT_PLUG_TIMES;
|
|
||||||
|
|
||||||
return maxPlugRatio;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -114,161 +53,70 @@ export default {
|
|||||||
if (neu && !neu.includes('null')) {
|
if (neu && !neu.includes('null')) {
|
||||||
this.localMemory = neu;
|
this.localMemory = neu;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
maxCpu(neu) {
|
|
||||||
this.maxLocalCpu = neu;
|
|
||||||
},
|
|
||||||
maxMemory(neu) {
|
|
||||||
if (neu && !neu.includes('null')) {
|
|
||||||
this.maxLocalMemory = neu;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
enableHotPlug(neu) {
|
|
||||||
this.localEnableHotPlug = neu;
|
|
||||||
},
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
hotPlugChanged(neu) {
|
change() {
|
||||||
// If hot plug is enabled, we need to update the maxCpu and maxMemory values
|
let memory = '';
|
||||||
if (neu) {
|
|
||||||
this.maxLocalCpu = this.localCpu ? this.localCpu * this.maxHotplugRatio : null;
|
if (String(this.localMemory).includes('Gi')) {
|
||||||
this.maxLocalMemory = this.localMemory ? `${ parseInt(this.localMemory, 10) * this.maxHotplugRatio }${ GIBIBYTE }` : null;
|
memory = this.localMemory;
|
||||||
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, this.maxLocalCpu, this.maxLocalMemory, neu);
|
|
||||||
} else {
|
} else {
|
||||||
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, '', null, neu);
|
memory = `${ this.localMemory }Gi`;
|
||||||
}
|
}
|
||||||
},
|
if (memory.includes('null')) {
|
||||||
changeMemory() {
|
memory = null;
|
||||||
if (this.localEnableHotPlug) {
|
|
||||||
this.maxLocalMemory = this.localMemory ? `${ parseInt(this.localMemory, 10) * this.maxHotplugRatio }${ GIBIBYTE }` : null;
|
|
||||||
}
|
}
|
||||||
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, this.maxLocalCpu, this.maxLocalMemory, this.localEnableHotPlug);
|
this.$emit('updateCpuMemory', this.localCpu, memory);
|
||||||
},
|
|
||||||
changeCPU() {
|
|
||||||
if (this.localEnableHotPlug) {
|
|
||||||
this.maxLocalCpu = this.localCpu ? this.localCpu * this.maxHotplugRatio : null;
|
|
||||||
}
|
|
||||||
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, this.maxLocalCpu, this.maxLocalMemory, this.localEnableHotPlug);
|
|
||||||
},
|
|
||||||
changeMaxCPUMemory() {
|
|
||||||
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, this.maxLocalCpu, this.maxLocalMemory, this.localEnableHotPlug);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="row">
|
||||||
<div class="row">
|
<div class="col span-6 mb-10">
|
||||||
<div class="col span-6 mb-10">
|
<InputOrDisplay
|
||||||
<InputOrDisplay
|
name="CPU"
|
||||||
name="CPU"
|
:value="cupDisplay"
|
||||||
:value="cpuDisplay"
|
:mode="mode"
|
||||||
|
>
|
||||||
|
<UnitInput
|
||||||
|
v-model:value="localCpu"
|
||||||
|
label="CPU"
|
||||||
|
suffix="C"
|
||||||
|
:delay="0"
|
||||||
|
required
|
||||||
|
:disabled="disabled"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
class="mb-10"
|
class="mb-20"
|
||||||
>
|
@update:value="change"
|
||||||
<UnitInput
|
/>
|
||||||
v-model:value="localCpu"
|
</InputOrDisplay>
|
||||||
label="CPU"
|
|
||||||
suffix="C"
|
|
||||||
:delay="0"
|
|
||||||
required
|
|
||||||
:disabled="disabled"
|
|
||||||
:mode="mode"
|
|
||||||
class="mb-20"
|
|
||||||
@update:value="changeCPU"
|
|
||||||
/>
|
|
||||||
</InputOrDisplay>
|
|
||||||
</div>
|
|
||||||
<div class="col span-6 mb-10">
|
|
||||||
<InputOrDisplay
|
|
||||||
:name="t('harvester.virtualMachine.input.memory')"
|
|
||||||
:value="memoryDisplay"
|
|
||||||
:mode="mode"
|
|
||||||
>
|
|
||||||
<UnitInput
|
|
||||||
v-model:value="localMemory"
|
|
||||||
:label="t('harvester.virtualMachine.input.memory')"
|
|
||||||
:mode="mode"
|
|
||||||
:input-exponent="3"
|
|
||||||
:delay="0"
|
|
||||||
:increment="1024"
|
|
||||||
:output-modifier="true"
|
|
||||||
:disabled="disabled"
|
|
||||||
required
|
|
||||||
:suffix="GIBIBYTE"
|
|
||||||
class="mb-20"
|
|
||||||
@update:value="changeMemory"
|
|
||||||
/>
|
|
||||||
</InputOrDisplay>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
v-if="isCPUMemoryHotPlugFeatureEnabled"
|
<div class="col span-6 mb-10">
|
||||||
class="row"
|
<InputOrDisplay
|
||||||
>
|
:name="t('harvester.virtualMachine.input.memory')"
|
||||||
<Checkbox
|
:value="memoryDisplay"
|
||||||
v-model:value="localEnableHotPlug"
|
:mode="mode"
|
||||||
class="check"
|
>
|
||||||
type="checkbox"
|
<UnitInput
|
||||||
:label="t('harvester.virtualMachine.hotplug.title')"
|
v-model:value="localMemory"
|
||||||
:disabled="isView"
|
:label="t('harvester.virtualMachine.input.memory')"
|
||||||
@update:value="hotPlugChanged"
|
|
||||||
/>
|
|
||||||
<i
|
|
||||||
v-clean-tooltip="{content: cpuMemoryHotplugTooltip, triggers: ['hover', 'touch', 'focus'] }"
|
|
||||||
v-stripped-aria-label="cpuMemoryHotplugTooltip"
|
|
||||||
class="icon icon-info"
|
|
||||||
tabindex="0"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="localEnableHotPlug && isCPUMemoryHotPlugFeatureEnabled"
|
|
||||||
class="row"
|
|
||||||
>
|
|
||||||
<div class="col span-6 mb-10">
|
|
||||||
<InputOrDisplay
|
|
||||||
:name="t('harvester.virtualMachine.input.maxCpu')"
|
|
||||||
:value="maxCpuDisplay"
|
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
class="mt-20"
|
:input-exponent="3"
|
||||||
>
|
:delay="0"
|
||||||
<UnitInput
|
:increment="1024"
|
||||||
v-model:value="maxLocalCpu"
|
:output-modifier="true"
|
||||||
:label="t('harvester.virtualMachine.input.maxCpu')"
|
:disabled="disabled"
|
||||||
suffix="C"
|
required
|
||||||
:delay="0"
|
class="mb-20"
|
||||||
:disabled="disabled"
|
@update:value="change"
|
||||||
:mode="mode"
|
/>
|
||||||
class="mt-20"
|
</InputOrDisplay>
|
||||||
@update:value="changeMaxCPUMemory"
|
|
||||||
/>
|
|
||||||
</InputOrDisplay>
|
|
||||||
</div>
|
|
||||||
<div class="col span-6 mb-10">
|
|
||||||
<InputOrDisplay
|
|
||||||
:name="t('harvester.virtualMachine.input.maxMemory')"
|
|
||||||
:value="maxMemoryDisplay"
|
|
||||||
:mode="mode"
|
|
||||||
class="mt-20"
|
|
||||||
>
|
|
||||||
<UnitInput
|
|
||||||
v-model:value="maxLocalMemory"
|
|
||||||
:label="t('harvester.virtualMachine.input.maxMemory')"
|
|
||||||
:mode="mode"
|
|
||||||
:input-exponent="3"
|
|
||||||
:delay="0"
|
|
||||||
:increment="1024"
|
|
||||||
:output-modifier="true"
|
|
||||||
:disabled="disabled"
|
|
||||||
:suffix="GIBIBYTE"
|
|
||||||
class="mt-20"
|
|
||||||
@update:value="changeMaxCPUMemory"
|
|
||||||
/>
|
|
||||||
</InputOrDisplay>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user