mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-01 03:36:04 +08:00
Compare commits
234
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e0cea8986 | ||
|
|
af80479bd9 | ||
|
|
4caecb48a7 | ||
|
|
8d1d370197 | ||
|
|
df9992146c | ||
|
|
acbc120dc1 | ||
|
|
59c63c8d9e | ||
|
|
4a4f2c96bb | ||
|
|
d7bc112a64 | ||
|
|
08e81b0628 | ||
|
|
4599159bff | ||
|
|
ef40cf5d8d | ||
|
|
d57dbd4c8e | ||
|
|
4c68d057c1 | ||
|
|
b4b2017c61 | ||
|
|
9f0d927c9c | ||
|
|
18a53f5baf | ||
|
|
f872bb5abf | ||
|
|
cd015d57e7 | ||
|
|
635254afdc | ||
|
|
3c464e9d10 | ||
|
|
d412ee2aa6 | ||
|
|
580b2892cb | ||
|
|
965c7d9b72 | ||
|
|
01e3867da1 | ||
|
|
fd52218350 | ||
|
|
1839bf387e | ||
|
|
3e9a0a4377 | ||
|
|
a2898a2554 | ||
|
|
d11a045aed | ||
|
|
ae2a2f6d28 | ||
|
|
cf68c3976a | ||
|
|
33f8300c0e | ||
|
|
b26e34a2bd | ||
|
|
01fd3e5e61 | ||
|
|
d5df7ba602 | ||
|
|
607aa106e1 | ||
|
|
49e2be6ff4 | ||
|
|
9ee81f4054 | ||
|
|
f8fa82f614 | ||
|
|
a6c60de2cb | ||
|
|
843b388ede | ||
|
|
f2e8cce29f | ||
|
|
31682dd165 | ||
|
|
9fac43d5e5 | ||
|
|
cd6ea6e402 | ||
|
|
3bfe57dc54 | ||
|
|
089573f193 | ||
|
|
5d0c4f0c5a | ||
|
|
57bb1e8562 | ||
|
|
30ff129691 | ||
|
|
9175f16bfc | ||
|
|
7c5e0f93d1 | ||
|
|
8a7d3dffbe | ||
|
|
b7810c5970 | ||
|
|
72dedd46d1 | ||
|
|
24f716ab20 | ||
|
|
38ad0fe1a7 | ||
|
|
c84d262099 | ||
|
|
dbb53eae00 | ||
|
|
4afacd754a | ||
|
|
10d159e2c7 | ||
|
|
de755a662a | ||
|
|
f1200f4508 | ||
|
|
0ca2b0591a | ||
|
|
44f003309c | ||
|
|
7c1596c965 | ||
|
|
a5130986a8 | ||
|
|
e4ae7cec5e | ||
|
|
2c7b59daed | ||
|
|
224e34315b | ||
|
|
9f56f9eccc | ||
|
|
dd76956972 | ||
|
|
d92a53d9a7 | ||
|
|
afc3f0d87f | ||
|
|
a607b604f8 | ||
|
|
4d7b0f7350 | ||
|
|
c570b5870b | ||
|
|
af36163b29 | ||
|
|
b3dbc2f0d3 | ||
|
|
fed86d4c24 | ||
|
|
d18ebfb26b | ||
|
|
477206d8d0 | ||
|
|
fd0836d489 | ||
|
|
2e3536abc6 | ||
|
|
45c0dce61b | ||
|
|
6b8b1f68eb | ||
|
|
bb645c00db | ||
|
|
ed8c6e81ca | ||
|
|
ed692b579d | ||
|
|
f02074a25e | ||
|
|
a8f8f0d288 | ||
|
|
9fa9b3837b | ||
|
|
4b3bdf8a59 | ||
|
|
7bdf40d737 | ||
|
|
a1e7345437 | ||
|
|
9322a77b20 | ||
|
|
ed7d7ffd2e | ||
|
|
2ef8e2e0aa | ||
|
|
a0ea6beedb | ||
|
|
85a723e20c | ||
|
|
bbe6b1bdf0 | ||
|
|
81a572e5b5 | ||
|
|
1a727f0001 | ||
|
|
91862244e1 | ||
|
|
c7b0e9ee7a | ||
|
|
d9337ff188 | ||
|
|
c16922c228 | ||
|
|
bbade7e50c | ||
|
|
193bf092d5 | ||
|
|
308b6ddbd7 | ||
|
|
c850b78da2 | ||
|
|
0fe9557eb8 | ||
|
|
b6953d73f7 | ||
|
|
8a89a9c502 | ||
|
|
d5f17bfdf1 | ||
|
|
9d0bb6e1f4 | ||
|
|
2ec37d81d9 | ||
|
|
e50bff8b5e | ||
|
|
e898181834 | ||
|
|
a0f877a23e | ||
|
|
a805490930 | ||
|
|
887a122358 | ||
|
|
1f48e8aea7 | ||
|
|
b2c1a3b068 | ||
|
|
df1566f394 | ||
|
|
0a3b122316 | ||
|
|
d497613cc4 | ||
|
|
91494f5832 | ||
|
|
c61d30a607 | ||
|
|
9428a07850 | ||
|
|
28603d3ab3 | ||
|
|
f863b881a9 | ||
|
|
083ab9f3e9 | ||
|
|
5d52c368a1 | ||
|
|
d72f379ab1 | ||
|
|
2931248fdb | ||
|
|
7166a5b1bb | ||
|
|
00c884cda0 | ||
|
|
1c65b71e35 | ||
|
|
3e4bc31a9d | ||
|
|
bc4bbd9a40 | ||
|
|
a6666ef384 | ||
|
|
92588c9adc | ||
|
|
832df26efc | ||
|
|
f7869cb241 | ||
|
|
27c0124682 | ||
|
|
6c69579c41 | ||
|
|
145b534c5d | ||
|
|
ce017a0e40 | ||
|
|
72a6f77fd2 | ||
|
|
275b15ad62 | ||
|
|
06c9a8b3d9 | ||
|
|
36b1406738 | ||
|
|
06b13a9cc5 | ||
|
|
2a3cd32447 | ||
|
|
40dc31226b | ||
|
|
def9e1a24e | ||
|
|
da25b28f7f | ||
|
|
10abbd0452 | ||
|
|
8a7cc69a69 | ||
|
|
c188f3367d | ||
|
|
26d85a7760 | ||
|
|
6eb845b46b | ||
|
|
8c20ddb440 | ||
|
|
a92f27d80c | ||
|
|
f523a161b6 | ||
|
|
ad2273b5ba | ||
|
|
171939e66b | ||
|
|
18bfbbaac1 | ||
|
|
e79f493cb6 | ||
|
|
e84c0fe886 | ||
|
|
bbfeb55af9 | ||
|
|
e7446e7e23 | ||
|
|
adacaf65fa | ||
|
|
7c761b2829 | ||
|
|
cc8532ff99 | ||
|
|
ede281a42d | ||
|
|
d34279d98f | ||
|
|
efd93c5e52 | ||
|
|
8d338b5f72 | ||
|
|
f59c81da31 | ||
|
|
cf6d44601e | ||
|
|
89b43060df | ||
|
|
7194d52000 | ||
|
|
a8ea0645e4 | ||
|
|
713173b0e4 | ||
|
|
6ed13bcec1 | ||
|
|
02519820d7 | ||
|
|
2604101eb2 | ||
|
|
1cb16f0c74 | ||
|
|
752cf2e93a | ||
|
|
f4ea7b6ea0 | ||
|
|
a6405989e9 | ||
|
|
fb8a81fd0f | ||
|
|
3a5ead3c2c | ||
|
|
def4b64cd3 | ||
|
|
a191b845d8 | ||
|
|
32a57d791a | ||
|
|
2a7e51ef4c | ||
|
|
f726fb99e5 | ||
|
|
9c19ae1f9b | ||
|
|
6ece2d0284 | ||
|
|
aec424b007 | ||
|
|
ff80b91311 | ||
|
|
52252c16f7 | ||
|
|
19cf1e9302 | ||
|
|
ddb8e8106b | ||
|
|
9af5ff7b12 | ||
|
|
fff6153f15 | ||
|
|
1ecac7adf4 | ||
|
|
408818904a | ||
|
|
b96637e6d9 | ||
|
|
8bdaca777f | ||
|
|
d278687e75 | ||
|
|
3ea8edb824 | ||
|
|
e33302050b | ||
|
|
8fefb59802 | ||
|
|
872aec1c10 | ||
|
|
8814bd4cda | ||
|
|
72a17cd9c2 | ||
|
|
8d79b69748 | ||
|
|
47760a8426 | ||
|
|
e6fc641a29 | ||
|
|
90b85d2be7 | ||
|
|
ee9575f710 | ||
|
|
7d97e6821c | ||
|
|
42fc9d8fae | ||
|
|
0f03fc0805 | ||
|
|
6d4a7cd1b1 | ||
|
|
f69d445ef7 | ||
|
|
d358a0a618 | ||
|
|
47841e49b1 | ||
|
|
57c7f0f856 |
@@ -1,7 +0,0 @@
|
||||
addAssignees: author
|
||||
addReviewers: true
|
||||
numberOfReviewers: 0
|
||||
reviewers:
|
||||
- a110605
|
||||
- houhoucoop
|
||||
- NickChungSUSE
|
||||
@@ -12,19 +12,6 @@ pull_request_rules:
|
||||
labels:
|
||||
- "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
|
||||
conditions:
|
||||
- conflict
|
||||
|
||||
@@ -2,13 +2,18 @@
|
||||
### Summary
|
||||
|
||||
### PR Checklists
|
||||
- Are backend engineers aware of UI changes ?
|
||||
- Do we need to backport this PR change to the [Harvester Dashboard](https://github.com/harvester/dashboard)?
|
||||
- [ ] Yes, the relevant PR is at:
|
||||
- Are backend engineers aware of UI changes?
|
||||
- [ ] Yes, the backend owner is:
|
||||
|
||||
### Related Issue #
|
||||
<!-- Define findings related to the feature or bug issue. -->
|
||||
|
||||
### Test screenshot or video
|
||||
### Test screenshot/video
|
||||
<!-- Attach screenshot or video of the changes and eventual comparison if you find it necessary -->
|
||||
|
||||
### Extra technical notes summary
|
||||
<!-- Outline technical changes which may pass unobserved or may help to understand the process of solving the issue -->
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -4,7 +4,6 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- 'release-harvester-v*'
|
||||
- 'main'
|
||||
|
||||
jobs:
|
||||
setup-target-branch:
|
||||
@@ -18,17 +17,14 @@ jobs:
|
||||
- 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
|
||||
if [[ "${{ github.ref_name }}" =~ ^release-harvester-v([0-9]+\.[0-9]+)$ ]]; then
|
||||
TARGET_BRANCH="v${BASH_REMATCH[1]}-head"
|
||||
echo "target_branch=${TARGET_BRANCH}" >> $GITHUB_ENV
|
||||
echo "target_branch=${TARGET_BRANCH}" >> $GITHUB_OUTPUT
|
||||
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
|
||||
@@ -132,7 +128,7 @@ jobs:
|
||||
git push origin ${{ needs.setup-target-branch.outputs.target_branch }}
|
||||
|
||||
- name: Run Helm chart releaser
|
||||
uses: helm/chart-releaser-action@v1.4.1
|
||||
uses: helm/chart-releaser-action@v1.7.0
|
||||
with:
|
||||
charts_dir: ./charts
|
||||
env:
|
||||
|
||||
@@ -17,37 +17,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Need full history for commit-lint
|
||||
fetch-depth: 1
|
||||
|
||||
- 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 }}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
npx --no -- commitlint --edit $1
|
||||
@@ -1 +0,0 @@
|
||||
yarn lint
|
||||
@@ -1,169 +1,62 @@
|
||||
# harvester-ui-extension
|
||||
|
||||
The Harvester UI Extension is a Rancher extension that provides the user interface for [Harvester](https://harvesterhci.io) within the [Rancher Dashboard](https://github.com/rancher/dashboard).
|
||||
Rancher Extension used in [rancher/dashboard](https://github.com/rancher/dashboard) for [Harvester](https://harvesterhci.io) Dashboard UI.
|
||||
|
||||
> **Note:**
|
||||
> This extension is available starting from **Rancher 2.10.0**. Ensure your Rancher version is **2.10.0 or later** to access Harvester integration.
|
||||
`
|
||||
From Rancher 2.10.0, Harvester integration is provided by this new Harvester UI Extension.
|
||||
`
|
||||
|
||||
## Installation
|
||||
|
||||
For detailed installation instructions, please refer to the [official Harvester documentation](https://docs.harvesterhci.io/v1.5/rancher/harvester-ui-extension#installation-on-rancher-210).
|
||||
To install the Harvester UI Extension in Rancher 2.10.0 and newer, please refer to [installation](https://docs.harvesterhci.io/v1.5/rancher/harvester-ui-extension#installation-on-rancher-210) on harvester doc.
|
||||
|
||||
## Running for Development
|
||||
|
||||
## Development Setup
|
||||
This is what you probably want to get started.
|
||||
|
||||
### Prerequisite
|
||||
|
||||
Nodejs version >= 20
|
||||
|
||||
Ensure **Node.js v20 or later** is installed for development and debugging.
|
||||
|
||||
### Standalone Mode
|
||||
|
||||
Run the extension standalone with hot reload at `https://localhost:8005`.
|
||||
For development, serve with hot reload at https://localhost:8005
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
yarn install
|
||||
|
||||
# Start the development server
|
||||
# Use RANCHER_ENV to specify the endpoint for your Harvester API
|
||||
RANCHER_ENV=harvester API=https://your-harvester-ip yarn dev
|
||||
|
||||
# Example with specific server version
|
||||
RANCHER_ENV=harvester VUE_APP_SERVER_VERSION=v1.5.0 API=https://192.168.1.123 yarn dev
|
||||
```
|
||||
# Use VUE_APP_SERVER_VERSION to specify the Harvester server version
|
||||
# in case your are running a dev build of Harvester
|
||||
RANCHER_ENV=harvester VUE_APP_SERVER_VERSION=your-harvester-server-version API=https://your-harvester-ip yarn dev
|
||||
|
||||
You may also define environment variables in a `.env` file:
|
||||
# or put your env variables into a .env file
|
||||
|
||||
```env
|
||||
RANCHER_ENV=harvester
|
||||
VUE_APP_SERVER_VERSION=v1.5.0
|
||||
API=https://192.168.1.123
|
||||
# Example
|
||||
RANCHER_ENV=harvester VUE_APP_SERVER_VERSION=v1.4.0 API=https://192.168.1.123 yarn dev
|
||||
```
|
||||
|
||||
### Rancher Integration Mode
|
||||
|
||||
To run as a Rancher extension, follow the [Rancher UI Extension Guide](https://extensions.rancher.io/extensions/next/extensions-getting-started#running-the-app).
|
||||
In this mode, you need to have Rancher and run Harvester UI Extension as Rancher extension, see more detail in [Rancher UI extension](https://extensions.rancher.io/extensions/next/extensions-getting-started#running-the-app).
|
||||
|
||||
```bash
|
||||
API=https://your-rancher-ip yarn dev
|
||||
|
||||
# Example
|
||||
API=https://192.168.1.123 yarn dev
|
||||
```
|
||||
|
||||
## Commit Message Guidelines
|
||||
|
||||
This project uses [commit-lint](https://commitlint.js.org/) with [Conventional Commits](https://www.conventionalcommits.org/) to ensure consistent and meaningful commit messages.
|
||||
|
||||
### Commit Message Format
|
||||
|
||||
All commit messages must follow the conventional commit format:
|
||||
|
||||
```
|
||||
<type>[optional scope]: <description>
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer(s)]
|
||||
```
|
||||
|
||||
### Supported Types
|
||||
|
||||
- **feat**: New features
|
||||
- **fix**: Bug fixes
|
||||
- **docs**: Documentation changes
|
||||
- **style**: Code style changes (formatting, missing semicolons, etc.)
|
||||
- **refactor**: Code refactoring
|
||||
- **perf**: Performance improvements
|
||||
- **test**: Adding or updating tests
|
||||
- **build**: Build system or external dependencies
|
||||
- **ci**: CI/CD changes
|
||||
- **chore**: Other changes that don't modify src or test files
|
||||
- **revert**: Reverts a previous commit
|
||||
- **wip**: Work in progress
|
||||
- **deps**: Dependency updates
|
||||
- **security**: Security fixes
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Feature
|
||||
git commit -m "feat: add new virtual machine creation wizard"
|
||||
|
||||
# Bug fix
|
||||
git commit -m "fix: resolve memory leak in VM console"
|
||||
|
||||
# Documentation
|
||||
git commit -m "docs: update installation instructions"
|
||||
|
||||
# Breaking change
|
||||
git commit -m "feat!: change API endpoint structure
|
||||
|
||||
BREAKING CHANGE: The /api/v1/vms endpoint has been replaced with /api/v2/vms"
|
||||
```
|
||||
|
||||
### Git Hooks
|
||||
|
||||
The project uses [Husky](https://typicode.github.io/husky/) to automatically validate commit messages and run linting before commits:
|
||||
|
||||
- **pre-commit**: Runs ESLint to ensure code quality
|
||||
- **commit-msg**: Validates commit message format using commit-lint
|
||||
|
||||
These hooks are automatically installed when you run `yarn install`.
|
||||
|
||||
### Manual Validation
|
||||
|
||||
You can manually validate commit messages:
|
||||
|
||||
```bash
|
||||
# Validate the last commit
|
||||
yarn commitlint
|
||||
|
||||
# Validate a specific commit
|
||||
npx commitlint --from <commit-hash>
|
||||
|
||||
# Validate a range of commits
|
||||
npx commitlint --from <start-hash> --to <end-hash>
|
||||
```
|
||||
|
||||
## Branch Structure
|
||||
|
||||
- **`main`** – Main development branch
|
||||
- **`release-harvester-vX.Y`** – Stable release branches per version series
|
||||
- **`vX.Y-head`** – Testing branches for ongoing changes to extension builds in each release series
|
||||
|
||||
> **Note:**
|
||||
> The `vX.Y-head` branches are auto-generated and kept in sync with release branches. Use these for testing the latest changes in each version series.
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
### UI Extension Testing
|
||||
|
||||
To validate changes in a release series, switch to the appropriate `vX.Y-head` branch. For main branch testing, use `main-head`.
|
||||
|
||||
- Examples:
|
||||
- Test `1.0.x` series → `v1.0-head`
|
||||
- Test `1.5.x` series → `v1.5-head`
|
||||
- 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
|
||||
|
||||
If you want to contribute, start by reading this document, then visit our [Getting Started guide](https://extensions.rancher.io/extensions/next/extensions-getting-started) to learn how to develop and submit changes.
|
||||
|
||||
## License
|
||||
For developers, after reading through the introduction on this page, head over to our [Getting Started](https://extensions.rancher.io/extensions/next/extensions-getting-started) guide to learn more.
|
||||
|
||||
License
|
||||
=======
|
||||
Copyright (c) 2014-2025 [SUSE, LLC.](https://www.suse.com/)
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (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'),
|
||||
],
|
||||
};
|
||||
+14
-19
@@ -1,36 +1,36 @@
|
||||
{
|
||||
"name": "harvester-ui-extension",
|
||||
"version": "1.6.0-rc4",
|
||||
"version": "1.5.2-rc2",
|
||||
"private": false,
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/plugin-transform-class-static-block": "7.26.0",
|
||||
"@rancher/shell": "3.0.5-rc.7",
|
||||
"@rancher/shell": "3.0.2-rc.6",
|
||||
"cache-loader": "^4.1.0",
|
||||
"color": "4.2.3",
|
||||
"ip": "2.0.1",
|
||||
"node-polyfill-webpack-plugin": "^3.0.0",
|
||||
"vue-draggable-next": "^2.2.1",
|
||||
"yaml": "^2.5.1"
|
||||
"yaml": "^2.5.1",
|
||||
"@babel/plugin-transform-class-static-block": "7.28.3"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/node": "~20.10.0",
|
||||
"cronstrue": "2.50.0",
|
||||
"@types/node": "~20.19.0",
|
||||
"cronstrue": "2.59.0",
|
||||
"d3-color": "3.1.0",
|
||||
"ejs": "3.1.10",
|
||||
"follow-redirects": "1.15.6",
|
||||
"follow-redirects": "1.15.11",
|
||||
"glob": "7.2.3",
|
||||
"glob-parent": "6.0.2",
|
||||
"json5": "2.2.3",
|
||||
"@types/lodash": "4.17.5",
|
||||
"@types/lodash": "4.17.20",
|
||||
"merge": "2.1.1",
|
||||
"node-forge": "1.3.1",
|
||||
"nth-check": "2.1.1",
|
||||
"qs": "6.11.1",
|
||||
"roarr": "7.0.4",
|
||||
"semver": "7.5.4",
|
||||
"qs": "6.14.0",
|
||||
"roarr": "7.21.1",
|
||||
"semver": "7.7.2",
|
||||
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -41,15 +41,10 @@
|
||||
"build-pkg": "./node_modules/@rancher/shell/scripts/build-pkg.sh",
|
||||
"serve-pkgs": "./node_modules/@rancher/shell/scripts/serve-pkgs",
|
||||
"publish-pkgs": "./node_modules/@rancher/shell/scripts/extension/publish",
|
||||
"parse-tag-name": "./node_modules/@rancher/shell/scripts/extension/parse-tag-name",
|
||||
"commitlint": "commitlint --edit",
|
||||
"prepare": "husky"
|
||||
"parse-tag-name": "./node_modules/@rancher/shell/scripts/extension/parse-tag-name"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^19.8.1",
|
||||
"@commitlint/config-conventional": "^19.8.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^7.1.0",
|
||||
"husky": "^9.1.7"
|
||||
"eslint-plugin-promise": "^7.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,8 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import ActionMenu from '@shell/components/ActionMenuShell';
|
||||
import { Banner } from '@components/Banner';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { HCI_ALLOWED_SETTINGS, HCI_SINGLE_CLUSTER_ALLOWED_SETTING, HCI_SETTING } from '../config/settings';
|
||||
import { DOC } from '../config/doc-links';
|
||||
import { docLink } from '../utils/feature-flags';
|
||||
import { HCI_ALLOWED_SETTINGS, HCI_SETTING } from '../config/settings';
|
||||
|
||||
const CATEGORY = {
|
||||
ui: [
|
||||
@@ -22,7 +19,6 @@ export default {
|
||||
components: {
|
||||
AsyncButton,
|
||||
Banner,
|
||||
ActionMenu
|
||||
},
|
||||
|
||||
props: {
|
||||
@@ -34,23 +30,15 @@ export default {
|
||||
category: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
searchQuery: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
const categorySettings = this.filterCategorySettings();
|
||||
const filteredSettings = this.filterSearchSettings(categorySettings, this.searchQuery);
|
||||
|
||||
return {
|
||||
HCI_SETTING,
|
||||
categorySettings,
|
||||
filteredSettings,
|
||||
originalHideMap: this.createHideMap(categorySettings)
|
||||
};
|
||||
},
|
||||
|
||||
@@ -60,85 +48,12 @@ export default {
|
||||
settings: {
|
||||
deep: true,
|
||||
handler() {
|
||||
this.categorySettings = this.filterCategorySettings();
|
||||
this.filteredSettings = this.filterSearchSettings(this.categorySettings, this.searchQuery);
|
||||
}
|
||||
},
|
||||
searchQuery: {
|
||||
immediate: true,
|
||||
handler(newQuery) {
|
||||
const filtered = this.filterSearchSettings(this.categorySettings, newQuery);
|
||||
|
||||
this.filteredSettings = newQuery ? this.openJsonSettings(filtered) : filtered.map((s) => ({ ...s, hide: this.originalHideMap[s.id] ?? false }));
|
||||
this['categorySettings'] = this.filterCategorySettings();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
createHideMap(settings = []) {
|
||||
const map = settings.reduce((acc, s) => {
|
||||
acc[s.id] = s.hide ?? false;
|
||||
|
||||
return acc;
|
||||
}, {} );
|
||||
|
||||
return map;
|
||||
},
|
||||
filterSearchSettings(settings, searchKey) {
|
||||
if (!searchKey) {
|
||||
return this.filterCategorySettings();
|
||||
}
|
||||
const searchQuery = searchKey.toLowerCase();
|
||||
|
||||
return settings.filter((setting) => {
|
||||
const id = setting.id?.toLowerCase() || '';
|
||||
|
||||
// filter by id
|
||||
if (id.includes(searchQuery) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let description = this.t(setting.description, this.getDocLinkParams(setting) || {}, true)?.toLowerCase() || '';
|
||||
|
||||
// remove <a> tags from description
|
||||
if (description.includes('<a')) {
|
||||
description = description.replace(/<a[^>]*>(.*?)<\/a>/g, '$1');
|
||||
}
|
||||
// filter by description
|
||||
if (description.includes(searchQuery)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// filter by customized value
|
||||
if (setting.customized === true && setting.data?.value) {
|
||||
const value = setting.data.value?.toLowerCase() || '';
|
||||
|
||||
return value.includes(searchQuery);
|
||||
}
|
||||
|
||||
// filter by json value
|
||||
if (setting.kind === 'json' && setting.json) {
|
||||
try {
|
||||
const json = JSON.parse(setting.json);
|
||||
const jsonString = JSON.stringify(json).toLowerCase();
|
||||
|
||||
return jsonString.includes(searchQuery);
|
||||
} catch (e) {
|
||||
console.error(`${ setting.id }: wrong format`, e); // eslint-disable-line no-console
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// filter by default value
|
||||
if (setting.data?.default) {
|
||||
return setting.data?.default.includes(searchQuery);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
},
|
||||
|
||||
filterCategorySettings() {
|
||||
return this.settings.filter((s) => {
|
||||
if (!this.getFeatureEnabled(s.featureFlag)) {
|
||||
@@ -159,21 +74,25 @@ export default {
|
||||
return id ? this.$store.getters['harvester-common/getFeatureEnabled'](id) : true;
|
||||
},
|
||||
|
||||
showActionMenu(e, setting) {
|
||||
const actionElement = e.srcElement;
|
||||
|
||||
this.$store.commit(`action-menu/show`, {
|
||||
resources: setting.data,
|
||||
elem: actionElement
|
||||
});
|
||||
},
|
||||
|
||||
getSettingOption(id) {
|
||||
return HCI_ALLOWED_SETTINGS.find((setting) => setting.id === id);
|
||||
},
|
||||
|
||||
openJsonSettings(settings) {
|
||||
return settings.map((s) => s.hide ? { ...s, hide: false } : s);
|
||||
},
|
||||
|
||||
toggleHide(s) {
|
||||
const setting = this.filteredSettings.find((setting) => setting.id === s.id);
|
||||
|
||||
if (setting) {
|
||||
setting.hide = !setting.hide;
|
||||
this.originalHideMap[setting.id] = setting.hide;
|
||||
}
|
||||
this.categorySettings.find((setting) => {
|
||||
if (setting.id === s.id) {
|
||||
setting.hide = !setting.hide;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async testConnect(buttonDone, value) {
|
||||
@@ -199,19 +118,6 @@ export default {
|
||||
}
|
||||
buttonDone(false);
|
||||
}
|
||||
},
|
||||
|
||||
getDocLinkParams(setting) {
|
||||
const settingConfig = HCI_ALLOWED_SETTINGS[setting.id] || HCI_SINGLE_CLUSTER_ALLOWED_SETTING[setting.id];
|
||||
|
||||
if (settingConfig?.docPath) {
|
||||
const version = this.$store.getters['harvester-common/getServerVersion']();
|
||||
const url = docLink(DOC[settingConfig.docPath], version);
|
||||
|
||||
return { url };
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -220,7 +126,7 @@ export default {
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-for="(setting, i) in filteredSettings"
|
||||
v-for="(setting, i) in categorySettings"
|
||||
:key="i"
|
||||
class="advanced-setting mb-20"
|
||||
>
|
||||
@@ -242,7 +148,7 @@ export default {
|
||||
Experimental
|
||||
</span>
|
||||
</h1>
|
||||
<h2 v-clean-html="t(setting.description, getDocLinkParams(setting) || {}, true)">
|
||||
<h2 v-clean-html="t(setting.description, {}, true)">
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
@@ -250,12 +156,15 @@ export default {
|
||||
:id="setting.id"
|
||||
class="action"
|
||||
>
|
||||
<ActionMenu
|
||||
:resource="setting.data"
|
||||
:button-aria-label="t('advancedSettings.edit.label')"
|
||||
data-testid="action-button"
|
||||
button-role="tertiary"
|
||||
/>
|
||||
<button
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
type="button"
|
||||
class="btn btn-sm role-multi-action actions"
|
||||
@click="showActionMenu($event, setting)"
|
||||
>
|
||||
<i class="icon icon-actions" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div value>
|
||||
@@ -312,12 +221,6 @@ export default {
|
||||
{{ setting.data.errMessage }}
|
||||
</Banner>
|
||||
</div>
|
||||
<div
|
||||
v-if="filteredSettings.length === 0"
|
||||
class="advanced-setting mb-20 no-search-match"
|
||||
>
|
||||
<p> {{ t('harvester.setting.noSearchMatch') }} </p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -368,8 +271,4 @@ export default {
|
||||
padding: 2px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.no-search-match {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,294 +0,0 @@
|
||||
<script>
|
||||
import { _EDIT } from '@shell/config/query-params';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import InfoBox from '@shell/components/InfoBox';
|
||||
import { Banner } from '@components/Banner';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { CSI_DRIVER } from '../../types';
|
||||
import { LONGHORN_DRIVER } from '@shell/config/types';
|
||||
|
||||
export default {
|
||||
name: 'CSIOnlineExpandValidation',
|
||||
|
||||
components: {
|
||||
Banner,
|
||||
InfoBox,
|
||||
LabeledSelect,
|
||||
},
|
||||
|
||||
props: {
|
||||
mode: {
|
||||
type: String,
|
||||
default: _EDIT,
|
||||
},
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
registerBeforeHook: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
try {
|
||||
await allHash({ csiDrivers: this.$store.dispatch(`${ inStore }/findAll`, { type: CSI_DRIVER }) });
|
||||
this.fetchError = null;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch CSI drivers:', error); // eslint-disable-line no-console
|
||||
this.fetchError = this.t(
|
||||
'harvester.setting.csiOnlineExpandValidation.failedToLoadDrivers',
|
||||
{ error: error.message || error },
|
||||
true
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
configArr: [],
|
||||
parseError: null,
|
||||
fetchError: null,
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
const initValue = this.value.value || this.value.default || '{}';
|
||||
|
||||
this.configArr = this.parseValue(initValue);
|
||||
this.registerBeforeHook?.(this.willSave, 'willSave');
|
||||
},
|
||||
|
||||
computed: {
|
||||
allErrors() {
|
||||
const errors = [];
|
||||
|
||||
if (this.fetchError) {
|
||||
errors.push(this.fetchError);
|
||||
}
|
||||
if (this.parseError) {
|
||||
errors.push(this.parseError);
|
||||
}
|
||||
|
||||
return errors;
|
||||
},
|
||||
|
||||
csiDrivers() {
|
||||
if (this.fetchError) return [];
|
||||
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
return this.$store.getters[`${ inStore }/all`](CSI_DRIVER) || [];
|
||||
},
|
||||
|
||||
provisioners() {
|
||||
const usedKeys = this.configArr.map(({ key }) => key);
|
||||
|
||||
return this.csiDrivers
|
||||
.filter(({ name }) => !usedKeys.includes(name))
|
||||
.map(({ name }) => name);
|
||||
},
|
||||
|
||||
provisionerValue() {
|
||||
return [
|
||||
{ label: 'True', value: true },
|
||||
{ label: 'False', value: false },
|
||||
];
|
||||
},
|
||||
|
||||
disableAdd() {
|
||||
return this.parseError || this.fetchError || this.configArr.length >= this.csiDrivers.length;
|
||||
},
|
||||
|
||||
disableConfigEditing() {
|
||||
return this.parseError || this.fetchError;
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
'value.value'(newVal, oldVal) {
|
||||
if (newVal !== oldVal) {
|
||||
this.configArr = this.parseValue(newVal || '{}');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
_convertToBoolean(value) {
|
||||
if (typeof value === 'boolean') return value;
|
||||
|
||||
if (typeof value === 'string') {
|
||||
const lowerCaseValue = value.toLowerCase();
|
||||
|
||||
if (lowerCaseValue === 'true') return true;
|
||||
if (lowerCaseValue === 'false') return false;
|
||||
}
|
||||
|
||||
return false; // default to false for any other string or non-boolean type
|
||||
},
|
||||
parseValue(raw) {
|
||||
try {
|
||||
const json = JSON.parse(raw);
|
||||
|
||||
this.parseError = null;
|
||||
|
||||
return Object.entries(json).map(([key, value]) => ({
|
||||
key,
|
||||
value: this._convertToBoolean(value),
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error('[CSIOnlineExpandValidation] JSON Parsing Error:', raw, e); // eslint-disable-line no-console
|
||||
this.parseError = this.t(
|
||||
'harvester.setting.csiOnlineExpandValidation.invalidJsonFormat',
|
||||
{ error: e.message },
|
||||
true
|
||||
);
|
||||
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
stringifyConfig() {
|
||||
const obj = {};
|
||||
|
||||
this.configArr.forEach(({ key, value }) => {
|
||||
obj[key] = value;
|
||||
});
|
||||
|
||||
return this.configArr.length ? JSON.stringify(obj) : '';
|
||||
},
|
||||
|
||||
update() {
|
||||
this.value.value = this.stringifyConfig();
|
||||
},
|
||||
|
||||
willSave() {
|
||||
const errors = [];
|
||||
|
||||
this.configArr.forEach(({ key }) => {
|
||||
if (!key) {
|
||||
errors.push(
|
||||
this.t('validation.required', { key: this.t('harvester.setting.csiOnlineExpandValidation.provisioner') }, true)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
this.value.value = this.stringifyConfig();
|
||||
|
||||
return errors.length ? Promise.reject(errors) : Promise.resolve();
|
||||
},
|
||||
|
||||
useDefault() {
|
||||
this.configArr = this.parseValue(this.value.default || '{}');
|
||||
this.update();
|
||||
},
|
||||
|
||||
disableEdit(driverKey) {
|
||||
return this.fetchError || driverKey === LONGHORN_DRIVER;
|
||||
},
|
||||
|
||||
add() {
|
||||
if (this.disableConfigEditing) return;
|
||||
|
||||
this.configArr.push({ key: '', value: true });
|
||||
},
|
||||
|
||||
remove(index) {
|
||||
if (this.disableConfigEditing) return;
|
||||
|
||||
this.configArr.splice(index, 1);
|
||||
this.update();
|
||||
},
|
||||
|
||||
onValueChange(idx, newVal) {
|
||||
if (this.disableConfigEditing) return;
|
||||
|
||||
const val = newVal === 'true' ? true : newVal === 'false' ? false : newVal;
|
||||
|
||||
this.configArr[idx].value = val;
|
||||
this.update();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Banner
|
||||
v-for="(errorMsg, index) in allErrors"
|
||||
:key="index"
|
||||
color="error"
|
||||
>
|
||||
{{ errorMsg }}
|
||||
</Banner>
|
||||
<InfoBox
|
||||
v-for="(driver, idx) in configArr"
|
||||
:key="idx"
|
||||
class="box"
|
||||
>
|
||||
<button
|
||||
class="role-link btn btn-sm remove"
|
||||
type="button"
|
||||
:disabled="disableEdit(driver.key)"
|
||||
@click="remove(idx)"
|
||||
>
|
||||
<i class="icon icon-x" />
|
||||
</button>
|
||||
|
||||
<div class="row">
|
||||
<div class="col span-4">
|
||||
<LabeledSelect
|
||||
v-model:value="driver.key"
|
||||
label-key="harvester.setting.csiOnlineExpandValidation.provisioner"
|
||||
required
|
||||
searchable
|
||||
:mode="mode"
|
||||
:disabled="disableEdit(driver.key)"
|
||||
:options="provisioners"
|
||||
@update:value="update"
|
||||
@keydown.native.enter.prevent
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col span-4">
|
||||
<LabeledSelect
|
||||
v-model:value="driver.value"
|
||||
:value="driver.value.toString()"
|
||||
label-key="harvester.setting.csiOnlineExpandValidation.value"
|
||||
required
|
||||
searchable
|
||||
:mode="mode"
|
||||
:disabled="disableEdit(driver.key)"
|
||||
:options="provisionerValue"
|
||||
@update:value="val => onValueChange(idx, val)"
|
||||
@keydown.native.enter.prevent
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</InfoBox>
|
||||
|
||||
<button
|
||||
class="btn btn-sm role-primary"
|
||||
:disabled="disableAdd"
|
||||
@click="add"
|
||||
>
|
||||
{{ t('generic.add') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.box {
|
||||
position: relative;
|
||||
padding-top: 40px;
|
||||
}
|
||||
.remove {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,222 +0,0 @@
|
||||
<script>
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import { RadioGroup } from '@components/Form/Radio';
|
||||
import { SECRET } from '@shell/config/types';
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import FileSelector, { createOnSelected } from '@shell/components/form/FileSelector';
|
||||
import YamlEditor from '@shell/components/YamlEditor';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterRancherCluster',
|
||||
|
||||
components: {
|
||||
RadioGroup,
|
||||
FileSelector,
|
||||
YamlEditor
|
||||
},
|
||||
|
||||
mixins: [CreateEditView],
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
registerBeforeHook: {
|
||||
type: Function,
|
||||
required: false,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
let parseDefaultValue = {};
|
||||
|
||||
try {
|
||||
const data = this.value.value || this.value.default || '{}';
|
||||
const parsed = JSON.parse(data);
|
||||
|
||||
parseDefaultValue = {
|
||||
kubeConfig: '',
|
||||
removeUpstreamClusterWhenNamespaceIsDeleted: parsed.removeUpstreamClusterWhenNamespaceIsDeleted || false
|
||||
};
|
||||
} catch (error) {
|
||||
parseDefaultValue = {
|
||||
kubeConfig: '',
|
||||
removeUpstreamClusterWhenNamespaceIsDeleted: false
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
parseDefaultValue,
|
||||
errors: [],
|
||||
existingSecret: null,
|
||||
};
|
||||
},
|
||||
|
||||
async created() {
|
||||
await this.checkExistingSecret();
|
||||
if (this.registerBeforeHook) {
|
||||
this.registerBeforeHook(this.willSave, 'willSave');
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onKeySelected: createOnSelected('parseDefaultValue.kubeConfig'),
|
||||
|
||||
update() {
|
||||
if (this.parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted) {
|
||||
this.value['value'] = JSON.stringify({ removeUpstreamClusterWhenNamespaceIsDeleted: true });
|
||||
} else {
|
||||
this.value['value'] = this.value.default || '{}';
|
||||
}
|
||||
},
|
||||
|
||||
async checkExistingSecret() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
await this.$store.dispatch(`${ inStore }/findAll`, { type: SECRET });
|
||||
const secrets = this.$store.getters[`${ inStore }/all`](SECRET) || [];
|
||||
|
||||
this.existingSecret = secrets.find((secret) => secret.metadata.name === 'rancher-cluster-config' &&
|
||||
secret.metadata.namespace === 'harvester-system'
|
||||
);
|
||||
|
||||
// If the secret exists and has data, populate the kubeConfig
|
||||
if (this.existingSecret?.data?.kubeConfig) {
|
||||
const decodedContent = atob(this.existingSecret.data.kubeConfig);
|
||||
|
||||
this.parseDefaultValue.kubeConfig = decodedContent;
|
||||
this.$nextTick(() => {
|
||||
this.update();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async createOrUpdateRancherKubeConfigSecret() {
|
||||
this.errors = [];
|
||||
// Check if kubeConfig is provided
|
||||
if (!this.parseDefaultValue.kubeConfig) {
|
||||
this.errors.push(this.t('validation.required', { key: this.t('harvester.setting.rancherCluster.kubeConfig') }, true));
|
||||
|
||||
return Promise.reject(this.errors);
|
||||
}
|
||||
|
||||
try {
|
||||
let secret;
|
||||
|
||||
if (this.existingSecret) {
|
||||
secret = this.existingSecret;
|
||||
secret.setData('kubeConfig', this.parseDefaultValue.kubeConfig);
|
||||
} else {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
secret = await this.$store.dispatch(`${ inStore }/create`, {
|
||||
apiVersion: 'v1',
|
||||
kind: 'Secret',
|
||||
metadata: {
|
||||
name: 'rancher-cluster-config',
|
||||
namespace: 'harvester-system'
|
||||
},
|
||||
type: 'secret',
|
||||
data: { kubeConfig: btoa(this.parseDefaultValue.kubeConfig) }
|
||||
});
|
||||
}
|
||||
await secret.save();
|
||||
|
||||
return Promise.resolve();
|
||||
} catch (err) {
|
||||
this.errors = exceptionToErrorsArray(err);
|
||||
|
||||
return Promise.reject(this.errors);
|
||||
}
|
||||
},
|
||||
|
||||
async deleteRancherKubeConfigSecret() {
|
||||
if (this.existingSecret) {
|
||||
this.existingSecret.remove();
|
||||
}
|
||||
},
|
||||
|
||||
async willSave() {
|
||||
// Only create or update secret if enabled
|
||||
if (this.parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted) {
|
||||
await this.createOrUpdateRancherKubeConfigSecret();
|
||||
} else {
|
||||
await this.deleteRancherKubeConfigSecret();
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
useDefault() {
|
||||
this.parseDefaultValue = {
|
||||
kubeConfig: '',
|
||||
removeUpstreamClusterWhenNamespaceIsDeleted: false
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
'parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted'(val, oldVal) {
|
||||
if (val && !oldVal && this.existingSecret?.data?.kubeConfig) {
|
||||
// Populate kubeConfig with the existing secret value
|
||||
this.parseDefaultValue.kubeConfig = atob(this.existingSecret.data.kubeConfig);
|
||||
}
|
||||
},
|
||||
'parseDefaultValue.kubeConfig'(val) {
|
||||
this.$refs.yaml?.updateValue(val);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="row mt-20">
|
||||
<div class="col span-12">
|
||||
<RadioGroup
|
||||
v-model:value="parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted"
|
||||
:label="t('harvester.setting.rancherCluster.removeUpstreamClusterWhenNamespaceIsDeleted')"
|
||||
name="removeUpstreamClusterWhenNamespaceIsDeleted"
|
||||
:options="[true, false]"
|
||||
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||
@update:value="update"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="parseDefaultValue.removeUpstreamClusterWhenNamespaceIsDeleted"
|
||||
class="row mt-20"
|
||||
>
|
||||
<div class="col span-12">
|
||||
<FileSelector
|
||||
class="btn btn-sm bg-primary mb-10"
|
||||
:label="t('generic.readFromFile')"
|
||||
@selected="onKeySelected"
|
||||
/>
|
||||
<YamlEditor
|
||||
ref="yaml"
|
||||
v-model:value="parseDefaultValue.kubeConfig"
|
||||
class="yaml-editor"
|
||||
:editor-mode="mode === 'view' ? 'VIEW_CODE' : 'EDIT_CODE'"
|
||||
@update:value="update"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$yaml-height: 540px;
|
||||
|
||||
:deep() .yaml-editor{
|
||||
flex: 1;
|
||||
min-height: $yaml-height;
|
||||
& .code-mirror .CodeMirror {
|
||||
position: initial;
|
||||
height: auto;
|
||||
min-height: $yaml-height;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -3,9 +3,6 @@ import { NAMESPACE } from '@shell/config/types';
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
|
||||
const SELECT_ALL = 'select_all';
|
||||
const UNSELECT_ALL = 'unselect_all';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterBundleNamespaces',
|
||||
|
||||
@@ -14,74 +11,47 @@ export default {
|
||||
mixins: [CreateEditView],
|
||||
|
||||
async fetch() {
|
||||
this.loading = true;
|
||||
|
||||
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() {
|
||||
let namespaces = [];
|
||||
const namespacesStr = this.value?.value || this.value?.default || '';
|
||||
const namespaces = namespacesStr ? namespacesStr.split(',') : [];
|
||||
|
||||
return {
|
||||
namespaces,
|
||||
defaultNamespaces: [],
|
||||
loading: true
|
||||
};
|
||||
if (namespacesStr) {
|
||||
namespaces = namespacesStr.split(',');
|
||||
}
|
||||
|
||||
return { namespaces };
|
||||
},
|
||||
|
||||
computed: {
|
||||
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() {
|
||||
const allSelected =
|
||||
this.namespaces.length === this.filteredNamespaces.length &&
|
||||
this.filteredNamespaces.every((ns) => this.namespaces.includes(ns));
|
||||
|
||||
const controlOption = allSelected ? { label: this.t('harvester.modal.bundle.namespaces.unselectAll'), value: UNSELECT_ALL } : { label: this.t('harvester.modal.bundle.namespaces.selectAll'), value: SELECT_ALL };
|
||||
|
||||
return [controlOption, ...this.filteredNamespaces];
|
||||
return this.$store.getters['harvester/all'](NAMESPACE).map((N) => {
|
||||
return {
|
||||
label: N.id,
|
||||
value: N.id
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
update(selected) {
|
||||
if (selected.includes(SELECT_ALL)) {
|
||||
this.namespaces = [...this.filteredNamespaces];
|
||||
} else if (selected.includes(UNSELECT_ALL)) {
|
||||
this.namespaces = [];
|
||||
} else {
|
||||
this.namespaces = selected.filter((val) => val !== SELECT_ALL && val !== UNSELECT_ALL);
|
||||
}
|
||||
update() {
|
||||
const namespaceStr = this.namespaces.join(',');
|
||||
|
||||
this.value.value = this.namespaces.join(',');
|
||||
this.value['value'] = namespaceStr;
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
'value.value'(newVal) {
|
||||
const raw = newVal || this.value.default || '';
|
||||
|
||||
this.namespaces = raw ? raw.split(',') : [];
|
||||
'value.value': {
|
||||
handler(neu) {
|
||||
if (neu === this.value.default || !neu) {
|
||||
this.namespaces = [];
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -92,7 +62,6 @@ export default {
|
||||
<div class="col span-12">
|
||||
<LabeledSelect
|
||||
v-model:value="namespaces"
|
||||
:loading="loading"
|
||||
:multiple="true"
|
||||
label-key="nameNsDescription.namespace.label"
|
||||
:mode="mode"
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
<script>
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { RadioGroup } from '@components/Form/Radio';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterUpgradeConfig',
|
||||
|
||||
components: {
|
||||
LabeledInput,
|
||||
LabeledSelect,
|
||||
RadioGroup
|
||||
},
|
||||
mixins: [CreateEditView],
|
||||
|
||||
data() {
|
||||
let parseDefaultValue = {};
|
||||
|
||||
try {
|
||||
parseDefaultValue = this.value.value ? JSON.parse(this.value.value) : JSON.parse(this.value.default);
|
||||
} catch (error) {
|
||||
parseDefaultValue = JSON.parse(this.value.default);
|
||||
}
|
||||
parseDefaultValue = this.normalizeValue(parseDefaultValue);
|
||||
|
||||
return {
|
||||
parseDefaultValue,
|
||||
errors: []
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
strategyOptions() {
|
||||
return [
|
||||
{ value: 'sequential', label: 'sequential' },
|
||||
{ value: 'skip', label: 'skip' },
|
||||
{ value: 'parallel', label: 'parallel' }
|
||||
];
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.update();
|
||||
},
|
||||
|
||||
methods: {
|
||||
normalizeValue(obj) {
|
||||
if (!obj.imagePreloadOption) {
|
||||
obj.imagePreloadOption = { strategy: { type: 'sequential' } };
|
||||
}
|
||||
if (!obj.imagePreloadOption.strategy) {
|
||||
obj.imagePreloadOption.strategy = { type: 'sequential' };
|
||||
}
|
||||
if (!obj.imagePreloadOption.strategy.type) {
|
||||
obj.imagePreloadOption.strategy.type = 'sequential';
|
||||
}
|
||||
// Only set concurrency if type is 'parallel'
|
||||
if (obj.imagePreloadOption.strategy.type === 'parallel') {
|
||||
if (typeof obj.imagePreloadOption.strategy.concurrency !== 'number') {
|
||||
obj.imagePreloadOption.strategy.concurrency = 0;
|
||||
}
|
||||
} else {
|
||||
delete obj.imagePreloadOption.strategy.concurrency;
|
||||
}
|
||||
if (typeof obj.restoreVM !== 'boolean') {
|
||||
obj.restoreVM = false;
|
||||
}
|
||||
|
||||
return obj;
|
||||
},
|
||||
update() {
|
||||
try {
|
||||
// Clone to avoid mutating the form state
|
||||
const valueToSave = JSON.parse(JSON.stringify(this.parseDefaultValue));
|
||||
|
||||
if (valueToSave.imagePreloadOption && valueToSave.imagePreloadOption.strategy) {
|
||||
if (valueToSave.imagePreloadOption.strategy.type !== 'parallel') {
|
||||
delete valueToSave.imagePreloadOption.strategy.concurrency;
|
||||
}
|
||||
}
|
||||
|
||||
this.value['value'] = JSON.stringify(valueToSave, null, 2);
|
||||
this.errors = [];
|
||||
} catch (e) {
|
||||
this.errors = ['Invalid JSON'];
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
value: {
|
||||
handler(neu) {
|
||||
let parseDefaultValue;
|
||||
|
||||
try {
|
||||
parseDefaultValue = JSON.parse(neu.value);
|
||||
} catch (err) {
|
||||
parseDefaultValue = JSON.parse(this.value.default);
|
||||
}
|
||||
parseDefaultValue = this.normalizeValue(parseDefaultValue);
|
||||
this['parseDefaultValue'] = parseDefaultValue;
|
||||
this.update();
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="row"
|
||||
@input="update"
|
||||
>
|
||||
<div class="col span-12">
|
||||
<label class="mb-5"><b>{{ t('harvester.setting.upgrade.imagePreloadStrategy') }}</b></label>
|
||||
<LabeledSelect
|
||||
v-model:value="parseDefaultValue.imagePreloadOption.strategy.type"
|
||||
class="mb-20"
|
||||
:mode="mode"
|
||||
:label="t('harvester.setting.upgrade.strategyType')"
|
||||
:options="strategyOptions"
|
||||
@update:value="update"
|
||||
/>
|
||||
<LabeledInput
|
||||
v-if="parseDefaultValue.imagePreloadOption.strategy.type === 'parallel'"
|
||||
v-model:value.number="parseDefaultValue.imagePreloadOption.strategy.concurrency"
|
||||
class="mb-20"
|
||||
:mode="mode"
|
||||
:label="t('harvester.setting.upgrade.concurrency')"
|
||||
min="0"
|
||||
type="number"
|
||||
/>
|
||||
<label class="mb-5"><b>{{ t('harvester.setting.upgrade.restoreVM') }}</b></label>
|
||||
<RadioGroup
|
||||
v-model:value="parseDefaultValue.restoreVM"
|
||||
class="mb-20"
|
||||
name="restoreVM"
|
||||
:options="[true, false]"
|
||||
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||
@update:value="update"
|
||||
/>
|
||||
<div
|
||||
v-if="errors.length"
|
||||
class="error"
|
||||
>
|
||||
{{ errors[0] }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.error {
|
||||
color: #d9534f;
|
||||
margin-top: 5px;
|
||||
}
|
||||
</style>
|
||||
@@ -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
|
||||
export const DOC = {
|
||||
CONSOLE_URL: `/host/#remote-console`,
|
||||
RANCHER_INTEGRATION_URL: `/rancher/rancher-integration`,
|
||||
KSMTUNED_MODE: `/host/#ksmtuned-mode`,
|
||||
UPGRADE_URL: `/upgrade/index`,
|
||||
UPGRADE_CONFIG_URL: `/advanced/index#upgrade-config`,
|
||||
STORAGE_NETWORK_EXAMPLE: `/advanced/storagenetwork#configuration-example`,
|
||||
SUPPORT_BUNDLE_NAMESPACES: `/advanced/index/#support-bundle-namespaces`,
|
||||
CONSOLE_URL: `/host/#remote-console`,
|
||||
RANCHER_INTEGRATION_URL: `/rancher/rancher-integration`,
|
||||
STORAGE_NETWORK_EXAMPLE: `/advanced/storagenetwork#configuration-example`,
|
||||
KSMTUNED_MODE: `/host/#ksmtuned-mode`,
|
||||
UPGRADE_URL: `/upgrade/index`
|
||||
};
|
||||
|
||||
@@ -21,7 +21,6 @@ const FEATURE_FLAGS = {
|
||||
'vmSnapshotQuota',
|
||||
'longhornV2LVMSupport',
|
||||
'improveMaintenanceMode',
|
||||
'upgradeConfigSetting'
|
||||
],
|
||||
'v1.4.1': [],
|
||||
'v1.4.2': [
|
||||
@@ -34,21 +33,9 @@ const FEATURE_FLAGS = {
|
||||
'efiPersistentState',
|
||||
'untaggedNetworkSetting',
|
||||
'skipSingleReplicaDetachedVol',
|
||||
'thirdPartyStorage',
|
||||
'liveMigrationProgress'
|
||||
'thirdPartyStorage'
|
||||
],
|
||||
'v1.5.1': [],
|
||||
'v1.6.0': [
|
||||
'vmMachineTypes',
|
||||
'customSupportBundle',
|
||||
'csiOnlineExpandValidation',
|
||||
'vmNetworkMigration',
|
||||
'kubeovnVpcSubnet',
|
||||
'rancherClusterSetting',
|
||||
'cpuMemoryHotplug',
|
||||
'cdiSettings',
|
||||
'vmCloneRunStrategy',
|
||||
]
|
||||
};
|
||||
|
||||
const generateFeatureFlags = () => {
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
CONFIGURED_PROVIDERS,
|
||||
SUB_TYPE,
|
||||
ADDRESS,
|
||||
DESCRIPTION,
|
||||
} from '@shell/config/table-headers';
|
||||
import { IF_HAVE } from '@shell/store/type-map';
|
||||
import {
|
||||
@@ -33,8 +32,7 @@ import {
|
||||
IMAGE_PROGRESS,
|
||||
SNAPSHOT_TARGET_VOLUME,
|
||||
IMAGE_VIRTUAL_SIZE,
|
||||
IMAGE_STORAGE_CLASS,
|
||||
HARVESTER_DESCRIPTION
|
||||
IMAGE_STORAGE_CLASS
|
||||
} from './table-headers';
|
||||
|
||||
const TEMPLATE = HCI.VM_VERSION;
|
||||
@@ -224,7 +222,6 @@ export function init($plugin, store) {
|
||||
STATE,
|
||||
NAME_COL,
|
||||
NAMESPACE_COL,
|
||||
HARVESTER_DESCRIPTION,
|
||||
IMAGE_STORAGE_CLASS,
|
||||
IMAGE_PROGRESS,
|
||||
IMAGE_DOWNLOAD_SIZE,
|
||||
@@ -258,7 +255,7 @@ export function init($plugin, store) {
|
||||
});
|
||||
|
||||
if (isSingleVirtualCluster) {
|
||||
headers(NAMESPACE, [STATE, NAME_UNLINKED, DESCRIPTION, AGE]);
|
||||
headers(NAMESPACE, [STATE, NAME_UNLINKED, AGE]);
|
||||
basicType([NAMESPACE]);
|
||||
virtualType({
|
||||
labelKey: 'harvester.namespace.label',
|
||||
@@ -424,7 +421,6 @@ export function init($plugin, store) {
|
||||
[
|
||||
HCI.CLUSTER_NETWORK,
|
||||
HCI.NETWORK_ATTACHMENT,
|
||||
HCI.VPC,
|
||||
HCI.LB,
|
||||
HCI.IP_POOL,
|
||||
],
|
||||
@@ -551,21 +547,6 @@ export function init($plugin, store) {
|
||||
exact: false
|
||||
});
|
||||
|
||||
configureType(HCI.VPC, { hiddenNamespaceGroupButton: true, canYaml: false });
|
||||
|
||||
virtualType({
|
||||
labelKey: 'harvester.vpc.label',
|
||||
name: HCI.VPC,
|
||||
namespaced: true,
|
||||
weight: 187,
|
||||
route: {
|
||||
name: `${ PRODUCT_NAME }-c-cluster-resource`,
|
||||
params: { resource: HCI.VPC }
|
||||
},
|
||||
exact: false,
|
||||
ifHaveType: HCI.VPC,
|
||||
});
|
||||
|
||||
configureType(HCI.SNAPSHOT, {
|
||||
isCreatable: false,
|
||||
location: {
|
||||
|
||||
@@ -70,7 +70,6 @@ export const ADD_ONS = {
|
||||
RANCHER_MONITORING: 'rancher-monitoring',
|
||||
VM_IMPORT_CONTROLLER: 'vm-import-controller',
|
||||
LVM_DRIVER: 'lvm.driver.harvesterhci.io',
|
||||
KUBEOVN_OPERATOR: 'kubeovn-operator',
|
||||
};
|
||||
|
||||
export const CSI_SECRETS = {
|
||||
|
||||
@@ -35,7 +35,6 @@ export const HCI = {
|
||||
NODE_SCHEDULABLE: 'kubevirt.io/schedulable',
|
||||
NETWORK_ROUTE: 'network.harvesterhci.io/route',
|
||||
MATCHED_NODES: 'network.harvesterhci.io/matched-nodes',
|
||||
UPGRADE: 'harvesterhci.io/upgrade',
|
||||
OS_UPGRADE_IMAGE: 'harvesterhci.io/os-upgrade-image',
|
||||
LATEST_UPGRADE: 'harvesterhci.io/latestUpgrade',
|
||||
UPGRADE_STATE: 'harvesterhci.io/upgradeState',
|
||||
@@ -67,13 +66,5 @@ export const HCI = {
|
||||
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',
|
||||
K8S_ARCH: 'kubernetes.io/arch'
|
||||
};
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export const CLUSTER_NETWORK = 'clusterNetwork';
|
||||
export const VPC = 'vpc';
|
||||
|
||||
@@ -28,17 +28,12 @@ export const HCI_SETTING = {
|
||||
RELEASE_DOWNLOAD_URL: 'release-download-url',
|
||||
CCM_CSI_VERSION: 'harvester-csi-ccm-versions',
|
||||
CSI_DRIVER_CONFIG: 'csi-driver-config',
|
||||
CSI_ONLINE_EXPAND_VALIDATION: 'csi-online-expand-validation',
|
||||
VM_TERMINATION_PERIOD: 'default-vm-termination-grace-period-seconds',
|
||||
NTP_SERVERS: 'ntp-servers',
|
||||
AUTO_ROTATE_RKE2_CERTS: 'auto-rotate-rke2-certs',
|
||||
KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES: 'kubeconfig-default-token-ttl-minutes',
|
||||
LONGHORN_V2_DATA_ENGINE_ENABLED: 'longhorn-v2-data-engine-enabled',
|
||||
ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO: 'additional-guest-memory-overhead-ratio',
|
||||
UPGRADE_CONFIG: 'upgrade-config',
|
||||
VM_MIGRATION_NETWORK: 'vm-migration-network',
|
||||
RANCHER_CLUSTER: 'rancher-cluster',
|
||||
MAX_HOTPLUG_RATIO: 'max-hotplug-ratio'
|
||||
};
|
||||
|
||||
export const HCI_ALLOWED_SETTINGS = {
|
||||
@@ -57,21 +52,18 @@ export const HCI_ALLOWED_SETTINGS = {
|
||||
from: 'import',
|
||||
featureFlag: 'autoRotateRke2CertsSetting'
|
||||
},
|
||||
[HCI_SETTING.CSI_DRIVER_CONFIG]: { kind: 'json', from: 'import' },
|
||||
[HCI_SETTING.CSI_ONLINE_EXPAND_VALIDATION]: {
|
||||
kind: 'json', from: 'import', featureFlag: 'csiOnlineExpandValidation'
|
||||
},
|
||||
[HCI_SETTING.SERVER_VERSION]: { readOnly: true },
|
||||
[HCI_SETTING.UPGRADE_CHECKER_ENABLED]: { kind: 'boolean' },
|
||||
[HCI_SETTING.UPGRADE_CHECKER_URL]: { kind: 'url' },
|
||||
[HCI_SETTING.HTTP_PROXY]: { kind: 'json', from: 'import' },
|
||||
[HCI_SETTING.ADDITIONAL_CA]: {
|
||||
[HCI_SETTING.CSI_DRIVER_CONFIG]: { kind: 'json', from: 'import' },
|
||||
[HCI_SETTING.SERVER_VERSION]: { readOnly: true },
|
||||
[HCI_SETTING.UPGRADE_CHECKER_ENABLED]: { kind: 'boolean' },
|
||||
[HCI_SETTING.UPGRADE_CHECKER_URL]: { kind: 'url' },
|
||||
[HCI_SETTING.HTTP_PROXY]: { kind: 'json', from: 'import' },
|
||||
[HCI_SETTING.ADDITIONAL_CA]: {
|
||||
kind: 'multiline', canReset: true, from: 'import'
|
||||
},
|
||||
[HCI_SETTING.OVERCOMMIT_CONFIG]: { kind: 'json', from: 'import' },
|
||||
[HCI_SETTING.SUPPORT_BUNDLE_TIMEOUT]: { kind: 'number' },
|
||||
[HCI_SETTING.SUPPORT_BUNDLE_EXPIRATION]: { kind: 'number' },
|
||||
[HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT]: { kind: 'number', featureFlag: 'supportBundleNodeCollectionTimeoutSetting' },
|
||||
[HCI_SETTING.SUPPORT_BUNDLE_TIMEOUT]: {},
|
||||
[HCI_SETTING.SUPPORT_BUNDLE_EXPIRATION]: {},
|
||||
[HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT]: { featureFlag: 'supportBundleNodeCollectionTimeoutSetting' },
|
||||
[HCI_SETTING.SUPPORT_BUNDLE_IMAGE]: { kind: 'json', from: 'import' },
|
||||
[HCI_SETTING.STORAGE_NETWORK]: {
|
||||
kind: 'custom', from: 'import', canReset: true
|
||||
@@ -97,26 +89,13 @@ export const HCI_ALLOWED_SETTINGS = {
|
||||
[HCI_SETTING.NTP_SERVERS]: {
|
||||
kind: 'json', from: 'import', canReset: true
|
||||
},
|
||||
[HCI_SETTING.KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES]: { kind: 'number', featureFlag: 'kubeconfigDefaultTokenTTLMinutesSetting' },
|
||||
[HCI_SETTING.KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES]: { featureFlag: 'kubeconfigDefaultTokenTTLMinutesSetting' },
|
||||
[HCI_SETTING.LONGHORN_V2_DATA_ENGINE_ENABLED]: {
|
||||
kind: 'boolean',
|
||||
experimental: true,
|
||||
featureFlag: 'longhornV2LVMSupport'
|
||||
},
|
||||
[HCI_SETTING.ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO]: { kind: 'string', from: 'import' },
|
||||
[HCI_SETTING.UPGRADE_CONFIG]: {
|
||||
kind: 'json',
|
||||
from: 'import',
|
||||
featureFlag: 'upgradeConfigSetting',
|
||||
docPath: 'UPGRADE_CONFIG_URL'
|
||||
},
|
||||
[HCI_SETTING.RANCHER_CLUSTER]: {
|
||||
kind: 'custom', from: 'import', canReset: true, featureFlag: 'rancherClusterSetting'
|
||||
},
|
||||
[HCI_SETTING.MAX_HOTPLUG_RATIO]: { kind: 'number', featureFlag: 'cpuMemoryHotplug' },
|
||||
[HCI_SETTING.VM_MIGRATION_NETWORK]: {
|
||||
kind: 'json', from: 'import', canReset: true, featureFlag: 'vmNetworkMigration',
|
||||
},
|
||||
};
|
||||
|
||||
export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/**
|
||||
* Harvester
|
||||
*/
|
||||
import { DESCRIPTION } from '@shell/config/table-headers';
|
||||
|
||||
// image
|
||||
export const IMAGE_DOWNLOAD_SIZE = {
|
||||
@@ -97,37 +96,5 @@ export const IMAGE_STORAGE_CLASS = {
|
||||
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',
|
||||
width: 200,
|
||||
};
|
||||
|
||||
@@ -5,21 +5,10 @@ export const BACKUP_TYPE = {
|
||||
|
||||
export const NETWORK_TYPE = {
|
||||
L2VLAN: 'L2VlanNetwork',
|
||||
UNTAGGED: 'UntaggedNetwork',
|
||||
OVERLAY: 'OverlayNetwork',
|
||||
UNTAGGED: 'UntaggedNetwork'
|
||||
};
|
||||
|
||||
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',
|
||||
};
|
||||
|
||||
@@ -47,26 +47,20 @@ export default {
|
||||
STATE,
|
||||
NAME,
|
||||
{
|
||||
name: 'CPU',
|
||||
label: 'CPU',
|
||||
sort: ['displayCPU'],
|
||||
value: 'displayCPU',
|
||||
align: 'center',
|
||||
dashIfEmpty: true,
|
||||
name: 'vmCPU',
|
||||
labelKey: 'tableHeaders.cpu',
|
||||
search: false,
|
||||
sort: ['spec.template.spec.domain.cpu.cores'],
|
||||
value: 'spec.template.spec.domain.cpu.cores',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
name: 'Memory',
|
||||
value: 'displayMemory',
|
||||
sort: ['memorySort'],
|
||||
align: 'center',
|
||||
labelKey: 'tableHeaders.memory',
|
||||
formatter: 'Si',
|
||||
formatterOpts: {
|
||||
opts: {
|
||||
increment: 1024, addSuffix: true, maxExponent: 3, minExponent: 3, suffix: 'i',
|
||||
},
|
||||
needParseSi: true
|
||||
},
|
||||
name: 'vmRAM',
|
||||
labelKey: 'glance.memory',
|
||||
search: false,
|
||||
sort: ['memorySort'],
|
||||
value: 'spec.template.spec.domain.resources.limits.memory',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
name: 'ip',
|
||||
|
||||
@@ -125,7 +125,6 @@ export default {
|
||||
:resource="value"
|
||||
:mode="mode"
|
||||
:apply-hooks="applyHooks"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<Tabbed
|
||||
v-if="spec"
|
||||
@@ -172,9 +171,6 @@ export default {
|
||||
:cpu="cpu"
|
||||
:mode="mode"
|
||||
:memory="memory"
|
||||
:max-cpu="maxCpu"
|
||||
:max-memory="maxMemory"
|
||||
:enable-hot-plug="cpuMemoryHotplugEnabled"
|
||||
/>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
||||
@@ -118,10 +118,6 @@ export default {
|
||||
imageName() {
|
||||
return this.value?.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_NAME] || '-';
|
||||
},
|
||||
|
||||
sourceType() {
|
||||
return this.value?.spec?.sourceType;
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -256,16 +252,6 @@ export default {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col span-12">
|
||||
<LabelValue
|
||||
:name="t('harvester.image.source')"
|
||||
:value="sourceType"
|
||||
class="mb-20"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="errorMessage !== '-'"
|
||||
class="row"
|
||||
|
||||
@@ -125,7 +125,6 @@ export default {
|
||||
:resource="value"
|
||||
:mode="mode"
|
||||
:apply-hooks="applyHooks"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<Tabbed
|
||||
v-if="spec"
|
||||
@@ -172,9 +171,6 @@ export default {
|
||||
:cpu="cpu"
|
||||
:mode="mode"
|
||||
:memory="memory"
|
||||
:max-cpu="maxCpu"
|
||||
:max-memory="maxMemory"
|
||||
:enable-hot-plug="cpuMemoryHotplugEnabled"
|
||||
/>
|
||||
|
||||
<div class="row mb-10">
|
||||
|
||||
+2
-3
@@ -5,7 +5,6 @@ import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import HarvesterIpAddress from '../../../formatters/HarvesterIpAddress';
|
||||
import VMConsoleBar from '../../../components/VMConsoleBar';
|
||||
import { HCI } from '../../../types';
|
||||
import { getVmCPUMemoryValues } from '../../../utils/cpuMemory';
|
||||
|
||||
const UNDEFINED = 'n/a';
|
||||
|
||||
@@ -92,9 +91,9 @@ export default {
|
||||
},
|
||||
|
||||
flavor() {
|
||||
const { cpu, memory } = getVmCPUMemoryValues(this.value);
|
||||
const domain = this.value?.spec?.template?.spec?.domain;
|
||||
|
||||
return `${ cpu } vCPU , ${ memory } ${ this.t('harvester.virtualMachine.input.memory') }`;
|
||||
return `${ domain.cpu?.cores } vCPU , ${ domain.resources?.limits?.memory } ${ this.t('harvester.virtualMachine.input.memory') }`;
|
||||
},
|
||||
|
||||
kernelRelease() {
|
||||
|
||||
-25
@@ -17,13 +17,6 @@ export default {
|
||||
default: () => {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
vmimResource: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -32,12 +25,6 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
liveMigrationProgressEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('liveMigrationProgress');
|
||||
},
|
||||
migrationPhase() {
|
||||
return this.vmimResource?.status?.phase || 'N/A';
|
||||
},
|
||||
migrationState() {
|
||||
return this.localResource?.status?.migrationState;
|
||||
},
|
||||
@@ -71,18 +58,6 @@ export default {
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-if="liveMigrationProgressEnabled"
|
||||
class="row mb-20"
|
||||
>
|
||||
<div class="col span-6">
|
||||
<LabelValue
|
||||
:name="t('harvester.virtualMachine.detail.details.phase')"
|
||||
:value="migrationPhase"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<LabelValue
|
||||
|
||||
@@ -27,8 +27,6 @@ import { formatSi } from '@shell/utils/units';
|
||||
|
||||
const VM_METRICS_DETAIL_URL = '/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/d/harvester-vm-detail-1/vm-info-detail?orgId=1';
|
||||
|
||||
const VM_MIGRATION_DETAIL_URL = '/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/d/harvester-vm-migration-details-1/harvester-vm-migration-details?orgId=1';
|
||||
|
||||
export default {
|
||||
name: 'VMIDetailsPage',
|
||||
|
||||
@@ -65,7 +63,6 @@ export default {
|
||||
hasResourceQuotaSchema: false,
|
||||
switchToCloud: false,
|
||||
VM_METRICS_DETAIL_URL,
|
||||
VM_MIGRATION_DETAIL_URL,
|
||||
showVmMetrics: false,
|
||||
};
|
||||
},
|
||||
@@ -81,7 +78,6 @@ export default {
|
||||
events: this.$store.dispatch(`${ inStore }/findAll`, { type: EVENT }),
|
||||
allSSHs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SSH }),
|
||||
vmis: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMI }),
|
||||
vmims: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VMIM }),
|
||||
restore: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.RESTORE }),
|
||||
};
|
||||
|
||||
@@ -135,39 +131,6 @@ export default {
|
||||
return this.$store.getters[`${ inStore }/all`](EVENT);
|
||||
},
|
||||
|
||||
vmim() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const vmimList = this.$store.getters[`${ inStore }/all`](HCI.VMIM) || [];
|
||||
|
||||
const vmiName = this.vmi?.name || '';
|
||||
|
||||
// filter the corresponding vmims by vmim.spec.vmiName and find the latest one by creationTimestamp
|
||||
const vmim = vmimList.filter((VMIM) => VMIM?.spec?.vmiName === vmiName).sort((a, b) => {
|
||||
if (a?.metadata?.creationTimestamp > b?.metadata?.creationTimestamp) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 1;
|
||||
});
|
||||
|
||||
return vmim.length > 0 && vmim[0] ? vmim[0] : null;
|
||||
},
|
||||
|
||||
migrationEvents() {
|
||||
const migrationVMName = this.vmim?.metadata.name || '';
|
||||
|
||||
if (migrationVMName === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.allEvents.filter((e) => {
|
||||
const { creationTimestamp } = this.value?.metadata || {};
|
||||
const involvedName = e?.involvedObject?.name;
|
||||
|
||||
return involvedName === migrationVMName && e.firstTimestamp >= creationTimestamp;
|
||||
}).sort((a, b) => a.lastTimestamp > b.lastTimestamp);
|
||||
},
|
||||
|
||||
events() {
|
||||
return this.allEvents.filter((e) => {
|
||||
const { name, creationTimestamp } = this.value?.metadata || {};
|
||||
@@ -175,6 +138,7 @@ export default {
|
||||
const pvcName = this.value.persistentVolumeClaimName || [];
|
||||
|
||||
const involvedName = e?.involvedObject?.name;
|
||||
|
||||
const matchPVC = pvcName.find((name) => name === involvedName);
|
||||
|
||||
return (involvedName === name || involvedName === podName || matchPVC) && e.firstTimestamp >= creationTimestamp;
|
||||
@@ -193,10 +157,6 @@ export default {
|
||||
vm: this.value.name
|
||||
};
|
||||
},
|
||||
|
||||
liveMigrationProgressEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('liveMigrationProgress');
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
@@ -386,20 +346,6 @@ export default {
|
||||
<Migration
|
||||
:value="value"
|
||||
:vmi-resource="vmi"
|
||||
:vmim-resource="vmim"
|
||||
/>
|
||||
<DashboardMetrics
|
||||
v-if="showVmMetrics && liveMigrationProgressEnabled"
|
||||
:detail-url="VM_MIGRATION_DETAIL_URL"
|
||||
graph-height="640px"
|
||||
:has-summary-and-detail="false"
|
||||
:vars="graphVars"
|
||||
class="mb-30"
|
||||
/>
|
||||
<Events
|
||||
v-if="liveMigrationProgressEnabled"
|
||||
:resource="vmi"
|
||||
:events="migrationEvents"
|
||||
/>
|
||||
</Tab>
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<script>
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { runStrategies as runStrategyOptions } from '../config/harvester-map';
|
||||
|
||||
import { Card } from '@components/Card';
|
||||
import { Banner } from '@components/Banner';
|
||||
import { Checkbox } from '@components/Form/Checkbox';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
|
||||
export default {
|
||||
name: 'CloneVMModal',
|
||||
@@ -15,7 +14,7 @@ export default {
|
||||
emits: ['close'],
|
||||
|
||||
components: {
|
||||
AsyncButton, Banner, Checkbox, Card, LabeledInput, LabeledSelect
|
||||
AsyncButton, Banner, Checkbox, Card, LabeledInput
|
||||
},
|
||||
|
||||
props: {
|
||||
@@ -27,11 +26,9 @@ export default {
|
||||
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
cloneData: true,
|
||||
errors: [],
|
||||
runStrategy: runStrategyOptions[1],
|
||||
runStrategyOptions
|
||||
name: '',
|
||||
cloneData: true,
|
||||
errors: []
|
||||
};
|
||||
},
|
||||
|
||||
@@ -41,9 +38,6 @@ export default {
|
||||
actionResource() {
|
||||
return this.resources[0];
|
||||
},
|
||||
vmCloneRunStrategyEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('vmCloneRunStrategy');
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
@@ -64,8 +58,7 @@ export default {
|
||||
|
||||
// deep clone
|
||||
try {
|
||||
const payload = this.vmCloneRunStrategyEnabled ? { targetVm: this.name, runStrategy: this.runStrategy } : { targetVm: this.name };
|
||||
const res = await this.actionResource.doAction('clone', payload, {}, false);
|
||||
const res = await this.actionResource.doAction('clone', { targetVm: this.name }, {}, false);
|
||||
|
||||
if (res._status === 200 || res._status === 204) {
|
||||
this.$store.dispatch('growl/success', {
|
||||
@@ -113,13 +106,6 @@ export default {
|
||||
:label="t('harvester.modal.cloneVM.name')"
|
||||
required
|
||||
/>
|
||||
<LabeledSelect
|
||||
v-if="vmCloneRunStrategyEnabled"
|
||||
v-model:value="runStrategy"
|
||||
label-key="harvester.virtualMachine.runStrategy"
|
||||
:options="runStrategyOptions"
|
||||
:mode="mode"
|
||||
/>
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
<script>
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import { Card } from '@components/Card';
|
||||
import { Banner } from '@components/Banner';
|
||||
import { Checkbox } from '@components/Form/Checkbox';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
|
||||
export default {
|
||||
name: 'CloneVMModal',
|
||||
|
||||
emits: ['close'],
|
||||
|
||||
components: {
|
||||
AsyncButton, Banner, Checkbox, Card, LabeledInput
|
||||
},
|
||||
|
||||
props: {
|
||||
resources: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
cloneData: true,
|
||||
errors: []
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
actionResource() {
|
||||
return this.resources[0];
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
close() {
|
||||
this.name = '';
|
||||
this.$emit('close');
|
||||
},
|
||||
|
||||
async create(buttonCb) {
|
||||
// shadow clone
|
||||
if (!this.cloneData) {
|
||||
this.resources[0].goToClone();
|
||||
buttonCb(false);
|
||||
this.close();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// deep clone
|
||||
try {
|
||||
const res = await this.actionResource.doAction('clone', { targetVm: this.name }, {}, false);
|
||||
|
||||
if (res._status === 200 || res._status === 204) {
|
||||
this.$store.dispatch('growl/success', {
|
||||
title: this.t('harvester.notification.title.succeed'),
|
||||
message: this.t('harvester.modal.cloneVM.message.success', { name: this.name })
|
||||
}, { root: true });
|
||||
|
||||
this.close();
|
||||
buttonCb(true);
|
||||
} else {
|
||||
const error = [res?.data] || exceptionToErrorsArray(res);
|
||||
|
||||
this['errors'] = error;
|
||||
buttonCb(false);
|
||||
}
|
||||
} catch (err) {
|
||||
const error = err?.data || err;
|
||||
const message = exceptionToErrorsArray(error);
|
||||
|
||||
this['errors'] = message;
|
||||
buttonCb(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card :show-highlight-border="false">
|
||||
<template #title>
|
||||
{{ t('harvester.modal.cloneVM.title') }}
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<Checkbox
|
||||
v-model:value="cloneData"
|
||||
class="mb-10"
|
||||
label-key="harvester.modal.cloneVM.type"
|
||||
/>
|
||||
|
||||
<LabeledInput
|
||||
v-show="cloneData"
|
||||
v-model:value="name"
|
||||
class="mb-20"
|
||||
:label="t('harvester.modal.cloneVM.name')"
|
||||
required
|
||||
/>
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
:label="err"
|
||||
color="error"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<div class="actions">
|
||||
<div class="buttons">
|
||||
<button
|
||||
class="btn role-secondary mr-10"
|
||||
@click="close"
|
||||
>
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
|
||||
<AsyncButton
|
||||
mode="create"
|
||||
:action-label="cloneData ? t('harvester.modal.cloneVM.action.create') : t('harvester.modal.cloneVM.action.clone')"
|
||||
:disabled="cloneData && !name"
|
||||
@click="create"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,185 +0,0 @@
|
||||
<script>
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { getVmCPUMemoryValues } from '../utils/cpuMemory';
|
||||
import UnitInput from '@shell/components/form/UnitInput';
|
||||
import { Card } from '@components/Card';
|
||||
import { Banner } from '@components/Banner';
|
||||
import AsyncButton from '@shell/components/AsyncButton';
|
||||
import { GIBIBYTE } from '../utils/unit';
|
||||
|
||||
export default {
|
||||
name: 'CPUMemoryHotplugModal',
|
||||
|
||||
emits: ['close'],
|
||||
|
||||
components: {
|
||||
AsyncButton, Card, Banner, UnitInput
|
||||
},
|
||||
|
||||
props: {
|
||||
resources: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
const { cpu, memory } = getVmCPUMemoryValues(this.resources[0] || {});
|
||||
|
||||
return {
|
||||
cpu,
|
||||
memory,
|
||||
errors: [],
|
||||
GIBIBYTE
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
|
||||
actionResource() {
|
||||
return this.resources[0] || {};
|
||||
},
|
||||
maxResourcesMessage() {
|
||||
const { maxCpu, maxMemory } = getVmCPUMemoryValues(this.actionResource);
|
||||
|
||||
if (maxCpu && maxMemory) {
|
||||
return this.t('harvester.modal.cpuMemoryHotplug.maxResourcesMessage', { maxCpu, maxMemory });
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
close() {
|
||||
this.cpu = '';
|
||||
this.memory = '';
|
||||
this.$emit('close');
|
||||
},
|
||||
|
||||
change() {
|
||||
if (parseInt(this.memory, 10) < 1 ) {
|
||||
this.memory = '1Gi';
|
||||
}
|
||||
if (this.cpu < 1) {
|
||||
this.cpu = 1;
|
||||
}
|
||||
},
|
||||
|
||||
async save(buttonCb) {
|
||||
if (this.actionResource) {
|
||||
try {
|
||||
const res = await this.actionResource.doAction('cpuAndMemoryHotplug', { sockets: this.cpu, memory: this.memory });
|
||||
|
||||
if (res._status === 200 || res._status === 204) {
|
||||
this.$store.dispatch('growl/success', {
|
||||
title: this.t('generic.notification.title.succeed'),
|
||||
message: this.t('harvester.modal.cpuMemoryHotplug.success', { vm: this.actionResource.nameDisplay })
|
||||
}, { root: true });
|
||||
|
||||
this.close();
|
||||
buttonCb(true);
|
||||
} else {
|
||||
const error = [res?.data] || exceptionToErrorsArray(res);
|
||||
|
||||
this['errors'] = error;
|
||||
buttonCb(false);
|
||||
}
|
||||
} catch (err) {
|
||||
const error = err?.data || err;
|
||||
const message = exceptionToErrorsArray(error);
|
||||
|
||||
this['errors'] = message;
|
||||
buttonCb(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card
|
||||
ref="modal"
|
||||
name="modal"
|
||||
:show-highlight-border="false"
|
||||
>
|
||||
<template #title>
|
||||
<h4
|
||||
v-clean-html="t('harvester.modal.cpuMemoryHotplug.title')"
|
||||
class="text-default-text"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<Banner
|
||||
v-if="maxResourcesMessage"
|
||||
:label="maxResourcesMessage"
|
||||
color="info"
|
||||
/>
|
||||
<UnitInput
|
||||
v-model:value="cpu"
|
||||
:label="t('harvester.virtualMachine.input.cpu')"
|
||||
:delay="0"
|
||||
required
|
||||
suffix="C"
|
||||
class="mb-20"
|
||||
:mode="mode"
|
||||
:min="1"
|
||||
@update:value="change"
|
||||
/>
|
||||
<UnitInput
|
||||
v-model:value="memory"
|
||||
:label="t('harvester.virtualMachine.input.memory')"
|
||||
:mode="mode"
|
||||
:input-exponent="3"
|
||||
:delay="0"
|
||||
:min="1"
|
||||
:increment="1024"
|
||||
:output-modifier="true"
|
||||
:disabled="disabled"
|
||||
:suffix="GIBIBYTE"
|
||||
class="mb-20"
|
||||
@update:value="change"
|
||||
/>
|
||||
<Banner
|
||||
v-for="(err, i) in errors"
|
||||
:key="i"
|
||||
:label="err"
|
||||
color="error"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<div class="actions">
|
||||
<div class="buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="btn role-secondary mr-10"
|
||||
@click="close"
|
||||
>
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
<AsyncButton
|
||||
mode="apply"
|
||||
@click="save"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -10,7 +10,6 @@ import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { NAMESPACE, STORAGE_CLASS } from '@shell/config/types';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { isInternalStorageClass } from '../utils/storage-class';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterExportImageDialog',
|
||||
@@ -106,17 +105,11 @@ export default {
|
||||
}
|
||||
|
||||
const options = storages.filter((s) => !s.parameters?.backingImage).map((s) => {
|
||||
let label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||
const isInternal = isInternalStorageClass(s.metadata?.name);
|
||||
|
||||
if (isInternal) {
|
||||
label += ` (${ this.t('harvester.storage.internal.label') })`;
|
||||
}
|
||||
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||
|
||||
return {
|
||||
label,
|
||||
value: s.name,
|
||||
disabled: isInternal,
|
||||
value: s.name,
|
||||
};
|
||||
}) || [];
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script>
|
||||
import { NAMESPACE } from '@shell/config/types';
|
||||
import { randomStr } from '@shell/utils/string';
|
||||
import { exceptionToErrorsArray, stringify } from '@shell/utils/error';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
@@ -7,15 +6,7 @@ import AsyncButton from '@shell/components/AsyncButton';
|
||||
import GraphCircle from '@shell/components/graph/Circle';
|
||||
import { Banner } from '@components/Banner';
|
||||
import AppModal from '@shell/components/AppModal';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import UnitInput from '@shell/components/form/UnitInput';
|
||||
import { HCI } from '../types';
|
||||
import { HCI_SETTING } from '../config/settings';
|
||||
import { DOC } from '../config/doc-links';
|
||||
import { docLink } from '../utils/feature-flags';
|
||||
|
||||
const SELECT_ALL = 'select_all';
|
||||
const UNSELECT_ALL = 'unselect_all';
|
||||
|
||||
export default {
|
||||
name: 'SupportBundle',
|
||||
@@ -26,36 +17,14 @@ export default {
|
||||
AsyncButton,
|
||||
Banner,
|
||||
AppModal,
|
||||
LabeledSelect,
|
||||
UnitInput
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
await this.$store.dispatch('harvester/findAll', { type: NAMESPACE });
|
||||
|
||||
try {
|
||||
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/namespaces?link=supportbundle');
|
||||
const response = await this.$store.dispatch('harvester/request', { url });
|
||||
|
||||
this.defaultNamespaces = response.data || [];
|
||||
} catch (error) {
|
||||
this.defaultNamespaces = [];
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isOpen: false,
|
||||
errors: [],
|
||||
version: '',
|
||||
clusterName: '',
|
||||
url: '',
|
||||
description: '',
|
||||
namespaces: [],
|
||||
defaultNamespaces: [],
|
||||
timeout: '',
|
||||
expiration: '',
|
||||
nodeTimeout: '',
|
||||
url: '',
|
||||
description: '',
|
||||
errors: [],
|
||||
isOpen: false,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -70,51 +39,23 @@ export default {
|
||||
|
||||
percentage() {
|
||||
return this.$store.getters['harvester-common/getBundlePercentage'];
|
||||
},
|
||||
|
||||
availableNamespaces() {
|
||||
const allNamespaces = this.$store.getters['harvester/all'](NAMESPACE).map((ns) => ns.id);
|
||||
const defaultNamespacesIds = this.defaultNamespaces.map((ns) => ns.id);
|
||||
|
||||
return allNamespaces.filter((ns) => !defaultNamespacesIds.includes(ns) || this.namespaces.includes(ns));
|
||||
},
|
||||
|
||||
namespaceOptions() {
|
||||
if (this.availableNamespaces.length === 0) return [];
|
||||
|
||||
const allSelected = this.namespaces.length === this.availableNamespaces.length &&
|
||||
this.availableNamespaces.every((ns) => this.namespaces.includes(ns));
|
||||
|
||||
const controlOption = allSelected ? { label: this.t('harvester.modal.bundle.namespaces.unselectAll'), value: UNSELECT_ALL } : { label: this.t('harvester.modal.bundle.namespaces.selectAll'), value: SELECT_ALL };
|
||||
|
||||
return [controlOption, ...this.availableNamespaces];
|
||||
},
|
||||
|
||||
docLink() {
|
||||
const version = this.$store.getters['harvester-common/getServerVersion']();
|
||||
|
||||
return docLink(DOC.SUPPORT_BUNDLE_NAMESPACES, version);
|
||||
},
|
||||
|
||||
customSupportBundleFeatureEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('customSupportBundle');
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
isShowBundleModal: {
|
||||
immediate: true,
|
||||
handler(show) {
|
||||
this.isOpen = show;
|
||||
}
|
||||
},
|
||||
|
||||
isOpen(newVal) {
|
||||
if (newVal) {
|
||||
this.loadDefaultSettings();
|
||||
} else {
|
||||
this.resetForm();
|
||||
}
|
||||
if (show) {
|
||||
this.$nextTick(() => {
|
||||
this.isOpen = true;
|
||||
});
|
||||
} else {
|
||||
this.isOpen = false;
|
||||
this.url = '';
|
||||
this.description = '';
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
},
|
||||
|
||||
@@ -124,93 +65,33 @@ export default {
|
||||
close() {
|
||||
this.isOpen = false;
|
||||
this.$store.commit('harvester-common/toggleBundleModal', false);
|
||||
},
|
||||
|
||||
loadDefaultSettings() {
|
||||
const cluster = this.$store.getters['currentCluster'];
|
||||
const versionSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SERVER_VERSION);
|
||||
const namespacesSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_NAMESPACES);
|
||||
const timeoutSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_TIMEOUT);
|
||||
const expirationSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_EXPIRATION);
|
||||
const nodeTimeoutSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT);
|
||||
|
||||
this.version = versionSetting?.currentVersion || '';
|
||||
this.clusterName = cluster?.id || '';
|
||||
this.namespaces = (namespacesSetting?.value ?? namespacesSetting?.default ?? '').split(',').map((ns) => ns.trim()).filter((ns) => ns);
|
||||
this.timeout = timeoutSetting?.value ?? timeoutSetting?.default ?? '';
|
||||
this.expiration = expirationSetting?.value ?? expirationSetting?.default ?? '';
|
||||
this.nodeTimeout = nodeTimeoutSetting?.value ?? nodeTimeoutSetting?.default ?? '';
|
||||
this.url = '';
|
||||
this.description = '';
|
||||
this.errors = [];
|
||||
},
|
||||
|
||||
resetForm() {
|
||||
this.url = '';
|
||||
this.description = '';
|
||||
this.namespaces = [];
|
||||
this.timeout = '';
|
||||
this.expiration = '';
|
||||
this.nodeTimeout = '';
|
||||
this.errors = [];
|
||||
},
|
||||
|
||||
updateNamespaces(selected) {
|
||||
if (selected.includes(SELECT_ALL)) {
|
||||
this.namespaces = [...this.availableNamespaces];
|
||||
} else if (selected.includes(UNSELECT_ALL)) {
|
||||
this.namespaces = [];
|
||||
} else {
|
||||
this.namespaces = selected.filter((val) => val !== SELECT_ALL && val !== UNSELECT_ALL);
|
||||
}
|
||||
},
|
||||
|
||||
updateNumberValue(field, value) {
|
||||
if (value === '' || value === null || isNaN(value)) {
|
||||
this[field] = '';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const num = Number(value);
|
||||
const isValid = Number.isInteger(num) && num >= 0;
|
||||
|
||||
this[field] = isValid ? String(num) : '';
|
||||
},
|
||||
|
||||
onKeyDown(e) {
|
||||
if (['e', 'E', '+', '-', '.'].includes(e.key)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
this.backUpName = '';
|
||||
},
|
||||
|
||||
async save(buttonCb) {
|
||||
this.errors = [];
|
||||
|
||||
const name = `bundle-${ this.clusterName }-${ this.version }-${ randomStr(5).toLowerCase() }`;
|
||||
const name = `bundle-${ randomStr(5).toLowerCase() }`;
|
||||
const namespace = 'harvester-system';
|
||||
|
||||
const spec = {
|
||||
description: this.description.trim(),
|
||||
...(this.url.trim() && { issueURL: this.url.trim() }),
|
||||
...(this.namespaces.length > 0 && { extraCollectionNamespaces: this.namespaces }),
|
||||
...(this.timeout !== '' && { timeout: Number(this.timeout) }),
|
||||
...(this.expiration !== '' && { expiration: Number(this.expiration) }),
|
||||
...(this.nodeTimeout !== '' && { nodeTimeout: Number(this.nodeTimeout) }),
|
||||
};
|
||||
|
||||
const bundleCrd = {
|
||||
apiVersion: 'harvesterhci.io/v1beta1',
|
||||
type: HCI.SUPPORT_BUNDLE,
|
||||
kind: 'SupportBundle',
|
||||
metadata: { name, namespace },
|
||||
spec,
|
||||
metadata: {
|
||||
name,
|
||||
namespace
|
||||
},
|
||||
spec: {
|
||||
issueURL: this.url,
|
||||
description: this.description
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const bundleValue = await this.$store.dispatch(`${ inStore }/create`, bundleCrd);
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const bundleValue = await this.$store.dispatch(`${ inStore }/create`, bundleCrd);
|
||||
|
||||
try {
|
||||
await bundleValue.save();
|
||||
|
||||
this.$store.commit('harvester-common/setLatestBundleId', `${ namespace }/${ name }`, { root: true });
|
||||
@@ -237,110 +118,69 @@ export default {
|
||||
@close="close"
|
||||
>
|
||||
<div class="p-20">
|
||||
<h2>{{ t('harvester.modal.bundle.title') }}</h2>
|
||||
<div class="content">
|
||||
<div
|
||||
v-if="bundlePending"
|
||||
class="circle mb-20"
|
||||
>
|
||||
<h2>
|
||||
{{ t('harvester.modal.bundle.title') }}
|
||||
</h2>
|
||||
|
||||
<div
|
||||
v-if="!bundlePending"
|
||||
class="content"
|
||||
>
|
||||
<LabeledInput
|
||||
v-model:value="url"
|
||||
:label="t('harvester.modal.bundle.url')"
|
||||
class="mb-20"
|
||||
/>
|
||||
|
||||
<LabeledInput
|
||||
v-model:value="description"
|
||||
:label="t('harvester.modal.bundle.description')"
|
||||
type="multiline"
|
||||
:min-height="120"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="content"
|
||||
>
|
||||
<div class="circle">
|
||||
<GraphCircle
|
||||
primary-stroke-color="green"
|
||||
secondary-stroke-color="lightgrey"
|
||||
secondary-stroke-color="white"
|
||||
:stroke-width="6"
|
||||
:percentage="percentage"
|
||||
:show-text="true"
|
||||
/>
|
||||
</div>
|
||||
<template v-else>
|
||||
<p
|
||||
v-if="customSupportBundleFeatureEnabled"
|
||||
v-clean-html="t('harvester.modal.bundle.tip', { doc: docLink }, true)"
|
||||
class="mb-20"
|
||||
/>
|
||||
<LabeledInput
|
||||
v-model:value="url"
|
||||
:label="t('harvester.modal.bundle.url')"
|
||||
class="mb-10"
|
||||
/>
|
||||
<LabeledInput
|
||||
v-model:value="description"
|
||||
required
|
||||
:label="t('harvester.modal.bundle.description')"
|
||||
type="multiline"
|
||||
:min-height="80"
|
||||
class="mb-10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template v-if="customSupportBundleFeatureEnabled">
|
||||
<LabeledSelect
|
||||
v-model:value="namespaces"
|
||||
:label="t('harvester.modal.bundle.namespaces.label')"
|
||||
:clearable="true"
|
||||
:multiple="true"
|
||||
:options="namespaceOptions"
|
||||
class="mb-10 label-select"
|
||||
:tooltip="t('harvester.modal.bundle.namespaces.tooltip', _, true)"
|
||||
@update:value="updateNamespaces"
|
||||
/>
|
||||
<UnitInput
|
||||
v-model:value="timeout"
|
||||
:label="t('harvester.modal.bundle.timeout.label')"
|
||||
class="mb-10"
|
||||
type="number"
|
||||
:min="0"
|
||||
:tooltip="t('harvester.modal.bundle.timeout.tooltip', _, true)"
|
||||
:suffix="timeout > 1 ? 'Minutes' : 'Minute'"
|
||||
@keydown="onKeyDown"
|
||||
@update:value="val => updateNumberValue('timeout', val)"
|
||||
/>
|
||||
<UnitInput
|
||||
v-model:value="expiration"
|
||||
:label="t('harvester.modal.bundle.expiration.label')"
|
||||
class="mb-10"
|
||||
type="number"
|
||||
:min="0"
|
||||
:tooltip="t('harvester.modal.bundle.expiration.tooltip', _, true)"
|
||||
:suffix="expiration > 1 ? 'Minutes' : 'Minute'"
|
||||
@keydown="onKeyDown"
|
||||
@update:value="val => updateNumberValue('expiration', val)"
|
||||
/>
|
||||
<UnitInput
|
||||
v-model:value="nodeTimeout"
|
||||
:label="t('harvester.modal.bundle.nodeTimeout.label')"
|
||||
class="mb-10"
|
||||
type="number"
|
||||
:min="0"
|
||||
:tooltip="t('harvester.modal.bundle.nodeTimeout.tooltip', _, true)"
|
||||
:suffix="nodeTimeout > 1 ? 'Minutes' : 'Minute'"
|
||||
@keydown="onKeyDown"
|
||||
@update:value="val => updateNumberValue('nodeTimeout', val)"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
<div
|
||||
v-for="(err, idx) in errors"
|
||||
:key="idx"
|
||||
<div
|
||||
v-for="(err, idx) in errors"
|
||||
:key="idx"
|
||||
>
|
||||
<Banner
|
||||
color="error"
|
||||
:label="stringify(err)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="footer mt-20">
|
||||
<button
|
||||
class="btn btn-sm role-secondary mr-10"
|
||||
@click="close"
|
||||
>
|
||||
<Banner
|
||||
color="error"
|
||||
:label="stringify(err)"
|
||||
/>
|
||||
</div>
|
||||
<div class="footer mt-20">
|
||||
<button
|
||||
class="btn btn-sm role-secondary mr-10"
|
||||
@click="close"
|
||||
>
|
||||
{{ t('generic.close') }}
|
||||
</button>
|
||||
<AsyncButton
|
||||
type="submit"
|
||||
mode="generate"
|
||||
class="btn btn-sm bg-primary"
|
||||
:disabled="bundlePending"
|
||||
@click="save"
|
||||
/>
|
||||
</div>
|
||||
{{ t('generic.close') }}
|
||||
</button>
|
||||
|
||||
<AsyncButton
|
||||
type="submit"
|
||||
mode="generate"
|
||||
class="btn btn-sm bg-primary"
|
||||
:disabled="bundlePending"
|
||||
@click="save"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</app-modal>
|
||||
@@ -354,10 +194,6 @@ export default {
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
.labeled-select.taggable ::v-deep(.vs__selected-options .vs__selected.vs__selected > button) {
|
||||
margin: 0 7px;
|
||||
}
|
||||
|
||||
.bundle {
|
||||
cursor: pointer;
|
||||
color: var(--primary);
|
||||
@@ -368,8 +204,10 @@ export default {
|
||||
}
|
||||
|
||||
.content {
|
||||
height: 218px;
|
||||
|
||||
.circle {
|
||||
padding: 10px 0;
|
||||
padding-top: 20px;
|
||||
height: 160px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,10 +73,8 @@ export default {
|
||||
|
||||
data.spec.clusterNetwork = this.clusterNetwork;
|
||||
|
||||
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl'](`v1/harvester/${ HCI.VLAN_CONFIG }s/${ data.id }`);
|
||||
|
||||
await this.$store.dispatch('harvester/request', {
|
||||
url,
|
||||
url: `/v1/harvester/${ HCI.VLAN_CONFIG }s/${ data.id }`,
|
||||
method: 'PUT',
|
||||
data,
|
||||
});
|
||||
|
||||
@@ -38,7 +38,6 @@ export default {
|
||||
:mode="mode"
|
||||
:errors="errors"
|
||||
@finish="save"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<component
|
||||
:is="currentComponent"
|
||||
|
||||
@@ -80,7 +80,6 @@ export default {
|
||||
:apply-hooks="applyHooks"
|
||||
@finish="save"
|
||||
@cancel="done"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<NameNsDescription
|
||||
:value="value"
|
||||
|
||||
@@ -75,7 +75,6 @@ export default {
|
||||
:errors="errors"
|
||||
:apply-hooks="applyHooks"
|
||||
@finish="save"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<div class="header mb-20">
|
||||
<FileSelector
|
||||
|
||||
@@ -191,7 +191,7 @@ export default {
|
||||
:validation-passed="true"
|
||||
:errors="errors"
|
||||
:can-yaml="true"
|
||||
@error="e=>errors=e"
|
||||
@error="e=>errors = e"
|
||||
@finish="save"
|
||||
@cancel="done"
|
||||
>
|
||||
|
||||
@@ -189,7 +189,7 @@ export default {
|
||||
:resource="value"
|
||||
:subtypes="[]"
|
||||
:cancel-event="true"
|
||||
@error="e=>errors=e"
|
||||
@error="e=>errors = e"
|
||||
@finish="save"
|
||||
@cancel="done"
|
||||
>
|
||||
|
||||
@@ -6,13 +6,14 @@ import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import { RadioGroup } from '@components/Form/Radio';
|
||||
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
|
||||
import { HCI as HCI_LABELS_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { HCI } from '../types';
|
||||
import { NETWORK_TYPE } from '../config/types';
|
||||
|
||||
const { L2VLAN, UNTAGGED, OVERLAY } = NETWORK_TYPE;
|
||||
const { L2VLAN, UNTAGGED } = NETWORK_TYPE;
|
||||
|
||||
const AUTO = 'auto';
|
||||
const MANUAL = 'manual';
|
||||
@@ -43,10 +44,13 @@ export default {
|
||||
|
||||
data() {
|
||||
const config = JSON.parse(this.value.spec.config);
|
||||
|
||||
const annotations = this.value?.metadata?.annotations || {};
|
||||
const layer3Network = JSON.parse(annotations[HCI_LABELS_ANNOTATIONS.NETWORK_ROUTE] || '{}');
|
||||
|
||||
if ((config.bridge || '').endsWith('-br')) {
|
||||
config.bridge = config.bridge.slice(0, -3);
|
||||
}
|
||||
|
||||
const type = this.value.vlanType || L2VLAN ;
|
||||
|
||||
return {
|
||||
@@ -74,30 +78,6 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
clusterBridge: {
|
||||
get() {
|
||||
if (!this.config.bridge) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// remove -br suffix if exists
|
||||
return this.config?.bridge?.endsWith('-br') ? this.config.bridge.slice(0, -3) : '';
|
||||
},
|
||||
|
||||
set(neu) {
|
||||
if (neu === '') {
|
||||
this.config.bridge = '';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!neu.endsWith('-br')) {
|
||||
this.config.bridge = `${ neu }-br`;
|
||||
} else {
|
||||
this.config.bridge = neu;
|
||||
}
|
||||
}
|
||||
},
|
||||
modeOptions() {
|
||||
return [{
|
||||
label: this.t('harvester.network.layer3Network.mode.auto'),
|
||||
@@ -108,14 +88,6 @@ export default {
|
||||
}];
|
||||
},
|
||||
|
||||
kubeovnVpcSubnetSupport() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('kubeovnVpcSubnet');
|
||||
},
|
||||
|
||||
longhornV2LVMSupport() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('longhornV2LVMSupport');
|
||||
},
|
||||
|
||||
clusterNetworkOptions() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
|
||||
@@ -131,30 +103,8 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
networkTypes() {
|
||||
const types = [L2VLAN, UNTAGGED];
|
||||
|
||||
if (this.kubeovnVpcSubnetSupport) {
|
||||
types.push(OVERLAY);
|
||||
}
|
||||
|
||||
return types;
|
||||
},
|
||||
|
||||
isL2VlanNetwork() {
|
||||
if (this.isView) {
|
||||
return this.value.vlanType === L2VLAN;
|
||||
}
|
||||
|
||||
return this.type === L2VLAN;
|
||||
},
|
||||
|
||||
isOverlayNetwork() {
|
||||
if (this.isView) {
|
||||
return this.value.vlanType === OVERLAY;
|
||||
}
|
||||
|
||||
return this.type === OVERLAY;
|
||||
networkType() {
|
||||
return [L2VLAN, UNTAGGED];
|
||||
},
|
||||
|
||||
isUntaggedNetwork() {
|
||||
@@ -166,54 +116,36 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
type(newType) {
|
||||
if (newType === OVERLAY) {
|
||||
this.config.type = 'kube-ovn';
|
||||
this.config.provider = `${ this.value.metadata.name }.${ this.value.metadata.namespace }.ovn`;
|
||||
this.config.server_socket = '/run/openvswitch/kube-ovn-daemon.sock';
|
||||
} else {
|
||||
this.config.type = 'bridge';
|
||||
this.config.promiscMode = true;
|
||||
this.config.ipam = {};
|
||||
this.config.bridge = '';
|
||||
delete this.config.provider;
|
||||
delete this.config.server_socket;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async saveNetwork(buttonCb) {
|
||||
const errors = [];
|
||||
|
||||
if (this.isL2VlanNetwork || this.isUntaggedNetwork) {
|
||||
if (!this.config.vlan && !this.isUntaggedNetwork) {
|
||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('tableHeaders.networkVlan') }));
|
||||
}
|
||||
|
||||
if (!this.config.bridge) {
|
||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.clusterNetwork.label') }));
|
||||
}
|
||||
|
||||
if (this.layer3Network.mode === MANUAL) {
|
||||
if (!this.layer3Network.gateway) {
|
||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.gateway.label') }));
|
||||
}
|
||||
if (!this.layer3Network.cidr) {
|
||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.cidr.label') }));
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
buttonCb(false);
|
||||
this.errors = errors;
|
||||
|
||||
return false;
|
||||
}
|
||||
this.value.setAnnotation(HCI_LABELS_ANNOTATIONS.NETWORK_ROUTE, JSON.stringify(this.layer3Network));
|
||||
if (!this.config.vlan && !this.isUntaggedNetwork) {
|
||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('tableHeaders.networkVlan') }));
|
||||
}
|
||||
|
||||
if (!this.config.bridge) {
|
||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.clusterNetwork.label') }));
|
||||
}
|
||||
|
||||
if (this.layer3Network.mode === MANUAL) {
|
||||
if (!this.layer3Network.gateway) {
|
||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.gateway.label') }));
|
||||
}
|
||||
if (!this.layer3Network.cidr) {
|
||||
errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.cidr.label') }));
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
buttonCb(false);
|
||||
this.errors = errors;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
this.value.setAnnotation(HCI_LABELS_ANNOTATIONS.NETWORK_ROUTE, JSON.stringify(this.layer3Network));
|
||||
|
||||
await this.save(buttonCb);
|
||||
},
|
||||
|
||||
@@ -237,19 +169,14 @@ export default {
|
||||
updateBeforeSave() {
|
||||
this.config.name = this.value.metadata.name;
|
||||
|
||||
if (this.isOverlayNetwork) {
|
||||
this.config.provider = `${ this.value.metadata.name }.${ this.value.metadata.namespace }.ovn`;
|
||||
delete this.config.bridge;
|
||||
delete this.config.promiscMode;
|
||||
delete this.config.vlan;
|
||||
delete this.config.ipam;
|
||||
}
|
||||
|
||||
if (this.isUntaggedNetwork) {
|
||||
delete this.config.vlan;
|
||||
}
|
||||
|
||||
this.value.spec.config = JSON.stringify({ ...this.config });
|
||||
this.value.spec.config = JSON.stringify({
|
||||
...this.config,
|
||||
bridge: `${ this.config.bridge }-br`,
|
||||
});
|
||||
},
|
||||
}
|
||||
};
|
||||
@@ -263,7 +190,6 @@ export default {
|
||||
:errors="errors"
|
||||
:apply-hooks="applyHooks"
|
||||
@finish="saveNetwork"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<NameNsDescription
|
||||
ref="nd"
|
||||
@@ -285,15 +211,14 @@ export default {
|
||||
<LabeledSelect
|
||||
v-model:value="type"
|
||||
class="mb-20"
|
||||
:options="networkTypes"
|
||||
:options="networkType"
|
||||
:mode="mode"
|
||||
:disabled="isEdit"
|
||||
:label="t('harvester.fields.type')"
|
||||
required
|
||||
/>
|
||||
|
||||
<LabeledInput
|
||||
v-if="isL2VlanNetwork"
|
||||
v-if="!isUntaggedNetwork"
|
||||
v-model:value.number="config.vlan"
|
||||
class="mb-20"
|
||||
required
|
||||
@@ -303,20 +228,25 @@ export default {
|
||||
:mode="mode"
|
||||
@update:value="input"
|
||||
/>
|
||||
<LabeledSelect
|
||||
v-if="!isOverlayNetwork"
|
||||
v-model:value="clusterBridge"
|
||||
class="mb-20"
|
||||
:label="t('harvester.network.clusterNetwork.label')"
|
||||
required
|
||||
:disabled="isEdit"
|
||||
:options="clusterNetworkOptions"
|
||||
:mode="mode"
|
||||
:placeholder="t('harvester.network.clusterNetwork.selectPlaceholder')"
|
||||
/>
|
||||
|
||||
<div class="row">
|
||||
<div
|
||||
class="col span-12"
|
||||
>
|
||||
<LabeledSelect
|
||||
v-model:value="config.bridge"
|
||||
class="mb-20"
|
||||
:label="t('harvester.network.clusterNetwork.label')"
|
||||
required
|
||||
:options="clusterNetworkOptions"
|
||||
:mode="mode"
|
||||
:placeholder="t('harvester.network.clusterNetwork.selectPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab
|
||||
v-if="isL2VlanNetwork"
|
||||
v-if="!isUntaggedNetwork"
|
||||
name="layer3Network"
|
||||
:label="t('harvester.network.tabs.layer3Network')"
|
||||
:weight="98"
|
||||
|
||||
@@ -198,7 +198,7 @@ export default {
|
||||
:apply-hooks="applyHooks"
|
||||
:validation-passed="canSave"
|
||||
@finish="save"
|
||||
@error="e=>errors=e"
|
||||
@error="e=>errors = e"
|
||||
>
|
||||
<div class="banner">
|
||||
<Banner
|
||||
|
||||
@@ -84,10 +84,6 @@ export default {
|
||||
this.value['_type'] = TYPES.OPAQUE;
|
||||
}
|
||||
|
||||
if ( this.realMode === _CLONE) {
|
||||
this.value['_type'] = this.liveValue._type;
|
||||
}
|
||||
|
||||
return {
|
||||
isCloud,
|
||||
nodeDrivers: null,
|
||||
@@ -238,7 +234,7 @@ export default {
|
||||
:resource="value"
|
||||
:errors="errors"
|
||||
@finish="saveSecret"
|
||||
@error="e=>errors=e"
|
||||
@error="e=>errors = e"
|
||||
>
|
||||
<NameNsDescription
|
||||
:value="value"
|
||||
|
||||
@@ -4,10 +4,8 @@ import { RadioGroup } from '@components/Form/Radio';
|
||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||
import { TextAreaAutoGrow } from '@components/Form/TextArea';
|
||||
import UnitInput from '@shell/components/form/UnitInput';
|
||||
|
||||
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';
|
||||
|
||||
@@ -17,8 +15,7 @@ export default {
|
||||
LabeledInput,
|
||||
LabeledSelect,
|
||||
RadioGroup,
|
||||
TextAreaAutoGrow,
|
||||
UnitInput
|
||||
TextAreaAutoGrow
|
||||
},
|
||||
|
||||
mixins: [CreateEditView],
|
||||
@@ -60,13 +57,7 @@ export default {
|
||||
|
||||
return {
|
||||
setting,
|
||||
description: isHarvester ? t(
|
||||
`advancedSettings.descriptions.harv-${ this.value.id }`,
|
||||
this.getDocLinkParams()
|
||||
) : t(
|
||||
`advancedSettings.descriptions.${ this.value.id }`,
|
||||
this.getDocLinkParams()
|
||||
),
|
||||
description: isHarvester ? t(`advancedSettings.descriptions.harv-${ this.value.id }`) : t(`advancedSettings.descriptions.${ this.value.id }`),
|
||||
editHelp: t(`advancedSettings.editHelp.${ this.value.id }`),
|
||||
enumOptions,
|
||||
canReset,
|
||||
@@ -137,13 +128,6 @@ export default {
|
||||
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);
|
||||
},
|
||||
|
||||
@@ -179,18 +163,6 @@ export default {
|
||||
if (typeof this.$refs.settingComp?.useDefault === 'function') {
|
||||
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="[]"
|
||||
:can-yaml="false"
|
||||
:cancel-event="true"
|
||||
@error="e=>errors=e"
|
||||
@error="e => (errors = e)"
|
||||
@finish="saveSettings"
|
||||
@cancel="done"
|
||||
>
|
||||
@@ -266,13 +238,6 @@ export default {
|
||||
:min-height="254"
|
||||
/>
|
||||
</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>
|
||||
<LabeledInput
|
||||
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>
|
||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||
import CruResource from '@shell/components/CruResource';
|
||||
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 Tabbed from '@shell/components/Tabbed';
|
||||
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 Loading from '@shell/components/Loading';
|
||||
|
||||
import { _CREATE, _VIEW } from '@shell/config/query-params';
|
||||
import { mapFeature, UNSUPPORTED_STORAGE_DRIVERS } from '@shell/store/features';
|
||||
import {
|
||||
@@ -21,7 +22,6 @@ import { CSI_DRIVER } from '../../types';
|
||||
import Tags from '../../components/DiskTags';
|
||||
import { DATA_ENGINE_V1, DATA_ENGINE_V2 } from '../../models/harvester/persistentvolumeclaim';
|
||||
import { LVM_DRIVER } from '../../models/harvester/storage.k8s.io.storageclass';
|
||||
import CDISettings from './CDISettings';
|
||||
|
||||
export const LVM_TOPOLOGY_LABEL = 'topology.lvm.csi/node';
|
||||
|
||||
@@ -45,7 +45,6 @@ export default {
|
||||
Tabbed,
|
||||
Loading,
|
||||
Tags,
|
||||
CDISettings,
|
||||
},
|
||||
|
||||
mixins: [CreateEditView],
|
||||
@@ -118,7 +117,6 @@ export default {
|
||||
key: '',
|
||||
values: [],
|
||||
},
|
||||
cdiSettings: {},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -126,10 +124,10 @@ export default {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
const hash = {
|
||||
namespaces: this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE }),
|
||||
storages: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }),
|
||||
longhornNodes: this.$store.dispatch(`${ inStore }/findAll`, { type: LONGHORN.NODES }),
|
||||
csiDrivers: this.$store.dispatch(`${ inStore }/findAll`, { type: CSI_DRIVER }),
|
||||
namespaces: this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE }),
|
||||
storages: this.$store.dispatch(`${ inStore }/findAll`, { type: STORAGE_CLASS }),
|
||||
longhornNodes: this.$store.dispatch(`${ inStore }/findAll`, { type: LONGHORN.NODES }),
|
||||
csiDrivers: this.$store.dispatch(`${ inStore }/findAll`, { type: CSI_DRIVER })
|
||||
};
|
||||
|
||||
if (this.value.longhornV2LVMSupport) {
|
||||
@@ -199,14 +197,6 @@ export default {
|
||||
|
||||
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: {
|
||||
@@ -259,10 +249,6 @@ export default {
|
||||
});
|
||||
|
||||
this.formatAllowedTopoloties();
|
||||
|
||||
if (this.shouldShowCDISettingsTab) {
|
||||
this.formatCDISettings();
|
||||
}
|
||||
},
|
||||
|
||||
formatAllowedTopoloties() {
|
||||
@@ -284,34 +270,6 @@ export default {
|
||||
if (matchLabelExpressions.length > 0) {
|
||||
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);
|
||||
}
|
||||
|
||||
if (this.cdiSettings.volumeSnapshotClass) {
|
||||
annotations[HCI_ANNOTATIONS.VOLUME_SNAPSHOT_CLASS] = this.cdiSettings.volumeSnapshotClass;
|
||||
}
|
||||
|
||||
if (this.cdiSettings.cloneStrategy) {
|
||||
annotations[HCI_ANNOTATIONS.CLONE_STRATEGY] = this.cdiSettings.cloneStrategy;
|
||||
}
|
||||
|
||||
if (this.cdiSettings.filesystemOverhead) {
|
||||
annotations[HCI_ANNOTATIONS.FILESYSTEM_OVERHEAD] = this.cdiSettings.filesystemOverhead;
|
||||
}
|
||||
|
||||
this.value.metadata.annotations = annotations;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -328,7 +286,7 @@ export default {
|
||||
:validation-passed="true"
|
||||
:apply-hooks="applyHooks"
|
||||
:errors="errors"
|
||||
@error="e=>errors=e"
|
||||
@error="e=>errors = e"
|
||||
@finish="save"
|
||||
@cancel="done"
|
||||
>
|
||||
@@ -446,19 +404,6 @@ export default {
|
||||
</template>
|
||||
</ArrayList>
|
||||
</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>
|
||||
</CruResource>
|
||||
</template>
|
||||
|
||||
@@ -17,7 +17,6 @@ import { VM_IMAGE_FILE_FORMAT } from '../validators/vm-image';
|
||||
import { OS } from '../mixins/harvester-vm';
|
||||
import { HCI } from '../types';
|
||||
import { LVM_DRIVER } from '../models/harvester/storage.k8s.io.storageclass';
|
||||
import { isInternalStorageClass } from '../utils/storage-class';
|
||||
|
||||
const ENCRYPT = 'encrypt';
|
||||
const DECRYPT = 'decrypt';
|
||||
@@ -111,7 +110,6 @@ export default {
|
||||
headers: {},
|
||||
fileUrl: '',
|
||||
file: '',
|
||||
HCI_ANNOTATIONS,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -169,18 +167,11 @@ export default {
|
||||
|
||||
return filteredStorages
|
||||
.map((s) => {
|
||||
let label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||
let disabled = false;
|
||||
|
||||
if (isInternalStorageClass(s.name)) {
|
||||
label += ` (${ this.t('harvester.storage.internal.label') })`;
|
||||
disabled = true;
|
||||
}
|
||||
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||
|
||||
return {
|
||||
label,
|
||||
value: s.name,
|
||||
disabled
|
||||
};
|
||||
}) || [];
|
||||
},
|
||||
@@ -604,8 +595,6 @@ export default {
|
||||
:pad-left="false"
|
||||
:read-allowed="false"
|
||||
:value-can-be-empty="true"
|
||||
:toggle-filter="true"
|
||||
:protected-keys="[HCI_ANNOTATIONS.IMAGE_DISPLAY_NAME]"
|
||||
@focusKey="focusKey"
|
||||
@update:value="value.setLabels($event)"
|
||||
>
|
||||
@@ -622,7 +611,7 @@ export default {
|
||||
<input
|
||||
v-else
|
||||
v-model="row[valueName]"
|
||||
:disabled="isView || row[keyName] === HCI_ANNOTATIONS.IMAGE_DISPLAY_NAME"
|
||||
:disabled="isView"
|
||||
:type="'text'"
|
||||
:placeholder="t('keyValue.valuePlaceholder')"
|
||||
autocorrect="off"
|
||||
|
||||
@@ -189,16 +189,6 @@ export default {
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
await this.saveSecret(res);
|
||||
@@ -229,7 +219,6 @@ export default {
|
||||
:errors="errors"
|
||||
:apply-hooks="applyHooks"
|
||||
@finish="saveVMT"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<NameNsDescription
|
||||
:value="templateValue"
|
||||
@@ -259,9 +248,6 @@ export default {
|
||||
<CpuMemory
|
||||
:cpu="cpu"
|
||||
:memory="memory"
|
||||
:max-cpu="maxCpu"
|
||||
:max-memory="maxMemory"
|
||||
:enable-hot-plug="cpuMemoryHotplugEnabled"
|
||||
:disabled="isConfig"
|
||||
@updateCpuMemory="updateCpuMemory"
|
||||
/>
|
||||
@@ -372,26 +358,6 @@ export default {
|
||||
@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
|
||||
name="advanced"
|
||||
:label="t('harvester.tab.advanced')"
|
||||
|
||||
@@ -23,7 +23,6 @@ import { LVM_DRIVER } from '../models/harvester/storage.k8s.io.storageclass';
|
||||
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 {
|
||||
name: 'HarvesterVolume',
|
||||
@@ -207,18 +206,11 @@ export default {
|
||||
|
||||
storageClassOptions() {
|
||||
return this.storageClasses.filter((s) => !s.parameters?.backingImage).map((s) => {
|
||||
let label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||
let disabled = false;
|
||||
|
||||
if (isInternalStorageClass(s.name)) {
|
||||
label += ` (${ this.t('harvester.storage.internal.label') })`;
|
||||
disabled = true;
|
||||
}
|
||||
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||
|
||||
return {
|
||||
label,
|
||||
value: s.name,
|
||||
disabled
|
||||
};
|
||||
}) || [];
|
||||
},
|
||||
@@ -312,10 +304,6 @@ export default {
|
||||
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 storageClass = this.storageClasses.find((sc) => sc.name === storageClassName);
|
||||
|
||||
@@ -386,12 +374,10 @@ export default {
|
||||
:generate-yaml="generateYaml"
|
||||
:apply-hooks="applyHooks"
|
||||
@finish="save"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<NameNsDescription
|
||||
:value="value"
|
||||
:namespaced="true"
|
||||
:name-required="false"
|
||||
:mode="mode"
|
||||
@update:value="$emit('update:value', $event)"
|
||||
/>
|
||||
|
||||
@@ -64,7 +64,6 @@ export default {
|
||||
:errors="errors"
|
||||
:apply-hooks="applyHooks"
|
||||
@finish="save"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<NameNsDescription
|
||||
: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,
|
||||
default: 'create'
|
||||
},
|
||||
osType: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
@@ -68,12 +64,6 @@ export default {
|
||||
},
|
||||
|
||||
watch: {
|
||||
osType(neu) {
|
||||
if (neu === 'windows') {
|
||||
this.id = '';
|
||||
}
|
||||
},
|
||||
|
||||
value(neu) {
|
||||
this.yamlScript = neu;
|
||||
},
|
||||
|
||||
@@ -28,10 +28,6 @@ export default {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
osType: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
userScript: {
|
||||
type: String,
|
||||
default: ''
|
||||
@@ -207,7 +203,6 @@ export default {
|
||||
<DataTemplate
|
||||
ref="userTemplate"
|
||||
:value="userScript"
|
||||
:os-type="osType"
|
||||
type="user"
|
||||
:view-code="viewCode"
|
||||
:mode="mode"
|
||||
@@ -224,7 +219,6 @@ export default {
|
||||
ref="networkTemplate"
|
||||
:value="networkScript"
|
||||
type="network"
|
||||
:os-type="osType"
|
||||
:view-code="viewCode"
|
||||
:mode="mode"
|
||||
:config-id="configNetworkId"
|
||||
|
||||
@@ -2,44 +2,23 @@
|
||||
import UnitInput from '@shell/components/form/UnitInput';
|
||||
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 {
|
||||
name: 'HarvesterEditCpuMemory',
|
||||
|
||||
emits: ['updateCpuMemory'],
|
||||
|
||||
components: {
|
||||
UnitInput, InputOrDisplay, Checkbox
|
||||
},
|
||||
components: { UnitInput, InputOrDisplay },
|
||||
|
||||
props: {
|
||||
cpu: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
maxCpu: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
memory: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
maxMemory: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
enableHotPlug: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'create',
|
||||
@@ -50,59 +29,21 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
|
||||
const res = await allHash({ settings: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SETTING }) });
|
||||
|
||||
this.settings = res.settings || [];
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
GIBIBYTE,
|
||||
localCpu: this.cpu,
|
||||
localMemory: this.memory,
|
||||
maxLocalCpu: this.maxCpu,
|
||||
maxLocalMemory: this.maxMemory,
|
||||
localEnableHotPlug: this.enableHotPlug,
|
||||
settings: []
|
||||
localCpu: this.cpu,
|
||||
localMemory: this.memory
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
isView() {
|
||||
return this.mode === _VIEW;
|
||||
},
|
||||
cpuDisplay() {
|
||||
cupDisplay() {
|
||||
return `${ this.localCpu } C`;
|
||||
},
|
||||
|
||||
maxCpuDisplay() {
|
||||
return `${ this.maxLocalCpu } C`;
|
||||
},
|
||||
|
||||
memoryDisplay() {
|
||||
return `${ this.localMemory }`;
|
||||
},
|
||||
|
||||
maxMemoryDisplay() {
|
||||
return `${ this.maxLocalMemory }`;
|
||||
},
|
||||
|
||||
cpuMemoryHotplugTooltip() {
|
||||
return this.t('harvester.virtualMachine.hotplug.tooltip', { hotPlugTimes: this.maxHotplugRatio });
|
||||
},
|
||||
|
||||
isCPUMemoryHotPlugFeatureEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('cpuMemoryHotplug');
|
||||
},
|
||||
|
||||
maxHotplugRatio() {
|
||||
const maxHotPlugRatioSetting = this.settings.find((s) => s.id === HCI_SETTING.MAX_HOTPLUG_RATIO);
|
||||
const maxPlugRatio = maxHotPlugRatioSetting?.value ? parseInt(maxHotPlugRatioSetting.value, 10) : DEFAULT_HOT_PLUG_TIMES;
|
||||
|
||||
return maxPlugRatio;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -114,161 +55,71 @@ export default {
|
||||
if (neu && !neu.includes('null')) {
|
||||
this.localMemory = neu;
|
||||
}
|
||||
},
|
||||
maxCpu(neu) {
|
||||
this.maxLocalCpu = neu;
|
||||
},
|
||||
maxMemory(neu) {
|
||||
if (neu && !neu.includes('null')) {
|
||||
this.maxLocalMemory = neu;
|
||||
}
|
||||
},
|
||||
enableHotPlug(neu) {
|
||||
this.localEnableHotPlug = neu;
|
||||
},
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
hotPlugChanged(neu) {
|
||||
// If hot plug is enabled, we need to update the maxCpu and maxMemory values
|
||||
if (neu) {
|
||||
this.maxLocalCpu = this.localCpu ? this.localCpu * this.maxHotplugRatio : null;
|
||||
this.maxLocalMemory = this.localMemory ? `${ parseInt(this.localMemory, 10) * this.maxHotplugRatio }${ GIBIBYTE }` : null;
|
||||
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, this.maxLocalCpu, this.maxLocalMemory, neu);
|
||||
change() {
|
||||
let memory = '';
|
||||
|
||||
if (String(this.localMemory).includes('Gi')) {
|
||||
memory = this.localMemory;
|
||||
} else {
|
||||
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, '', null, neu);
|
||||
memory = `${ this.localMemory }${ GIBIBYTE }`;
|
||||
}
|
||||
},
|
||||
changeMemory() {
|
||||
if (this.localEnableHotPlug) {
|
||||
this.maxLocalMemory = this.localMemory ? `${ parseInt(this.localMemory, 10) * this.maxHotplugRatio }${ GIBIBYTE }` : null;
|
||||
if (memory.includes('null')) {
|
||||
memory = null;
|
||||
}
|
||||
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, this.maxLocalCpu, this.maxLocalMemory, this.localEnableHotPlug);
|
||||
},
|
||||
changeCPU() {
|
||||
if (this.localEnableHotPlug) {
|
||||
this.maxLocalCpu = this.localCpu ? this.localCpu * this.maxHotplugRatio : null;
|
||||
}
|
||||
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, this.maxLocalCpu, this.maxLocalMemory, this.localEnableHotPlug);
|
||||
},
|
||||
changeMaxCPUMemory() {
|
||||
this.$emit('updateCpuMemory', this.localCpu, this.localMemory, this.maxLocalCpu, this.maxLocalMemory, this.localEnableHotPlug);
|
||||
this.$emit('updateCpuMemory', this.localCpu, memory);
|
||||
},
|
||||
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="col span-6 mb-10">
|
||||
<InputOrDisplay
|
||||
name="CPU"
|
||||
:value="cpuDisplay"
|
||||
<div class="row">
|
||||
<div class="col span-6 mb-10">
|
||||
<InputOrDisplay
|
||||
name="CPU"
|
||||
:value="cupDisplay"
|
||||
:mode="mode"
|
||||
>
|
||||
<UnitInput
|
||||
v-model:value="localCpu"
|
||||
label="CPU"
|
||||
suffix="C"
|
||||
:delay="0"
|
||||
required
|
||||
:disabled="disabled"
|
||||
:mode="mode"
|
||||
class="mb-10"
|
||||
>
|
||||
<UnitInput
|
||||
v-model:value="localCpu"
|
||||
label="CPU"
|
||||
suffix="C"
|
||||
:delay="0"
|
||||
required
|
||||
:disabled="disabled"
|
||||
:mode="mode"
|
||||
class="mb-20"
|
||||
@update:value="changeCPU"
|
||||
/>
|
||||
</InputOrDisplay>
|
||||
</div>
|
||||
<div class="col span-6 mb-10">
|
||||
<InputOrDisplay
|
||||
:name="t('harvester.virtualMachine.input.memory')"
|
||||
:value="memoryDisplay"
|
||||
:mode="mode"
|
||||
>
|
||||
<UnitInput
|
||||
v-model:value="localMemory"
|
||||
:label="t('harvester.virtualMachine.input.memory')"
|
||||
:mode="mode"
|
||||
:input-exponent="3"
|
||||
:delay="0"
|
||||
:increment="1024"
|
||||
:output-modifier="true"
|
||||
:disabled="disabled"
|
||||
required
|
||||
:suffix="GIBIBYTE"
|
||||
class="mb-20"
|
||||
@update:value="changeMemory"
|
||||
/>
|
||||
</InputOrDisplay>
|
||||
</div>
|
||||
class="mb-20"
|
||||
@update:value="change"
|
||||
/>
|
||||
</InputOrDisplay>
|
||||
</div>
|
||||
<div
|
||||
v-if="isCPUMemoryHotPlugFeatureEnabled"
|
||||
class="row"
|
||||
>
|
||||
<Checkbox
|
||||
v-model:value="localEnableHotPlug"
|
||||
class="check"
|
||||
type="checkbox"
|
||||
:label="t('harvester.virtualMachine.hotplug.title')"
|
||||
:disabled="isView"
|
||||
@update:value="hotPlugChanged"
|
||||
/>
|
||||
<i
|
||||
v-clean-tooltip="{content: cpuMemoryHotplugTooltip, triggers: ['hover', 'touch', 'focus'] }"
|
||||
v-stripped-aria-label="cpuMemoryHotplugTooltip"
|
||||
class="icon icon-info"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="localEnableHotPlug && isCPUMemoryHotPlugFeatureEnabled"
|
||||
class="row"
|
||||
>
|
||||
<div class="col span-6 mb-10">
|
||||
<InputOrDisplay
|
||||
:name="t('harvester.virtualMachine.input.maxCpu')"
|
||||
:value="maxCpuDisplay"
|
||||
|
||||
<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"
|
||||
class="mt-20"
|
||||
>
|
||||
<UnitInput
|
||||
v-model:value="maxLocalCpu"
|
||||
:label="t('harvester.virtualMachine.input.maxCpu')"
|
||||
suffix="C"
|
||||
:delay="0"
|
||||
:disabled="disabled"
|
||||
:mode="mode"
|
||||
class="mt-20"
|
||||
@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>
|
||||
:input-exponent="3"
|
||||
:delay="0"
|
||||
:increment="1024"
|
||||
:output-modifier="true"
|
||||
:disabled="disabled"
|
||||
required
|
||||
:suffix="GIBIBYTE"
|
||||
class="mb-20"
|
||||
@update:value="change"
|
||||
/>
|
||||
</InputOrDisplay>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -240,7 +240,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mt-20">
|
||||
<div>
|
||||
<LabeledSelect
|
||||
v-model:value="checkedSsh"
|
||||
:label="t('harvester.virtualMachine.input.sshKey')"
|
||||
|
||||
@@ -17,7 +17,6 @@ import { SOURCE_TYPE } from '../../../config/harvester-map';
|
||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../../config/harvester';
|
||||
import { HCI } from '../../../types';
|
||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
||||
import { OFF } from '../../../models/kubevirt.io.virtualmachine';
|
||||
|
||||
export default {
|
||||
emits: ['update:value'],
|
||||
@@ -267,17 +266,7 @@ export default {
|
||||
|
||||
isLonghornV2(volume) {
|
||||
return volume?.pvc?.isLonghornV2 || volume?.pvc?.storageClass?.isLonghornV2;
|
||||
},
|
||||
|
||||
isResizeDisabled(volume) {
|
||||
if (this.isCreate) return false;
|
||||
if (volume.newCreateId) return false;
|
||||
|
||||
const isStopped = this.vm.stateDisplay === OFF;
|
||||
const isLonghornV2 = this.isLonghornV2(volume);
|
||||
|
||||
return !isStopped || isLonghornV2;
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -358,7 +347,6 @@ export default {
|
||||
:is-edit="isEdit"
|
||||
:is-view="isView"
|
||||
:is-virtual-type="isVirtualType"
|
||||
:is-resize-disabled="isResizeDisabled(rows[i])"
|
||||
:mode="mode"
|
||||
:idx="i"
|
||||
:validate-required="validateRequired"
|
||||
|
||||
+12
-26
@@ -8,7 +8,6 @@ import { Banner } from '@components/Banner';
|
||||
import { PVC } from '@shell/config/types';
|
||||
import { formatSi, parseSi } from '@shell/utils/units';
|
||||
import { HCI } from '../../../../types';
|
||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
import { VOLUME_TYPE, InterfaceOption } from '../../../../config/harvester-map';
|
||||
import { _VIEW } from '@shell/config/query-params';
|
||||
import LabelValue from '@shell/components/LabelValue';
|
||||
@@ -64,19 +63,13 @@ export default {
|
||||
isVirtualType: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
|
||||
isResizeDisabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
GIBIBYTE,
|
||||
VOLUME_TYPE,
|
||||
HCI_ANNOTATIONS,
|
||||
InterfaceOption,
|
||||
loading: false,
|
||||
images: [],
|
||||
@@ -97,23 +90,12 @@ export default {
|
||||
},
|
||||
|
||||
imagesOption() {
|
||||
return this.images
|
||||
.filter((image) => {
|
||||
if (!image.isReady) return false;
|
||||
|
||||
// exclude internal images created during upgrade
|
||||
const isInternalCreatedImage =
|
||||
image.namespace === 'harvester-system' &&
|
||||
image.labels?.[HCI_ANNOTATIONS.UPGRADE];
|
||||
|
||||
return !isInternalCreatedImage;
|
||||
})
|
||||
.sort((a, b) => (a.creationTimestamp > b.creationTimestamp ? -1 : 1))
|
||||
.map((image) => ({
|
||||
label: this.imageOptionLabel(image),
|
||||
value: image.id,
|
||||
disabled: image.isImportedImage
|
||||
}));
|
||||
return this.images.filter((c) => c.isReady).sort((a, b) => a.creationTimestamp > b.creationTimestamp ? -1 : 1).map( (I) => {
|
||||
return {
|
||||
label: this.imageOptionLabel(I),
|
||||
value: I.id
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
imageName() {
|
||||
@@ -140,6 +122,10 @@ export default {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
|
||||
},
|
||||
|
||||
isLonghornV2() {
|
||||
return this.value.pvc?.isLonghornV2 || this.value.pvc?.storageClass?.isLonghornV2;
|
||||
},
|
||||
|
||||
selectedImage() {
|
||||
return this.$store.getters['harvester/all'](HCI.IMAGE)?.find( (I) => this.value.image === I.id);
|
||||
},
|
||||
@@ -351,7 +337,7 @@ export default {
|
||||
:label="t('harvester.fields.size')"
|
||||
:mode="mode"
|
||||
:required="validateRequired"
|
||||
:disabled="isResizeDisabled"
|
||||
:disable="isLonghornV2"
|
||||
:suffix="GIBIBYTE"
|
||||
@update:value="update"
|
||||
/>
|
||||
|
||||
@@ -13,7 +13,6 @@ import { ucFirst } from '@shell/utils/string';
|
||||
import { LVM_DRIVER } from '../../../../models/harvester/storage.k8s.io.storageclass';
|
||||
import { DATA_ENGINE_V2 } from '../../../../models/harvester/persistentvolumeclaim';
|
||||
import { GIBIBYTE } from '../../../../utils/unit';
|
||||
import { isInternalStorageClass } from '../../../../utils/storage-class';
|
||||
import { VOLUME_MODE } from '@pkg/harvester/config/types';
|
||||
|
||||
export default {
|
||||
@@ -56,12 +55,7 @@ export default {
|
||||
isVirtualType: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
|
||||
isResizeDisabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
@@ -124,22 +118,19 @@ export default {
|
||||
|
||||
storageClassOptions() {
|
||||
return this.storageClasses.filter((s) => !s.parameters?.backingImage).map((s) => {
|
||||
let label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||
let disabled = false;
|
||||
|
||||
if (isInternalStorageClass(s.name)) {
|
||||
label += ` (${ this.t('harvester.storage.internal.label') })`;
|
||||
disabled = true;
|
||||
}
|
||||
const label = s.isDefault ? `${ s.name } (${ this.t('generic.default') })` : s.name;
|
||||
|
||||
return {
|
||||
label,
|
||||
value: s.name,
|
||||
disabled
|
||||
};
|
||||
}) || [];
|
||||
},
|
||||
|
||||
isLonghornV2() {
|
||||
return this.value.pvc?.isLonghornV2 || this.value.pvc?.storageClass?.isLonghornV2;
|
||||
},
|
||||
|
||||
isLonghornStorageClass() {
|
||||
const selectedSC = this.storageClasses.find((sc) => sc.name === this.value?.storageClassName) || {};
|
||||
|
||||
@@ -311,7 +302,7 @@ export default {
|
||||
:mode="mode"
|
||||
:required="validateRequired"
|
||||
:label="t('harvester.fields.size')"
|
||||
:disabled="isResizeDisabled"
|
||||
:disabled="isLonghornV2"
|
||||
:suffix="GIBIBYTE"
|
||||
@update:value="update"
|
||||
/>
|
||||
|
||||
@@ -114,11 +114,13 @@ export default {
|
||||
},
|
||||
|
||||
machineTypeOptions() {
|
||||
return this.machineTypes.map((type) => {
|
||||
if (!type) return { label: 'None', value: '' };
|
||||
|
||||
return { label: type, value: type };
|
||||
});
|
||||
return [{
|
||||
label: 'None',
|
||||
value: ''
|
||||
}, {
|
||||
label: 'q35',
|
||||
value: 'q35'
|
||||
}];
|
||||
},
|
||||
|
||||
templateOptions() {
|
||||
@@ -237,9 +239,6 @@ export default {
|
||||
}
|
||||
const versions = await this.$store.dispatch('harvester/findAll', { type: HCI.VM_VERSION });
|
||||
const curVersion = versions.find( (V) => V.id === id);
|
||||
|
||||
if (!curVersion?.spec?.vm) return;
|
||||
|
||||
const cloneVersionVM = clone(curVersion.spec.vm);
|
||||
|
||||
delete cloneVersionVM.spec?.template?.spec?.accessCredentials;
|
||||
@@ -247,6 +246,7 @@ export default {
|
||||
delete cloneVersionVM.spec?.template?.metadata?.annotations?.[HCI_ANNOTATIONS.DYNAMIC_SSHKEYS_USERS];
|
||||
|
||||
const claimTemplate = parseVolumeClaimTemplates(cloneVersionVM);
|
||||
|
||||
const deleteDataSource = claimTemplate.map((volume) => {
|
||||
if (volume?.spec?.dataSource) {
|
||||
delete volume.spec.dataSource;
|
||||
@@ -257,17 +257,6 @@ export default {
|
||||
|
||||
cloneVersionVM.metadata.annotations[HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE] = JSON.stringify(deleteDataSource);
|
||||
|
||||
// Update instance labels, labels and annotations
|
||||
this.value.spec.template.metadata.labels = cloneVersionVM.spec.template.metadata.labels || {};
|
||||
this.value.metadata.labels = {
|
||||
...(curVersion.metadata?.labels || {}),
|
||||
...(cloneVersionVM.metadata?.labels || {})
|
||||
};
|
||||
this.value.metadata.annotations = {
|
||||
...(curVersion.metadata?.annotations || {}),
|
||||
...(cloneVersionVM.metadata?.annotations || {})
|
||||
};
|
||||
|
||||
this.getInitConfig({
|
||||
value: cloneVersionVM, existUserData: true, fromTemplate: true
|
||||
});
|
||||
@@ -466,9 +455,7 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
const cpuMemoryErrors = this.getCPUMemoryValidation();
|
||||
const accessCredentialsErrors = this.getAccessCredentialsValidation();
|
||||
const errors = [...cpuMemoryErrors, ...accessCredentialsErrors];
|
||||
const errors = this.getAccessCredentialsValidation();
|
||||
|
||||
if (errors.length > 0) {
|
||||
return Promise.reject(errors);
|
||||
@@ -532,7 +519,6 @@ export default {
|
||||
:apply-hooks="applyHooks"
|
||||
@finish="saveVM"
|
||||
@cancel="cancelAction"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<RadioGroup
|
||||
v-if="isCreate"
|
||||
@@ -606,11 +592,8 @@ export default {
|
||||
>
|
||||
<CpuMemory
|
||||
:cpu="cpu"
|
||||
:max-cpu="maxCpu"
|
||||
:memory="memory"
|
||||
:max-memory="maxMemory"
|
||||
:mode="mode"
|
||||
:enable-hot-plug="cpuMemoryHotplugEnabled"
|
||||
@updateCpuMemory="updateCpuMemory"
|
||||
/>
|
||||
|
||||
@@ -778,31 +761,10 @@ export default {
|
||||
/>
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
name="annotations"
|
||||
:label="t('harvester.tab.annotations')"
|
||||
:weight="-11"
|
||||
>
|
||||
<Banner color="info">
|
||||
<t k="harvester.virtualMachine.annotations.banner" />
|
||||
</Banner>
|
||||
<KeyValue
|
||||
key="annotations"
|
||||
:value="value.annotations"
|
||||
:protected-keys="value.systemAnnotations || []"
|
||||
:toggle-filter="toggler"
|
||||
:add-label="t('labels.addAnnotation')"
|
||||
:mode="mode"
|
||||
:read-allowed="false"
|
||||
:value-can-be-empty="true"
|
||||
@update:value="value.setAnnotations($event)"
|
||||
/>
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
name="advanced"
|
||||
:label="t('harvester.tab.advanced')"
|
||||
:weight="-12"
|
||||
:weight="-11"
|
||||
>
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
@@ -900,7 +862,6 @@ export default {
|
||||
ref="yamlEditor"
|
||||
:user-script="userScript"
|
||||
:mode="mode"
|
||||
:os-type="osType"
|
||||
:view-code="isWindows"
|
||||
:namespace="value.metadata.namespace"
|
||||
:network-script="networkScript"
|
||||
|
||||
@@ -81,7 +81,6 @@ export default {
|
||||
:apply-hooks="applyHooks"
|
||||
:yaml-modifiers="yamlModifiers"
|
||||
@finish="save"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<NameNsDescription
|
||||
:value="value"
|
||||
|
||||
@@ -235,7 +235,6 @@ export default {
|
||||
:apply-hooks="applyHooks"
|
||||
:yaml-modifiers="yamlModifiers"
|
||||
@finish="save"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<NameNsDescription
|
||||
:value="value"
|
||||
|
||||
@@ -213,7 +213,7 @@ export default {
|
||||
:subtypes="[]"
|
||||
:can-yaml="false"
|
||||
:validation-passed="fvFormIsValid"
|
||||
@error="e=>errors=e"
|
||||
@error="e=>errors = e"
|
||||
@finish="save"
|
||||
@cancel="done"
|
||||
>
|
||||
|
||||
@@ -67,7 +67,6 @@ export default {
|
||||
:mode="mode"
|
||||
:errors="errors"
|
||||
@finish="save"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<NameNsDescription
|
||||
:value="value"
|
||||
|
||||
@@ -373,7 +373,6 @@ export default {
|
||||
:mode="mode"
|
||||
:errors="errors"
|
||||
@finish="save"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<NameNsDescription
|
||||
:value="value"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script>
|
||||
import { NORMAN } from '@shell/config/types';
|
||||
import { HCI } from '../types';
|
||||
|
||||
export default {
|
||||
@@ -14,24 +13,10 @@ export default {
|
||||
const harvesterSettings = await this.$store.dispatch('harvester/findAll', { type: HCI.SETTING });
|
||||
|
||||
this.harvesterSettings = harvesterSettings;
|
||||
|
||||
const clusterRoleTemplateBindingSchema = this.$store.getters['rancher/schemaFor'](NORMAN.CLUSTER_ROLE_TEMPLATE_BINDING);
|
||||
|
||||
if (clusterRoleTemplateBindingSchema) {
|
||||
const normanBindings = await this.$store.dispatch('rancher/findAll', { type: NORMAN.CLUSTER_ROLE_TEMPLATE_BINDING }, { root: true });
|
||||
|
||||
this.clusterRoleTemplateBinding = normanBindings;
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
const user = this.$store.getters['auth/v3User'];
|
||||
|
||||
return {
|
||||
harvesterSettings: [],
|
||||
clusterRoleTemplateBinding: [],
|
||||
user,
|
||||
};
|
||||
return { harvesterSettings: [] };
|
||||
},
|
||||
|
||||
computed: {
|
||||
@@ -46,28 +31,19 @@ export default {
|
||||
} catch (e) {}
|
||||
|
||||
return isMatch;
|
||||
},
|
||||
|
||||
isClusterOwner() {
|
||||
const template = this.clusterRoleTemplateBinding.find((template) => {
|
||||
return template.userId === this.user?.id;
|
||||
});
|
||||
|
||||
return template?.roleTemplateId === 'cluster-owner';
|
||||
},
|
||||
|
||||
shouldShowError() {
|
||||
return this.isClusterOwner && !this.isMatch && this.value;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="isMatch">
|
||||
{{ value }}
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ value }}
|
||||
<p
|
||||
v-if="shouldShowError"
|
||||
v-if="value"
|
||||
class="text-error"
|
||||
>
|
||||
{{ t('harvester.backup.matchTarget') }}
|
||||
|
||||
@@ -35,26 +35,11 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
// Return VM instance IP and VM annotation IP
|
||||
ips() {
|
||||
if (this.vmiIp.length) {
|
||||
return [...this.vmiIp, ...this.networkAnnotationIP]
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => a.ip < b.ip ? -1 : 1);
|
||||
}
|
||||
|
||||
return this.customAnnotationIP;
|
||||
},
|
||||
|
||||
customAnnotationIP() {
|
||||
const annotationIp = get(this.row, `metadata.annotations."${ HCI_ANNOTATIONS.CUSTOM_IP }"`);
|
||||
|
||||
if (annotationIp && isIpv4(annotationIp)) {
|
||||
return [{
|
||||
name: 'custom-ip', ip: annotationIp, isCustom: true
|
||||
}];
|
||||
}
|
||||
|
||||
return [];
|
||||
return [...this.vmiIp, ...this.networkAnnotationIP]
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => a.ip < b.ip ? -1 : 1);
|
||||
},
|
||||
|
||||
networkAnnotationIP() {
|
||||
@@ -112,13 +97,12 @@ export default {
|
||||
<template>
|
||||
<div v-if="showIP">
|
||||
<span
|
||||
v-for="{ ip, name, isCustom } in ips"
|
||||
:key="`${ip}-${name}`"
|
||||
v-for="{ip, name} in ips"
|
||||
:key="ip"
|
||||
>
|
||||
<CopyToClipboardText
|
||||
v-clean-tooltip="isCustom ? t('harvester.formatters.harvesterIpAddress.customIpTooltip') : name"
|
||||
v-clean-tooltip="name"
|
||||
:text="ip"
|
||||
:plain="isCustom"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
+10
-163
@@ -49,13 +49,9 @@ dialog:
|
||||
}
|
||||
protip: "Tip: Hold the {alternateLabel} key while clicking action to bypass this confirmation"
|
||||
stop:
|
||||
message: "Are you sure you want to stop the {type} {names}?"
|
||||
message: "Are you sure you want to continue stop the {type} {names}?"
|
||||
pause:
|
||||
message: "Are you sure you want to pause the {type} {names}?"
|
||||
restart:
|
||||
message: "Are you sure you want to restart the {type} {names}?"
|
||||
softreboot:
|
||||
message: "Are you sure you want to soft reboot the {type} {names}?"
|
||||
message: "Are you sure you want to continue pause the {type} {names}?"
|
||||
promptRemove:
|
||||
title: "Delete {type}"
|
||||
warningMessage: "Deleting the selected {type} permanently removes all resources on {thisOrThese} {type}. This action is irreversible. Do you want to continue?"
|
||||
@@ -70,6 +66,7 @@ harvester:
|
||||
tip: 'Upload an icon to replace the Harvester favicon in the browser tab. Max file size is 20KB'
|
||||
productLabel: 'Harvester'
|
||||
modal:
|
||||
|
||||
backup:
|
||||
success: 'Backup { backUpName } has been initiated.'
|
||||
addBackup: Add Backup
|
||||
@@ -136,31 +133,13 @@ harvester:
|
||||
delete: Delete
|
||||
bundle:
|
||||
title: Generate a Support Bundle
|
||||
titleDescription: Collect system-related logs in Harvester to assist with troubleshooting and support.
|
||||
tip: Each field below specifies an aspect of the support bundle. For detailed explanations, please refer to the <a href="{doc}" target="_blank">documentation</a>.
|
||||
url: Issue URL
|
||||
description: Description
|
||||
namespaces:
|
||||
label: Namespaces
|
||||
tooltip: Select additional namespaces to include in the support bundle.<br/>See docs support-bundle-namespaces for detail.
|
||||
selectAll: 'Select All'
|
||||
unselectAll: 'Unselect All'
|
||||
timeout:
|
||||
label: Timeout
|
||||
tooltip: Minutes allows for completion of the support bundle generation process.<br/>See docs support-bundle-timeout for detail.
|
||||
expiration:
|
||||
label: Expiration
|
||||
tooltip: Minutes before deleting packaged but not downloaded support bundle.<br/>See docs support-bundle-expiration for detail.
|
||||
nodeTimeout:
|
||||
label: Node Collection Timeout
|
||||
tooltip: Minutes allowed for collecting logs/configurations on nodes.<br/>See docs support-bundle-node-collection-timeout for detail.
|
||||
requiredDesc: Description is required!
|
||||
titleDescription: Collect system-related logs in Harvester to help with troubleshooting and support.
|
||||
hotplug:
|
||||
success: 'Volume { diskName } is mounted to the virtual machine { vm }.'
|
||||
title: Add Volume
|
||||
cpuMemoryHotplug:
|
||||
success: 'CPU and Memory are updated to the virtual machine { vm }.'
|
||||
title: Edit CPU and Memory
|
||||
maxResourcesMessage: 'You can increase the CPU to maximum { maxCpu }C and memory to maximum { maxMemory }.'
|
||||
hotunplug:
|
||||
success: 'Volume { name } is detached successfully.'
|
||||
snapshot:
|
||||
@@ -217,7 +196,6 @@ harvester:
|
||||
suspendSchedule: Suspend
|
||||
restoreExistingVM: Replace Existing
|
||||
migrate: Migrate
|
||||
cpuAndMemoryHotplug: Edit CPU and Memory
|
||||
abortMigration: Abort Migration
|
||||
createTemplate: Generate Template
|
||||
enableMaintenance: Enable Maintenance Mode
|
||||
@@ -251,8 +229,6 @@ harvester:
|
||||
{count, plural,
|
||||
=1 {core}
|
||||
other {cores}}
|
||||
harvesterIpAddress:
|
||||
customIpTooltip: "Custom IP (set via annotation)"
|
||||
tableHeaders:
|
||||
imageEncryption: Encryption
|
||||
size: Size
|
||||
@@ -300,7 +276,6 @@ harvester:
|
||||
quotas: Quotas
|
||||
snapshots: Snapshots
|
||||
instanceLabel: Instance Labels
|
||||
annotations: Annotations
|
||||
fields:
|
||||
version: Version
|
||||
name: Name
|
||||
@@ -401,8 +376,6 @@ harvester:
|
||||
ruleFileTip: 'The file you have chosen ends in an extension that we do not support. We only accept image files that end in .img, .iso, .qcow, .qcow2, .raw.'
|
||||
hash:
|
||||
sha512: 'Invalid SHA512 checksum.'
|
||||
subnet:
|
||||
excludeIps: 'Exclude IPs cannot be empty. Please remove or fill in the exclude IPs.'
|
||||
|
||||
dashboard:
|
||||
label: Dashboard
|
||||
@@ -576,10 +549,6 @@ harvester:
|
||||
virtualMachine:
|
||||
label: Virtual Machines
|
||||
osType: OS Type
|
||||
hotplug:
|
||||
title: Enable CPU and memory hotplug
|
||||
tooltip: The default maximum CPU and maximum memory are {hotPlugTimes} times based on CPU and memory.
|
||||
restartVMMessage: Restart action is required for the virtual machine configuration change to take effect
|
||||
instance:
|
||||
singleInstance:
|
||||
multipleInstance:
|
||||
@@ -705,7 +674,7 @@ harvester:
|
||||
network:
|
||||
label: Network Data Template
|
||||
title: "Network Data:"
|
||||
tip: "The network-data configuration allows you to customize the instance's networking interfaces by assigning subnet configuration, virtual device creation (bonds, bridges, VLANs) routes and DNS configuration. <a href='https://cloudinit.readthedocs.io/en/latest/reference/network-config-format-v1.html' target='_blank'>Learn more</a>"
|
||||
tip: "The network-data configuration allows you to customize the instance’s networking interfaces by assigning subnet configuration, virtual device creation (bonds, bridges, VLANs) routes and DNS configuration. <a href='https://cloudinit.readthedocs.io/en/latest/reference/network-config-format-v1.html' target='_blank'>Learn more</a>"
|
||||
scheduling:
|
||||
affinity:
|
||||
anyNode: 'Run virtual machine on any available node'
|
||||
@@ -725,9 +694,6 @@ harvester:
|
||||
invalidUser: Invalid Username.
|
||||
input:
|
||||
name: Name
|
||||
cpu: CPU
|
||||
maxCpu: Maximum CPU
|
||||
maxMemory: Maximum Memory
|
||||
memory: Memory
|
||||
image: Image
|
||||
sshKey: SSHKey
|
||||
@@ -788,7 +754,6 @@ harvester:
|
||||
dedicatedResources: Dedicated Resources
|
||||
down: Virtual machine not running
|
||||
affinityRules: Affinity Rules
|
||||
phase: Phase
|
||||
sourceNode: Source Node
|
||||
targetNode: Target Node
|
||||
started: Started
|
||||
@@ -826,8 +791,6 @@ harvester:
|
||||
banner: These labels are automatically synchronized to the virtual machine instance.
|
||||
labels:
|
||||
banner: These key values are added as labels to the virtual machine.
|
||||
annotations:
|
||||
banner: These key values are added as annotations to the virtual machine.
|
||||
|
||||
volume:
|
||||
label: Volumes
|
||||
@@ -892,8 +855,6 @@ harvester:
|
||||
uploading: "{name} is uploading, please do not refresh or close the page."
|
||||
checksum: Checksum
|
||||
checksumTip: Validate the image using the SHA512 checksum, if specified.
|
||||
tooltip:
|
||||
imported: Created automatically by the vm-import-controller
|
||||
|
||||
vmTemplate:
|
||||
label: Templates
|
||||
@@ -930,7 +891,7 @@ harvester:
|
||||
releaseTip: Please read the upgrade documentation carefully. You can view details on the <a href="{url}" target="_blank">Harvester Release Notes</a>.
|
||||
checkReady: I have read and understood the upgrade instructions related to this Harvester version.
|
||||
pending: Pending
|
||||
upload:
|
||||
upload:
|
||||
duplicatedFile: The file you are trying to upload already exists.
|
||||
repoInfo:
|
||||
upgradeStatus: Upgrade Status
|
||||
@@ -1029,57 +990,6 @@ harvester:
|
||||
progress: Restore in progress
|
||||
complete: Restore completed
|
||||
|
||||
subnet:
|
||||
cidrBlock:
|
||||
tooltip: The subnet range in CIDR notation. Note that the CIDR blocks of different Subnets' within the same VPC cannot overlap.
|
||||
label: CIDR Block
|
||||
placeholder: e.g. 172.20.0.0/16
|
||||
protocol:
|
||||
label: Protocol
|
||||
provider:
|
||||
tooltip: Network provider for this Subnet. Must be one of the Virtual Machine Networks in OverlayNetwork type.
|
||||
label: Provider
|
||||
vpc:
|
||||
label: Virtual Private Cloud
|
||||
gateway:
|
||||
label: Gateway IP
|
||||
placeholder: e.g. 172.20.0.1
|
||||
private:
|
||||
label: Private Subnet
|
||||
tooltip: Enable network isolation for this Subnet. When enabled, VMs can only communicate within this subnet, even if other subnets exist under the same VPC.
|
||||
allowSubnet:
|
||||
label: Allow Subnets
|
||||
tooltip: You can specify certain subnets which can communicate with this subnet.
|
||||
addSubnet: Add Allow Subnet
|
||||
placeholder: e.g. 172.16.0.0/16
|
||||
excludeIPs:
|
||||
tooltip: The IP address list to reserve from automatic assignment. The gateway IP address is always excluded and will be automatically added to the list.
|
||||
|
||||
vpc:
|
||||
noAddonEnabled:
|
||||
prefix: The kubeovn-operator add-on is not enabled, click
|
||||
middle: here
|
||||
suffix: to enable the add-on to successfully create VPC and subnet.
|
||||
label: Virtual Private Cloud
|
||||
noChild: There is no subnet defined in this Virtual Private Cloud.
|
||||
createSubnet: Create Subnet
|
||||
staticRoutes:
|
||||
label: Static Routes
|
||||
cidr:
|
||||
label: CIDR
|
||||
placeholder: e.g. 172.16.0.0/16
|
||||
nextHopIP:
|
||||
tooltip: The localConnectIP on the other end of the peering VPC.
|
||||
label: Next Hop IP
|
||||
placeholder: e.g. 169.254.0.2
|
||||
vpcPeerings:
|
||||
label: VPC Peerings
|
||||
localConnectIP:
|
||||
tooltip: The designated IP address in CIDR notation of the VPC peering endpoint. Note that both IP addresses (for the VPC and the other VPC) should belong to the same subnet range and should not conflict with the cidrBlock in any existing subnets.
|
||||
label: Local Connect IP
|
||||
placeholder: e.g. 169.254.0.1/16
|
||||
remoteVpc:
|
||||
label: Remote VPC
|
||||
network:
|
||||
label: Virtual Machine Networks
|
||||
tabs:
|
||||
@@ -1122,7 +1032,6 @@ harvester:
|
||||
accessKeyId: Specify your access key ID
|
||||
secretAccessKey: Specify your secret access key
|
||||
cert: Upload a self-signed SSL certificate
|
||||
noSearchMatch: No settings match your search.
|
||||
vlanChangeTip: The newly modified default network interface only applies to newly added nodes, not existing ones.
|
||||
defaultPhysicalNIC: Default Network Interface
|
||||
modifiedMessage: Settings that have been customized from default settings are tagged with 'Modified'.
|
||||
@@ -1136,9 +1045,6 @@ harvester:
|
||||
publicCertificate: Public Certificate
|
||||
privateKey: Private Key
|
||||
ca: CA
|
||||
rancherCluster:
|
||||
kubeConfig: Rancher KubeConfig
|
||||
removeUpstreamClusterWhenNamespaceIsDeleted: Remove Upstream Cluster When Namespace Is Deleted
|
||||
storageNetwork:
|
||||
range:
|
||||
placeholder: e.g. 172.16.0.0/24
|
||||
@@ -1147,28 +1053,14 @@ harvester:
|
||||
clusterNetwork: Cluster Network
|
||||
vlan: VLAN ID
|
||||
exclude:
|
||||
label: Exclude IPs
|
||||
placeholder: e.g. 172.16.0.1
|
||||
label: Exclude
|
||||
placeholder: e.g. 172.16.0.1/32
|
||||
invalid: '"Exclude list" is invalid.'
|
||||
addIp: Add Exclude IP
|
||||
warning: 'WARNING: <br/> Any change to storage-network requires shutting down all virtual machines before applying this setting. <br/> Users have to ensure the cluster network is configured and VLAN Configuration will cover all nodes and ensure the network connectivity is working and expected in all nodes.'
|
||||
tip: 'Specify an IP range in the IPv4 CIDR format. <code>Number of IPs Required = Number of Nodes * 2 + Number of Disks * 2 + Number of Images to Download/Upload </code>. For more information about storage network settings, see the <a href="{url}" target="_blank">documentation</a>.'
|
||||
vmForceDeletionPolicy:
|
||||
period: Period
|
||||
vmMigrationNetwork:
|
||||
parseError: "Failed to parse existing configuration."
|
||||
fetchError: "Failed to load required network resources: {error}. Please refresh the page or try again later."
|
||||
clusterNetwork: Cluster Network
|
||||
vlan: VLAN ID
|
||||
range:
|
||||
placeholder: e.g. 172.16.0.0/24
|
||||
label: IP Range
|
||||
invalid: '"Range" is invalid.'
|
||||
exclude:
|
||||
label: Excluded IPs
|
||||
placeholder: e.g. 172.16.0.1/32
|
||||
invalid: '"Exclude list" is invalid.'
|
||||
addButton: Add Exclude IP
|
||||
ratio : Ratio
|
||||
autoRotateRKE2Certs:
|
||||
expiringInHours: Expiring in
|
||||
@@ -1182,11 +1074,6 @@ harvester:
|
||||
provisioner: Provisioner
|
||||
volumeSnapshotClassName: Volume Snapshot Class Name
|
||||
backupVolumeSnapshotClassName: Backup Volume Snapshot Class Name
|
||||
csiOnlineExpandValidation:
|
||||
provisioner: Provisioner
|
||||
value: Value
|
||||
invalidJsonFormat: "Configuration value is not a valid JSON format: {error}"
|
||||
failedToLoadDrivers: "Failed to load CSI drivers. Error: {error}"
|
||||
containerdRegistry:
|
||||
mirrors:
|
||||
mirrors: Mirrors
|
||||
@@ -1217,10 +1104,6 @@ harvester:
|
||||
uploadSuccess: "{name} uploaded successfully. Press Upgrade button to start the cluster upgrade process."
|
||||
deleteImage: Please select an image to delete.
|
||||
deleteSuccess: "{name} deleted successfully."
|
||||
imagePreloadStrategy: Image Preload Strategy
|
||||
restoreVM: Restore VM
|
||||
strategyType: Strategy Type
|
||||
concurrency: Concurrency
|
||||
harvesterMonitoring:
|
||||
label: Harvester Monitoring
|
||||
section:
|
||||
@@ -1382,32 +1265,6 @@ harvester:
|
||||
allowedTopologies:
|
||||
title: Allowed Topologies
|
||||
tooltip: Allowed Topologies helps scheduling virtual machines on hosts which match all of below expressions.
|
||||
internal:
|
||||
label: Internal Storage Class
|
||||
cannotDeleteTooltip: Internal storage class volumes cannot be deleted
|
||||
cannotDeleteOrDefaultTooltip: Internal storage classes cannot be deleted or set as default
|
||||
cdiSettings:
|
||||
label: CDI Settings
|
||||
volumeModeAccessModes:
|
||||
label: Volume Mode / Access Modes
|
||||
tooltip: Specifies the default volume mode and access modes.
|
||||
add: Add
|
||||
volumeMode: Volume Mode
|
||||
accessModes:
|
||||
label: Access Modes
|
||||
volumeSnapshotClass:
|
||||
label: Volume Snapshot Class
|
||||
tooltip: Sets the Volume Snapshot Class name to be used when taking snapshots of PVCs under this StorageClass.
|
||||
cloneStrategy:
|
||||
label: Clone Strategy
|
||||
tooltip: Defines the clone strategy to use for volumes created with this StorageClass.
|
||||
copy: Copy
|
||||
snapshot: Snapshot
|
||||
csiClone: CSI Clone
|
||||
fileSystemOverhead:
|
||||
label: File System Overhead
|
||||
tooltip: Specifies the percentage of filesystem overhead to consider when calculating PVC size.
|
||||
placeholder: e.g. 0.05 (up to 3 decimal places, between 0 and 1)
|
||||
|
||||
vlanConfig:
|
||||
title: Network Configuration
|
||||
@@ -1712,7 +1569,6 @@ advancedSettings:
|
||||
'harv-backup-target': Custom backup target to store virtual machine backups.
|
||||
'branding': Branding allows administrators to globally re-brand the UI by customizing the Harvester product name, logos, and color scheme.
|
||||
'harv-csi-driver-config': Configure additional information for CSI drivers.
|
||||
'harv-csi-online-expand-validation': Allow online volume expansion for specific CSI drivers.
|
||||
'harv-containerd-registry': Containerd Registry Configuration to connect private registries.
|
||||
'harv-log-level': Configure Harvester server log level. Defaults to Info.
|
||||
'harv-server-version': Harvester server version.
|
||||
@@ -1731,7 +1587,7 @@ advancedSettings:
|
||||
'harv-vm-force-reset-policy': Configuration for the force-reset action when a virtual machine is stuck on a node that is down.
|
||||
'harv-ssl-parameters': Custom SSL Parameters for TLS validation.
|
||||
'harv-storage-network': 'Longhorn storage-network setting.'
|
||||
'harv-support-bundle-namespaces': Select additional namespaces to include in the support bundle.
|
||||
'harv-support-bundle-namespaces': Specify resources in other namespaces to be collected by the support package.
|
||||
'harv-auto-disk-provision-paths': Specify the disks(using glob pattern) that Harvester will automatically add as virtual machine storage.
|
||||
'harv-support-bundle-image': Support bundle image configuration. Find different versions in <a href="https://hub.docker.com/r/rancher/support-bundle-kit/tags" target="_blank">rancher/support-bundle-kit</a>.
|
||||
'harv-release-download-url': This setting allows you to configure the <code>upgrade release download</code> URL address. Harvester will get the ISO URL and checksum value from the (<code>$URL</code>/<code>$VERSION</code>/version.yaml) file hosted by the configured URL.
|
||||
@@ -1741,10 +1597,6 @@ advancedSettings:
|
||||
'harv-kubeconfig-default-token-ttl-minutes': 'TTL (in minutes) applied on Harvester administration kubeconfig files. Default is 0, which means to never expire.'
|
||||
'harv-longhorn-v2-data-engine-enabled': 'Enable the Longhorn V2 data engine. Default is false. <ul><li>Changing this setting will restart RKE2 on all nodes. This will not affect running VM workloads.</li><li>If you see "not enough hugepages-2Mi capacity" errors when enabling this setting, wait a minute for the error to clear. If the error remains, reboot the affected node.</li></ul>'
|
||||
'harv-additional-guest-memory-overhead-ratio': 'The ratio for kubevirt to adjust the VM overhead memory. The value could be zero, empty value or floating number between 1.0 and 10.0, default to 1.5.'
|
||||
'harv-upgrade-config': 'Configure image preloading and VM restore options for upgrades. See related fields in <a href="{url}" target="_blank" rel="noopener">settings/upgrade-config</a>'
|
||||
'harv-vm-migration-network': 'Segregated network for VM migration traffic.'
|
||||
'harv-rancher-cluster': 'Configure Rancher cluster integration settings for guest cluster management.'
|
||||
'harv-max-hotplug-ratio': 'The ratio for kubevirt to limit the maximum CPU and memory that can be hotplugged to a VM. The value could be an integer between 1 and 20, default to 4.'
|
||||
|
||||
typeLabel:
|
||||
kubevirt.io.virtualmachine: |-
|
||||
@@ -1802,11 +1654,6 @@ typeLabel:
|
||||
one { Virtual Machine Backup }
|
||||
other { Virtual Machine Backups }
|
||||
}
|
||||
kubeovn.io.vpc: |-
|
||||
{count, plural,
|
||||
one { Virtual Private Cloud }
|
||||
other { Virtual Private Clouds }
|
||||
}
|
||||
harvesterhci.io.cloudtemplate: |-
|
||||
{count, plural,
|
||||
one { Cloud Configuration Template }
|
||||
|
||||
@@ -85,7 +85,7 @@ export default {
|
||||
},
|
||||
|
||||
data() {
|
||||
return { initSettings: [], searchQuery: '' };
|
||||
return { initSettings: [] };
|
||||
},
|
||||
|
||||
computed: {
|
||||
@@ -116,15 +116,7 @@ export default {
|
||||
return {
|
||||
...s,
|
||||
description: isHarvester ? `advancedSettings.descriptions.harv-${ s.id }` : `advancedSettings.descriptions.${ s.id }`,
|
||||
customized: (!s.readOnly && s.data.value && (
|
||||
s.kind === 'json' ? (() => {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(s.data.value)) !== JSON.stringify(JSON.parse(s.data.default));
|
||||
} catch {
|
||||
return s.data.value !== s.data.default;
|
||||
}
|
||||
})() : s.data.value !== s.data.default
|
||||
)) || s.data.hasCustomized
|
||||
customized: (!s.readOnly && s.data.value && s.data.value !== s.data.default) || s.data.hasCustomized
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -143,19 +135,9 @@ export default {
|
||||
{{ t('harvester.setting.modifiedMessage') }}
|
||||
</div>
|
||||
</Banner>
|
||||
<div class="fixed-header-actions harvester-settings-search">
|
||||
<div class="search row">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="search"
|
||||
class="input-sm search-box"
|
||||
:aria-label="t('sortableTable.searchLabel')"
|
||||
:placeholder="t('sortableTable.search')"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabbed
|
||||
class="mt-15"
|
||||
class="mt-30"
|
||||
>
|
||||
<Tab
|
||||
name="advanced"
|
||||
@@ -164,7 +146,6 @@ export default {
|
||||
>
|
||||
<Settings
|
||||
:settings="settings"
|
||||
:search-query="searchQuery"
|
||||
category="advanced"
|
||||
/>
|
||||
</Tab>
|
||||
@@ -175,7 +156,6 @@ export default {
|
||||
>
|
||||
<Settings
|
||||
:settings="settings"
|
||||
:search-query="searchQuery"
|
||||
category="ui"
|
||||
/>
|
||||
</Tab>
|
||||
@@ -187,20 +167,4 @@ export default {
|
||||
.settings-banner {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.harvester-settings-search {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.search {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
height: 40px;
|
||||
margin-left: 10px;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -83,34 +83,6 @@ export default {
|
||||
:rows="rows"
|
||||
:schema="schema"
|
||||
:headers="headers"
|
||||
>
|
||||
<template #cell:name="{ row }">
|
||||
<td>
|
||||
<div>
|
||||
<router-link
|
||||
v-if="row?.detailLocation"
|
||||
:to="row.detailLocation"
|
||||
>
|
||||
{{ row.nameDisplay }}
|
||||
<i
|
||||
v-if="row.isInternalStorageClass && typeof row.isInternalStorageClass === 'function' ? row.isInternalStorageClass() : false"
|
||||
v-clean-tooltip="t('harvester.storage.internal.cannotDeleteOrDefaultTooltip')"
|
||||
class="icon icon-info text-info"
|
||||
style="margin-left: 0.4em;"
|
||||
/>
|
||||
</router-link>
|
||||
<span v-else>
|
||||
{{ row.nameDisplay }}
|
||||
<i
|
||||
v-if="row.isInternalStorageClass && typeof row.isInternalStorageClass === 'function' ? row.isInternalStorageClass() : false"
|
||||
v-clean-tooltip="t('harvester.storage.internal.cannotDeleteOrDefaultTooltip')"
|
||||
class="icon icon-info text-info"
|
||||
style="margin-left: 0.4em;"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</template>
|
||||
</ResourceTable>
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -3,8 +3,6 @@ import ResourceTable from '@shell/components/ResourceTable';
|
||||
import { Banner } from '@components/Banner';
|
||||
import { defaultTableSortGenerationFn } from '@shell/components/ResourceTable.vue';
|
||||
import FilterLabel from '../components/FilterLabel';
|
||||
import { HCI as HCI_ANNOTATIONS } from '../config/labels-annotations';
|
||||
import { isInternalStorageClass } from '../utils/storage-class';
|
||||
|
||||
export default {
|
||||
name: 'ListHarvesterImage',
|
||||
@@ -55,13 +53,6 @@ export default {
|
||||
|
||||
return base;
|
||||
},
|
||||
|
||||
isInternalStorageClass(row) {
|
||||
const name = row?.spec?.targetStorageClassName ||
|
||||
row?.metadata?.annotations?.[HCI_ANNOTATIONS.STORAGE_CLASS];
|
||||
|
||||
return isInternalStorageClass(name);
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -99,11 +90,6 @@ export default {
|
||||
v-if="row.isEncrypted"
|
||||
class="icon icon-lock"
|
||||
/>
|
||||
<i
|
||||
v-if="row.isImportedImage"
|
||||
v-clean-tooltip="t('harvester.image.tooltip.imported')"
|
||||
class="icon icon-info"
|
||||
/>
|
||||
</router-link>
|
||||
<span v-else>
|
||||
{{ row.nameDisplay }}
|
||||
|
||||
@@ -8,7 +8,6 @@ import { STATE, AGE, NAME, NAMESPACE } from '@shell/config/table-headers';
|
||||
import HarvesterVolumeState from '../formatters/HarvesterVolumeState';
|
||||
import { allSettled } from '../utils/promise';
|
||||
import { HCI, VOLUME_SNAPSHOT } from '../types';
|
||||
import { INTERNAL_STORAGE_CLASS } from '../config/types';
|
||||
|
||||
const schema = {
|
||||
id: HCI.VOLUME,
|
||||
@@ -131,10 +130,6 @@ export default {
|
||||
|
||||
getVMName(row) {
|
||||
return row.attachVM?.metadata?.name || '';
|
||||
},
|
||||
|
||||
isInternalStorageClass(storageClassName) {
|
||||
return this.$store.getters['type-map/labelFor'](INTERNAL_STORAGE_CLASS, storageClassName);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -175,7 +170,7 @@ export default {
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
<template #col:name="{ row }">
|
||||
<template #col:name="{row}">
|
||||
<td>
|
||||
<span>
|
||||
<router-link
|
||||
|
||||
@@ -1,285 +0,0 @@
|
||||
<script>
|
||||
import Loading from '@shell/components/Loading';
|
||||
import ResourceTable from '@shell/components/ResourceTable';
|
||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester';
|
||||
import { NAME, AGE, NAMESPACE, STATE } from '@shell/config/table-headers';
|
||||
import { PROVIDER, PROTOCOL, CIDR_BLOCK } from '@pkg/harvester/config/table-headers';
|
||||
import { HCI } from '../types';
|
||||
import { VPC } from '../config/query-params';
|
||||
import { ADD_ONS } from '../config/harvester-map';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import MessageLink from '@shell/components/MessageLink';
|
||||
import Banner from '@components/Banner/Banner.vue';
|
||||
|
||||
export default {
|
||||
name: 'HarvesterVPC',
|
||||
|
||||
components: {
|
||||
ResourceTable,
|
||||
Loading,
|
||||
MessageLink,
|
||||
Banner
|
||||
},
|
||||
|
||||
inheritAttrs: false,
|
||||
|
||||
props: {
|
||||
schema: {
|
||||
type: Object,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
|
||||
async fetch() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const hash = await allHash({ addons: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.ADD_ONS }) });
|
||||
|
||||
this.enabledKubeOvnAddon = hash.addons.find((addon) => addon.name === ADD_ONS.KUBEOVN_OPERATOR)?.spec?.enabled === true;
|
||||
|
||||
if (this.enabledKubeOvnAddon) {
|
||||
try {
|
||||
await allHash({
|
||||
rows: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.SUBNET }),
|
||||
vpcs: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VPC }),
|
||||
});
|
||||
this.$store.dispatch('type-map/configureType', { match: HCI.SUBNET, isCreatable: this.enabledKubeOvnAddon });
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Error fetching VPC and Subnet data:', e);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
HCI,
|
||||
hasBothSchema: false,
|
||||
enabledKubeOvnAddon: false,
|
||||
to: `${ HCI.ADD_ONS }/kube-system/${ ADD_ONS.KUBEOVN_OPERATOR }?mode=edit`
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
headers() {
|
||||
return [
|
||||
STATE,
|
||||
NAME,
|
||||
NAMESPACE,
|
||||
CIDR_BLOCK,
|
||||
PROTOCOL,
|
||||
PROVIDER,
|
||||
AGE
|
||||
];
|
||||
},
|
||||
|
||||
rows() {
|
||||
return this.$store.getters[`harvester/all`](HCI.SUBNET) || [];
|
||||
},
|
||||
|
||||
vpcWithoutSubnets() {
|
||||
const vpcs = this.$store.getters[`harvester/all`](HCI.VPC) || [];
|
||||
|
||||
const out = vpcs.map((v) => {
|
||||
const hasChild = v.status?.subnets?.length > 0 || false;
|
||||
|
||||
return {
|
||||
...v,
|
||||
hasChild
|
||||
};
|
||||
});
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
isSubnetCreatable() {
|
||||
return (this.subnetSchema?.collectionMethods || []).includes('POST');
|
||||
},
|
||||
|
||||
rowsWithFakeVpcs() {
|
||||
const fakeRows = this.vpcWithoutSubnets.map((vpc) => {
|
||||
return {
|
||||
groupByLabel: vpc.id,
|
||||
isFake: true,
|
||||
mainRowKey: vpc.id,
|
||||
nameDisplay: vpc.id,
|
||||
groupByVpc: vpc.id,
|
||||
availableActions: []
|
||||
};
|
||||
});
|
||||
|
||||
return [...this.rows, ...fakeRows];
|
||||
},
|
||||
|
||||
createVPCLocation() {
|
||||
const location = {
|
||||
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`,
|
||||
params: {
|
||||
product: HARVESTER_PRODUCT,
|
||||
resource: HCI.VPC,
|
||||
},
|
||||
};
|
||||
|
||||
return location;
|
||||
},
|
||||
|
||||
vpcSchema() {
|
||||
return this.$store.getters[`harvester/schemaFor`](HCI.VPC);
|
||||
},
|
||||
|
||||
subnetSchema() {
|
||||
return this.$store.getters[`harvester/schemaFor`](HCI.SUBNET);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
groupLabel(group) {
|
||||
return `${ this.t('harvester.vpc.label') }: ${ group.key }`;
|
||||
},
|
||||
|
||||
slotName(vpc) {
|
||||
return `main-row:${ vpc }`;
|
||||
},
|
||||
|
||||
createSubnetLocation(group) {
|
||||
const vpc = group.key;
|
||||
|
||||
const location = {
|
||||
name: `${ HARVESTER_PRODUCT }-c-cluster-resource-create`,
|
||||
params: {
|
||||
product: HARVESTER_PRODUCT,
|
||||
resource: HCI.SUBNET,
|
||||
},
|
||||
};
|
||||
|
||||
location.query = { [VPC]: vpc };
|
||||
|
||||
return location;
|
||||
},
|
||||
|
||||
showVpcAction(event, group) {
|
||||
const vpc = group.key;
|
||||
|
||||
const resource = this.$store.getters[`harvester/byId`](HCI.VPC, vpc);
|
||||
|
||||
this.$store.commit(`action-menu/show`, {
|
||||
resources: [resource],
|
||||
elem: event.target
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
typeDisplay() {
|
||||
return this.t('harvester.vpc.label');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Loading v-if="$fetchState.pending" />
|
||||
<div v-else-if="!enabledKubeOvnAddon">
|
||||
<Banner color="warning">
|
||||
<MessageLink
|
||||
:to="to"
|
||||
prefix-label="harvester.vpc.noAddonEnabled.prefix"
|
||||
middle-label="harvester.vpc.noAddonEnabled.middle"
|
||||
suffix-label="harvester.vpc.noAddonEnabled.suffix"
|
||||
/>
|
||||
</Banner>
|
||||
</div>
|
||||
<div v-else>
|
||||
<Masthead
|
||||
:schema="vpcSchema"
|
||||
:type-display="t('harvester.vpc.label')"
|
||||
:resource="HCI.VPC"
|
||||
:create-location="createVPCLocation"
|
||||
:create-button-label="t('harvester.clusterNetwork.create.button.label')"
|
||||
/>
|
||||
<ResourceTable
|
||||
:rows="rowsWithFakeVpcs"
|
||||
:headers="headers"
|
||||
:schema="subnetSchema"
|
||||
:groupable="true"
|
||||
group-by="groupByVpc"
|
||||
>
|
||||
<template #header-middle>
|
||||
<div />
|
||||
</template>
|
||||
<template #group-by="{group}">
|
||||
<div class="group-bar">
|
||||
<div class="group-tab">
|
||||
<span>
|
||||
{{ groupLabel(group) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="right">
|
||||
<router-link
|
||||
v-if="isSubnetCreatable"
|
||||
class="btn btn-sm role-secondary mr-5"
|
||||
:to="createSubnetLocation(group)"
|
||||
>
|
||||
{{ t('harvester.vpc.createSubnet') }}
|
||||
</router-link>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm role-multi-action actions mr-10"
|
||||
@click="showVpcAction($event, group)"
|
||||
>
|
||||
<i class="icon icon-actions" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template
|
||||
v-for="(vpc) in vpcWithoutSubnets"
|
||||
:key="vpc.id"
|
||||
v-slot:[slotName(vpc.id)]
|
||||
>
|
||||
<tr
|
||||
v-show="!vpc.hasChild"
|
||||
class="main-row"
|
||||
>
|
||||
<td
|
||||
class="empty text-center"
|
||||
colspan="12"
|
||||
>
|
||||
{{ t('harvester.vpc.noChild') }}
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</ResourceTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.state {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.icon-warning {
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
.group-bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
.right {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.group-tab {
|
||||
&, &::after {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
right: -20px;
|
||||
}
|
||||
|
||||
SPAN {
|
||||
color: var(--body-text) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -22,8 +22,8 @@ export const VM_HEADERS = [
|
||||
{
|
||||
name: 'CPU',
|
||||
label: 'CPU',
|
||||
sort: ['displayCPU'],
|
||||
value: 'displayCPU',
|
||||
sort: ['spec.template.spec.domain.cpu.cores'],
|
||||
value: 'spec.template.spec.domain.cpu.cores',
|
||||
align: 'center',
|
||||
dashIfEmpty: true,
|
||||
},
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
import Loading from '@shell/components/Loading';
|
||||
import ResourceTable from '@shell/components/ResourceTable';
|
||||
import Masthead from '@shell/components/ResourceList/Masthead';
|
||||
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { STATE, AGE, NAME } from '@shell/config/table-headers';
|
||||
import { mapPref, GROUP_RESOURCES } from '@shell/store/prefs';
|
||||
import { NODE } from '@shell/config/types';
|
||||
|
||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester';
|
||||
import { CLUSTER_NETWORK } from '../config/query-params';
|
||||
import { HCI } from '../types';
|
||||
@@ -36,6 +38,7 @@ export default {
|
||||
|
||||
computed: {
|
||||
groupPreference: mapPref(GROUP_RESOURCES),
|
||||
|
||||
headers() {
|
||||
return [
|
||||
STATE,
|
||||
@@ -206,6 +209,7 @@ export default {
|
||||
{{ groupLabel(group) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="right">
|
||||
<router-link
|
||||
v-if="isClusterNetworkCreatable && group.key !== 'mgmt'"
|
||||
|
||||
@@ -3,9 +3,10 @@ import jsyaml from 'js-yaml';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import difference from 'lodash/difference';
|
||||
|
||||
import { sortBy } from '@shell/utils/sort';
|
||||
import { set } from '@shell/utils/object';
|
||||
import { getVmCPUMemoryValues } from '../../utils/cpuMemory';
|
||||
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { randomStr } from '@shell/utils/string';
|
||||
import { base64Decode } from '@shell/utils/crypto';
|
||||
@@ -156,16 +157,12 @@ export default {
|
||||
diskRows: [],
|
||||
networkRows: [],
|
||||
machineType: '',
|
||||
machineTypes: [],
|
||||
secretName: '',
|
||||
secretRef: null,
|
||||
showAdvanced: false,
|
||||
deleteAgent: true,
|
||||
memory: null,
|
||||
cpu: '',
|
||||
maxMemory: null,
|
||||
maxCpu: '',
|
||||
cpuMemoryHotplugEnabled: false,
|
||||
reservedMemory: null,
|
||||
accessCredentials: [],
|
||||
efiEnabled: false,
|
||||
@@ -298,20 +295,6 @@ export default {
|
||||
|
||||
async created() {
|
||||
await this.$store.dispatch(`${ this.inStore }/findAll`, { type: SECRET });
|
||||
|
||||
if (this.value.vmMachineTypesFeatureEnabled) {
|
||||
try {
|
||||
const url = this.$store.getters['harvester-common/getHarvesterClusterUrl']('v1/harvester/clusters/local?link=machineTypes');
|
||||
const machineTypes = await this.$store.dispatch('harvester/request', { url });
|
||||
|
||||
this.machineTypes = machineTypes;
|
||||
} catch (err) {
|
||||
this.machineTypes = [''];
|
||||
}
|
||||
} else {
|
||||
this.machineTypes = [''];
|
||||
}
|
||||
|
||||
this.getInitConfig({ value: this.value, init: this.isCreate });
|
||||
},
|
||||
|
||||
@@ -348,13 +331,9 @@ export default {
|
||||
const maintenanceStrategy = vm.metadata.labels?.[HCI_ANNOTATIONS.VM_MAINTENANCE_MODE_STRATEGY] || 'Migrate';
|
||||
|
||||
const runStrategy = spec.runStrategy || 'RerunOnFailure';
|
||||
const machineType = spec.template.spec.domain?.machine?.type || this.machineTypes[0];
|
||||
|
||||
const {
|
||||
cpu, memory, maxCpu, maxMemory, isHotplugEnabled
|
||||
} = getVmCPUMemoryValues(vm);
|
||||
const cpuMemoryHotplugEnabled = isHotplugEnabled;
|
||||
|
||||
const machineType = value.machineType;
|
||||
const cpu = spec.template.spec.domain?.cpu?.cores;
|
||||
const memory = spec.template.spec.domain.resources.limits.memory;
|
||||
const reservedMemory = vm.metadata?.annotations?.[HCI_ANNOTATIONS.VM_RESERVED_MEMORY];
|
||||
const terminationGracePeriodSeconds = spec.template.spec?.terminationGracePeriodSeconds || this.defaultTerminationSetting;
|
||||
|
||||
@@ -414,9 +393,6 @@ export default {
|
||||
|
||||
this['cpu'] = cpu;
|
||||
this['memory'] = memory;
|
||||
this['maxCpu'] = maxCpu;
|
||||
this['maxMemory'] = maxMemory;
|
||||
this['cpuMemoryHotplugEnabled'] = cpuMemoryHotplugEnabled;
|
||||
this['reservedMemory'] = reservedMemory;
|
||||
this['machineType'] = machineType;
|
||||
this['terminationGracePeriodSeconds'] = terminationGracePeriodSeconds;
|
||||
@@ -641,26 +617,20 @@ export default {
|
||||
this.spec.template.spec.domain.machine['type'] = this.machineType;
|
||||
}
|
||||
|
||||
this.setCPUAndMemory();
|
||||
// update terminationGracePeriodSeconds
|
||||
this.spec.template.spec.domain.cpu.cores = this.cpu;
|
||||
this.spec.template.spec.domain.resources.limits.cpu = this.cpu ? this.cpu.toString() : this.cpu;
|
||||
this.spec.template.spec.domain.resources.limits.memory = this.memory;
|
||||
this.spec.template.spec.terminationGracePeriodSeconds = this.terminationGracePeriodSeconds;
|
||||
|
||||
// parse reserved memory
|
||||
const vm = this.resourceType === HCI.VM ? this.value : this.value.spec.vm;
|
||||
|
||||
// parse reserved memory
|
||||
if (!this.reservedMemory) {
|
||||
delete vm.metadata.annotations[HCI_ANNOTATIONS.VM_RESERVED_MEMORY];
|
||||
} else {
|
||||
vm.metadata.annotations[HCI_ANNOTATIONS.VM_RESERVED_MEMORY] = this.reservedMemory;
|
||||
}
|
||||
|
||||
// add or remove cpu memory hotplug annotation
|
||||
if (this.cpuMemoryHotplugEnabled) {
|
||||
vm.metadata.annotations[HCI_ANNOTATIONS.VM_CPU_MEMORY_HOTPLUG] = this.cpuMemoryHotplugEnabled.toString();
|
||||
} else {
|
||||
delete vm.metadata.annotations[HCI_ANNOTATIONS.VM_CPU_MEMORY_HOTPLUG];
|
||||
}
|
||||
|
||||
if (this.maintenanceStrategy === 'Migrate') {
|
||||
delete vm.metadata.labels[HCI_ANNOTATIONS.VM_MAINTENANCE_MODE_STRATEGY];
|
||||
} else {
|
||||
@@ -668,36 +638,6 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
setCPUAndMemory() {
|
||||
if (this.cpuMemoryHotplugEnabled) {
|
||||
// set CPU
|
||||
this.spec.template.spec.domain.cpu.sockets = this.cpu;
|
||||
this.spec.template.spec.domain.cpu.cores = 1;
|
||||
|
||||
// set max CPU
|
||||
set(this.spec.template.spec, 'domain.cpu.maxSockets', this.maxCpu);
|
||||
// domain.resources.limits.cpu and memory are defined by k8s which requires string values
|
||||
// see https://kubernetes.io/docs/tasks/configure-pod-container/assign-cpu-resource/
|
||||
set(this.spec.template.spec, 'domain.resources.limits.cpu', this.maxCpu?.toString());
|
||||
|
||||
// set memory
|
||||
set(this.spec.template.spec, 'domain.memory.guest', this.memory);
|
||||
|
||||
// set max memory
|
||||
set(this.spec.template.spec, 'domain.memory.maxGuest', this.maxMemory);
|
||||
set(this.spec.template.spec, 'domain.resources.limits.memory', this.maxMemory);
|
||||
} else {
|
||||
this.spec.template.spec.domain.cpu.sockets = 1;
|
||||
this.spec.template.spec.domain.cpu.cores = this.cpu;
|
||||
this.spec.template.spec.domain.resources.limits.cpu = this.cpu?.toString();
|
||||
this.spec.template.spec.domain.resources.limits.memory = this.memory;
|
||||
// clean
|
||||
delete this.spec.template.spec.resources;
|
||||
delete this.spec.template.spec.domain.memory;
|
||||
delete this.spec.template.spec.domain.cpu.maxSockets;
|
||||
}
|
||||
},
|
||||
|
||||
parseDiskRows(disk) {
|
||||
const disks = [];
|
||||
const volumes = [];
|
||||
@@ -996,12 +936,9 @@ export default {
|
||||
this['sshKey'] = neu;
|
||||
},
|
||||
|
||||
updateCpuMemory(cpu, memory, maxCpu = '', maxMemory = null, cpuMemoryHotplugEnabled = false) {
|
||||
updateCpuMemory(cpu, memory) {
|
||||
this['cpu'] = cpu;
|
||||
this['memory'] = memory;
|
||||
this['maxCpu'] = maxCpu;
|
||||
this['maxMemory'] = maxMemory;
|
||||
this['cpuMemoryHotplugEnabled'] = cpuMemoryHotplugEnabled;
|
||||
},
|
||||
|
||||
parseDisk(R, index) {
|
||||
@@ -1358,21 +1295,6 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
getCPUMemoryValidation() {
|
||||
const errors = [];
|
||||
const { cpu, memory } = this;
|
||||
|
||||
if ((!cpu)) {
|
||||
errors.push(this.t('validation.required', { key: this.t('harvester.virtualMachine.input.cpu') }, true));
|
||||
}
|
||||
|
||||
if ((!memory)) {
|
||||
errors.push(this.t('validation.required', { key: this.t('harvester.virtualMachine.input.memory') }, true));
|
||||
}
|
||||
|
||||
return errors;
|
||||
},
|
||||
|
||||
getAccessCredentialsValidation() {
|
||||
const errors = [];
|
||||
|
||||
@@ -1611,7 +1533,6 @@ export default {
|
||||
if (val) {
|
||||
this['sshKey'] = [];
|
||||
this['userScript'] = undefined;
|
||||
this['networkScript'] = undefined;
|
||||
this['installAgent'] = false;
|
||||
}
|
||||
},
|
||||
@@ -1669,9 +1590,8 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
osType(neu, old) {
|
||||
this.installAgent = old === 'windows' ? true : this.installAgent;
|
||||
const out = old === 'windows' ? this.getInitUserData({ osType: neu }) : this.getUserData({ installAgent: this.installAgent, osType: neu });
|
||||
osType(neu) {
|
||||
const out = this.getUserData({ installAgent: this.installAgent, osType: neu });
|
||||
|
||||
this['userScript'] = out;
|
||||
this.refreshYamlEditor();
|
||||
|
||||
@@ -4,7 +4,6 @@ import { HCI } from '../../types';
|
||||
import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../../config/harvester';
|
||||
import { LONGHORN_DRIVER } from '@shell/config/types';
|
||||
import { DATA_ENGINE_V1, DATA_ENGINE_V2 } from '../../models/harvester/persistentvolumeclaim';
|
||||
import { isInternalStorageClass } from '../../utils/storage-class';
|
||||
|
||||
export const LVM_DRIVER = 'lvm.driver.harvesterhci.io';
|
||||
|
||||
@@ -86,22 +85,4 @@ export default class HciStorageClass extends StorageClass {
|
||||
get thirdPartyStorageFeatureEnabled() {
|
||||
return this.$rootGetters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
|
||||
}
|
||||
|
||||
isInternalStorageClass() {
|
||||
return isInternalStorageClass(this.metadata?.name);
|
||||
}
|
||||
|
||||
get availableActions() {
|
||||
let out = super.availableActions || [];
|
||||
|
||||
out = out.map((action) => {
|
||||
if ((action.action === 'setDefault' || action.action === 'setAsDefault' || action.action === 'promptRemove') && this.isInternalStorageClass()) {
|
||||
return { ...action, enabled: false };
|
||||
}
|
||||
|
||||
return action;
|
||||
});
|
||||
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,18 +109,15 @@ export default class HciSetting extends HarvesterResource {
|
||||
}
|
||||
|
||||
get parseValue() {
|
||||
let parseDefaultValue = {};
|
||||
|
||||
try {
|
||||
if (this.value) {
|
||||
return JSON.parse(this.value);
|
||||
} else if (this.default) {
|
||||
return JSON.parse(this.default);
|
||||
}
|
||||
parseDefaultValue = JSON.parse(this.value);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to parse setting value or default:', err);
|
||||
parseDefaultValue = JSON.parse(this.default);
|
||||
}
|
||||
|
||||
return {};
|
||||
return parseDefaultValue;
|
||||
}
|
||||
|
||||
get isS3() {
|
||||
|
||||
@@ -25,10 +25,6 @@ export default class HciVmBackup extends HarvesterResource {
|
||||
return detailLocation;
|
||||
}
|
||||
|
||||
get disableResourceDetailDrawerConfigTab() {
|
||||
return true;
|
||||
}
|
||||
|
||||
get doneOverride() {
|
||||
const route = this.currentRoute();
|
||||
const detailLocation = clone(this._detailLocation);
|
||||
|
||||
@@ -43,11 +43,6 @@ export default class HciVmImage extends HarvesterResource {
|
||||
|
||||
out = out.filter( (A) => !toFilter.includes(A.action));
|
||||
|
||||
// show `Clone` only when imageSource is `download`
|
||||
if (this.imageSource !== 'download') {
|
||||
out = out.filter(({ action }) => action !== 'goToClone');
|
||||
}
|
||||
|
||||
const schema = this.$getters['schemaFor'](HCI.VM);
|
||||
let canCreateVM = true;
|
||||
|
||||
@@ -57,42 +52,40 @@ export default class HciVmImage extends HarvesterResource {
|
||||
|
||||
const customActions = this.isReady ? [
|
||||
{
|
||||
action: 'createFromImage',
|
||||
enabled: canCreateVM,
|
||||
icon: 'icon icon-circle-plus',
|
||||
label: this.t('harvester.action.createVM'),
|
||||
action: 'createFromImage',
|
||||
enabled: canCreateVM,
|
||||
icon: 'icon icon-circle-plus',
|
||||
label: this.t('harvester.action.createVM'),
|
||||
},
|
||||
{
|
||||
action: 'encryptImage',
|
||||
enabled: this.volumeEncryptionFeatureEnabled && !this.isEncrypted,
|
||||
icon: 'icon icon-lock',
|
||||
label: this.t('harvester.action.encryptImage'),
|
||||
action: 'encryptImage',
|
||||
enabled: this.volumeEncryptionFeatureEnabled && !this.isEncrypted,
|
||||
icon: 'icon icon-lock',
|
||||
label: this.t('harvester.action.encryptImage'),
|
||||
},
|
||||
{
|
||||
action: 'decryptImage',
|
||||
enabled: this.volumeEncryptionFeatureEnabled && this.isEncrypted,
|
||||
icon: 'icon icon-unlock',
|
||||
label: this.t('harvester.action.decryptImage'),
|
||||
action: 'decryptImage',
|
||||
enabled: this.volumeEncryptionFeatureEnabled && this.isEncrypted,
|
||||
icon: 'icon icon-unlock',
|
||||
label: this.t('harvester.action.decryptImage'),
|
||||
},
|
||||
{
|
||||
action: 'imageDownload',
|
||||
enabled: this.links?.download,
|
||||
icon: 'icon icon-download',
|
||||
label: this.t('asyncButton.download.action'),
|
||||
action: 'imageDownload',
|
||||
enabled: this.links?.download,
|
||||
icon: 'icon icon-download',
|
||||
label: this.t('asyncButton.download.action'),
|
||||
}
|
||||
] : [];
|
||||
|
||||
// imported image only allow download, download yaml and delete actions
|
||||
if (this.isImportedImage) {
|
||||
const custom = customActions.find((a) => a.action === 'imageDownload');
|
||||
const filtered = out.filter(({ action }) => ['download', 'promptRemove'].includes(action));
|
||||
let filteredOut;
|
||||
|
||||
return custom ? [custom, { divider: true }, ...filtered] : filtered;
|
||||
if (customActions.length > 0) {
|
||||
filteredOut = out;
|
||||
} else {
|
||||
// if the first item is a divider, remove it from the array
|
||||
filteredOut = out[0]?.divider ? out.slice(1) : out;
|
||||
}
|
||||
|
||||
// if the first item is a divider, remove it from the array
|
||||
const filteredOut = customActions.length > 0 ? out : (out[0]?.divider ? out.slice(1) : out);
|
||||
|
||||
return [
|
||||
...customActions,
|
||||
...filteredOut
|
||||
@@ -213,10 +206,6 @@ export default class HciVmImage extends HarvesterResource {
|
||||
!!this.spec.securityParameters?.sourceImageNamespace;
|
||||
}
|
||||
|
||||
get isImportedImage() {
|
||||
return (this.metadata?.labels?.[HCI_ANNOTATIONS.IMPORTED_IMAGE]) === 'true';
|
||||
}
|
||||
|
||||
get displayNameWithNamespace() {
|
||||
return `${ this.metadata.namespace }/${ this.spec.displayName }`;
|
||||
}
|
||||
|
||||
@@ -60,11 +60,11 @@ export default class HciVmTemplateVersion extends HarvesterResource {
|
||||
applyDefaults() {
|
||||
const spec = {
|
||||
vm: {
|
||||
metadata: { annotations: { [HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE]: '[]' }, labels: {} },
|
||||
metadata: { annotations: { [HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE]: '[]' } },
|
||||
spec: {
|
||||
runStrategy: 'RerunOnFailure',
|
||||
template: {
|
||||
metadata: { annotations: {}, labels: {} },
|
||||
metadata: { annotations: {} },
|
||||
spec: {
|
||||
domain: {
|
||||
machine: { type: '' },
|
||||
@@ -284,10 +284,4 @@ export default class HciVmTemplateVersion extends HarvesterResource {
|
||||
get efiPersistentStateFeatureEnabled() {
|
||||
return this.$rootGetters['harvester-common/getFeatureEnabled']('efiPersistentState');
|
||||
}
|
||||
|
||||
get systemAnnotations() {
|
||||
const annotations = this.annotations || {};
|
||||
|
||||
return Object.keys(annotations).filter((key) => key.includes(HCI_ANNOTATIONS.TEMPLATE_VERSION_CUSTOM_NAME));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { clone } from '@shell/utils/object';
|
||||
import { HCI } from '../types';
|
||||
import HarvesterResource from './harvester';
|
||||
|
||||
export default class HciSubnet extends HarvesterResource {
|
||||
get groupByVpc() {
|
||||
return this.spec?.vpc || '';
|
||||
}
|
||||
|
||||
get doneOverride() {
|
||||
const detailLocation = clone(this.listLocation);
|
||||
|
||||
detailLocation.params.resource = HCI.VPC;
|
||||
|
||||
return detailLocation;
|
||||
}
|
||||
|
||||
get parentLocationOverride() {
|
||||
return {
|
||||
...this.listLocation,
|
||||
params: {
|
||||
...this.listLocation.params,
|
||||
resource: HCI.VPC
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { clone } from '@shell/utils/object';
|
||||
import { HCI } from '../types';
|
||||
import HarvesterResource from './harvester';
|
||||
|
||||
export default class HciVPC extends HarvesterResource {
|
||||
get parentNameOverride() {
|
||||
return this.$rootGetters['i18n/t'](`typeLabel."${ HCI.VPC }"`, { count: 1 })?.trim();
|
||||
}
|
||||
|
||||
get doneOverride() {
|
||||
const detailLocation = clone(this.listLocation);
|
||||
|
||||
detailLocation.params.resource = HCI.VPC;
|
||||
|
||||
return detailLocation;
|
||||
}
|
||||
|
||||
get parentLocationOverride() {
|
||||
return {
|
||||
...this.listLocation,
|
||||
params: {
|
||||
...this.listLocation.params,
|
||||
resource: HCI.VPC
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import { parseVolumeClaimTemplates } from '@pkg/utils/vm';
|
||||
import { BACKUP_TYPE } from '../config/types';
|
||||
import { HCI } from '../types';
|
||||
import HarvesterResource from './harvester';
|
||||
import { getVmCPUMemoryValues } from '../utils/cpuMemory';
|
||||
|
||||
export const OFF = 'Off';
|
||||
|
||||
@@ -32,6 +31,8 @@ const PAUSED_VM_MODAL_MESSAGE =
|
||||
|
||||
const POD_STATUS_NOT_SCHEDULABLE = 'POD_NOT_SCHEDULABLE';
|
||||
const POD_STATUS_CONTAINER_FAILING = 'POD_CONTAINER_FAILING';
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const POD_STATUS_NOT_READY = 'POD_NOT_READY';
|
||||
|
||||
const POD_STATUS_FAILED = 'POD_FAILED';
|
||||
const POD_STATUS_CRASHLOOP_BACKOFF = 'POD_CRASHLOOP_BACKOFF';
|
||||
@@ -130,12 +131,11 @@ export default class VirtVm extends HarvesterResource {
|
||||
label: this.t('harvester.action.unpause')
|
||||
},
|
||||
{
|
||||
action: 'restartVM',
|
||||
enabled: !!this.actions?.restart,
|
||||
icon: 'icon icon-refresh',
|
||||
label: this.t('harvester.action.restart'),
|
||||
bulkable: true,
|
||||
bulkAction: 'restartVM'
|
||||
action: 'restartVM',
|
||||
enabled: !!this.actions?.restart,
|
||||
icon: 'icon icon-refresh',
|
||||
label: this.t('harvester.action.restart'),
|
||||
bulkable: true
|
||||
},
|
||||
{
|
||||
action: 'softrebootVM',
|
||||
@@ -168,12 +168,6 @@ export default class VirtVm extends HarvesterResource {
|
||||
icon: 'icon icon-storage',
|
||||
label: this.t('harvester.action.editVMQuota')
|
||||
},
|
||||
{
|
||||
action: 'cpuMemoryHotplug',
|
||||
enabled: !!this.actions?.cpuAndMemoryHotplug,
|
||||
icon: 'icon icon-os-management',
|
||||
label: this.t('harvester.action.cpuAndMemoryHotplug')
|
||||
},
|
||||
{
|
||||
action: 'createSchedule',
|
||||
enabled: this.schedulingVMBackupFeatureEnabled,
|
||||
@@ -239,7 +233,7 @@ export default class VirtVm extends HarvesterResource {
|
||||
const spec = {
|
||||
runStrategy: 'RerunOnFailure',
|
||||
template: {
|
||||
metadata: { annotations: {}, labels: {} },
|
||||
metadata: { annotations: {} },
|
||||
spec: {
|
||||
domain: {
|
||||
machine: { type: '' },
|
||||
@@ -289,7 +283,6 @@ export default class VirtVm extends HarvesterResource {
|
||||
|
||||
if (realMode !== _CLONE) {
|
||||
this.metadata['annotations'] = { [HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE]: '[]' };
|
||||
this.metadata['labels'] = {};
|
||||
this['spec'] = spec;
|
||||
}
|
||||
}
|
||||
@@ -318,22 +311,12 @@ export default class VirtVm extends HarvesterResource {
|
||||
this.metadata.annotations[HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE] = JSON.stringify(deleteDataSource);
|
||||
}
|
||||
|
||||
restartVM(resources = this) {
|
||||
this.$dispatch('promptModal', {
|
||||
resources,
|
||||
action: 'restart',
|
||||
warningMessageKey: 'dialog.confirmExecution.restart.message',
|
||||
component: 'ConfirmExecutionDialog'
|
||||
});
|
||||
restartVM() {
|
||||
this.doActionGrowl('restart', {});
|
||||
}
|
||||
|
||||
softrebootVM(resources = this) {
|
||||
this.$dispatch('promptModal', {
|
||||
resources,
|
||||
action: 'softreboot',
|
||||
warningMessageKey: 'dialog.confirmExecution.softreboot.message',
|
||||
component: 'ConfirmExecutionDialog'
|
||||
});
|
||||
softrebootVM() {
|
||||
this.doActionGrowl('softreboot', {});
|
||||
}
|
||||
|
||||
openLogs() {
|
||||
@@ -481,13 +464,6 @@ export default class VirtVm extends HarvesterResource {
|
||||
});
|
||||
}
|
||||
|
||||
cpuMemoryHotplug(resources = this) {
|
||||
this.$dispatch('promptModal', {
|
||||
resources,
|
||||
component: 'HarvesterCPUMemoryHotPlugDialog'
|
||||
});
|
||||
}
|
||||
|
||||
abortMigrationVM() {
|
||||
this.doActionGrowl('abortMigration', {});
|
||||
}
|
||||
@@ -740,7 +716,6 @@ export default class VirtVm extends HarvesterResource {
|
||||
if (this &&
|
||||
!this.isVMExpectedRunning &&
|
||||
this.isVMCreated &&
|
||||
this.vmi?.status?.phase !== undefined &&
|
||||
this.vmi?.status?.phase !== VMIPhase.Succeeded &&
|
||||
this.vmi?.status?.phase !== VMIPhase.Pending
|
||||
) {
|
||||
@@ -1104,6 +1079,19 @@ export default class VirtVm extends HarvesterResource {
|
||||
maxLength: 63,
|
||||
translationKey: 'harvester.fields.name'
|
||||
},
|
||||
{
|
||||
nullable: false,
|
||||
path: 'spec.template.spec.domain.cpu.cores',
|
||||
min: 1,
|
||||
required: true,
|
||||
translationKey: 'harvester.fields.cpu'
|
||||
},
|
||||
{
|
||||
nullable: false,
|
||||
path: 'spec.template.spec.domain.resources.limits.memory',
|
||||
required: true,
|
||||
translationKey: 'harvester.fields.memory'
|
||||
},
|
||||
{
|
||||
nullable: false,
|
||||
path: 'spec.template.spec',
|
||||
@@ -1127,11 +1115,12 @@ export default class VirtVm extends HarvesterResource {
|
||||
}
|
||||
|
||||
get memorySort() {
|
||||
const memory = getVmCPUMemoryValues(this).memory;
|
||||
const memory =
|
||||
this?.spec?.template?.spec?.domain?.resources?.requests?.memory || 0;
|
||||
|
||||
const formatSize = parseSi(memory);
|
||||
|
||||
return parseInt(formatSize, 10);
|
||||
return parseInt(formatSize);
|
||||
}
|
||||
|
||||
get ingoreVMMessage() {
|
||||
@@ -1157,22 +1146,14 @@ export default class VirtVm extends HarvesterResource {
|
||||
}
|
||||
|
||||
get stateDescription() {
|
||||
const conditions = get(this, 'status.conditions');
|
||||
const restartRequired = findBy(conditions, 'type', 'RestartRequired');
|
||||
|
||||
if (restartRequired && restartRequired.status === 'True') {
|
||||
return this.t('harvester.virtualMachine.hotplug.restartVMMessage');
|
||||
}
|
||||
|
||||
return this.ingoreVMMessage ? '' : super.stateDescription;
|
||||
}
|
||||
|
||||
get displayCPU() {
|
||||
return getVmCPUMemoryValues(this).cpu;
|
||||
}
|
||||
|
||||
get displayMemory() {
|
||||
return getVmCPUMemoryValues(this).memory;
|
||||
return (
|
||||
this.spec.template.spec.domain.resources?.limits?.memory ||
|
||||
this.spec.template.spec.domain.resources?.requests?.memory
|
||||
);
|
||||
}
|
||||
|
||||
get isQemuInstalled() {
|
||||
@@ -1224,10 +1205,6 @@ export default class VirtVm extends HarvesterResource {
|
||||
return this.$rootGetters['harvester-common/getFeatureEnabled']('thirdPartyStorage');
|
||||
}
|
||||
|
||||
get vmMachineTypesFeatureEnabled() {
|
||||
return this.$rootGetters['harvester-common/getFeatureEnabled']('vmMachineTypes');
|
||||
}
|
||||
|
||||
setInstanceLabels(val) {
|
||||
if ( !this.spec?.template?.metadata?.labels ) {
|
||||
set(this, 'spec.template.metadata.labels', {});
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "harvester",
|
||||
"description": "Rancher UI Extension for Harvester",
|
||||
"version": "1.6.0-rc4",
|
||||
"version": "1.5.2-rc2",
|
||||
"private": false,
|
||||
"rancher": {
|
||||
"annotations": {
|
||||
"catalog.cattle.io/display-name": "Harvester",
|
||||
"catalog.cattle.io/kube-version": ">= 1.16.0-0",
|
||||
"catalog.cattle.io/rancher-version": ">= 2.12.0-0",
|
||||
"catalog.cattle.io/rancher-version": ">= 2.11.0-0",
|
||||
"catalog.cattle.io/ui-extensions-version": ">= 3.0.0 < 4.0.0"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -381,7 +381,7 @@ export default {
|
||||
:cancel-event="true"
|
||||
@finish="save"
|
||||
@cancel="done"
|
||||
@error="e=>errors=e"
|
||||
@error="e=>errors = e"
|
||||
>
|
||||
<RadioGroup
|
||||
v-model:value="imageSource"
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
<script>
|
||||
import { resourceNames } from '@shell/utils/string';
|
||||
import { mapGetters, mapState } from 'vuex';
|
||||
import { BACKUP_TYPE } from '@pkg/harvester/config/types';
|
||||
import { HCI } from '../types';
|
||||
import { exceptionToErrorsArray } from '@shell/utils/error';
|
||||
|
||||
export default {
|
||||
name: 'PromptRemoveVMBackupDialog',
|
||||
|
||||
emits: ['errors'],
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
names: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
type: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
|
||||
close: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
|
||||
doneLocation: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return { errors: [] };
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState('action-menu', ['toRemove']),
|
||||
...mapGetters({ t: 'i18n/t' }),
|
||||
resourceType() {
|
||||
const isSnapshot = this.value?.[0]?.spec?.type === BACKUP_TYPE.SNAPSHOT;
|
||||
const count = this.names?.length || 1;
|
||||
|
||||
if (isSnapshot) {
|
||||
return this.t(`typeLabel."${ HCI.VM_SNAPSHOT }"`, { count });
|
||||
}
|
||||
|
||||
return this.t(`typeLabel."${ HCI.BACKUP }"`, { count });
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
resourceNames,
|
||||
async remove(buttonDone) {
|
||||
try {
|
||||
await Promise.all(this.value.map((resource) => resource.remove()));
|
||||
this.close(buttonDone);
|
||||
} catch (err) {
|
||||
this.errors = exceptionToErrorsArray(err);
|
||||
buttonDone(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mt-10">
|
||||
<div class="mb-5">
|
||||
{{ t('promptRemove.attemptingToRemove', { type: resourceType }) }} <span
|
||||
v-clean-html="resourceNames(names, null, t)"
|
||||
/>
|
||||
</div>
|
||||
<Banner
|
||||
v-for="(error, i) in errors"
|
||||
:key="i"
|
||||
class=""
|
||||
color="error"
|
||||
:label="error"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -135,7 +135,7 @@ export default {
|
||||
<div>
|
||||
<div class="mt-10">
|
||||
{{ t('promptRemove.attemptingToRemove', {type}) }}
|
||||
<span v-clean-html="resourceNames(names, null, t)"></span>
|
||||
<span v-clean-html="resourceNames(names, t)"></span>
|
||||
|
||||
<div class="mt-10">
|
||||
{{ t('harvester.virtualMachine.promptRemove.title') }}
|
||||
|
||||
@@ -16,8 +16,6 @@ export const HCI = {
|
||||
RESTORE: 'harvesterhci.io.virtualmachinerestore',
|
||||
NODE_NETWORK: 'network.harvesterhci.io.nodenetwork',
|
||||
CLUSTER_NETWORK: 'network.harvesterhci.io.clusternetwork',
|
||||
SUBNET: 'kubeovn.io.subnet',
|
||||
VPC: 'kubeovn.io.vpc',
|
||||
VM_IMAGE_DOWNLOADER: 'harvesterhci.io.virtualmachineimagedownloader',
|
||||
SUPPORT_BUNDLE: 'harvesterhci.io.supportbundle',
|
||||
NETWORK_ATTACHMENT: 'harvesterhci.io.networkattachmentdefinition',
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||
|
||||
export function getVmCPUMemoryValues(vm) {
|
||||
if (!vm) {
|
||||
return {
|
||||
cpu: 0,
|
||||
memory: null,
|
||||
isHotplugEnabled: false
|
||||
};
|
||||
}
|
||||
|
||||
const isHotplugEnabled = isCPUMemoryHotPlugEnabled(vm);
|
||||
|
||||
if (isHotplugEnabled) {
|
||||
return {
|
||||
cpu: vm.spec.template.spec.domain.cpu.sockets,
|
||||
memory: vm.spec.template.spec.domain?.memory?.guest || null,
|
||||
maxCpu: vm.spec.template.spec.domain.cpu?.maxSockets || 0,
|
||||
maxMemory: vm.spec.template.spec.domain?.memory?.maxGuest || null,
|
||||
isHotplugEnabled
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
cpu: vm.spec.template.spec.domain.cpu.cores,
|
||||
memory: vm.spec.template.spec.domain.resources?.limits?.memory || null,
|
||||
isHotplugEnabled
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function isCPUMemoryHotPlugEnabled(vm) {
|
||||
return vm?.metadata?.annotations[HCI_ANNOTATIONS.VM_CPU_MEMORY_HOTPLUG] === 'true' ||
|
||||
!!vm?.spec?.template?.spec?.domain.cpu?.maxSockets ||
|
||||
!!vm?.spec?.template?.spec?.domain?.memory?.maxGuest ||
|
||||
false;
|
||||
}
|
||||
@@ -8,7 +8,6 @@ export const docLink = (suffix, version) => {
|
||||
};
|
||||
|
||||
export function getVersion(v) {
|
||||
// e.g v1.4.0
|
||||
if (process.env.VUE_APP_SERVER_VERSION) {
|
||||
return process.env.VUE_APP_SERVER_VERSION;
|
||||
}
|
||||
@@ -18,20 +17,21 @@ export function getVersion(v) {
|
||||
return `v${ semver.major(v) }.${ semver.minor(v) }.${ semver.patch(v) }`;
|
||||
} catch (error) {
|
||||
// fallback to the latest version
|
||||
return latestVersion(Object.keys(RELEASE_FEATURES));
|
||||
return getLatestVersion();
|
||||
}
|
||||
}
|
||||
|
||||
function latestVersion(versions) {
|
||||
return versions.sort((a, b) => semver.compare(a, b)).pop();
|
||||
function getLatestVersion() {
|
||||
const allVersions = Object.keys(RELEASE_FEATURES).filter(semver.valid).sort(semver.rcompare);
|
||||
|
||||
return allVersions[0] || '';
|
||||
}
|
||||
|
||||
// v1.3.3 => latest v1.3.x, v1.4.2 => latest v1.4.x
|
||||
function latestMinorVersion(v) {
|
||||
const minor = `v${ semver.major(v) }.${ semver.minor(v) }`;
|
||||
const minorVersions = Object.keys(RELEASE_FEATURES).filter((version) => version.startsWith(minor));
|
||||
function getLatestCompatibleVersion(version) {
|
||||
const allVersions = Object.keys(RELEASE_FEATURES).filter(semver.valid);
|
||||
const compatible = allVersions.filter((v) => semver.lte(v, version)).sort(semver.rcompare);
|
||||
|
||||
return latestVersion(minorVersions);
|
||||
return compatible[0] || getLatestVersion();
|
||||
}
|
||||
|
||||
export const featureEnabled = (featureKey, serverVersion) => {
|
||||
@@ -46,12 +46,16 @@ export const featureEnabled = (featureKey, serverVersion) => {
|
||||
return false;
|
||||
}
|
||||
|
||||
let releasedFeatures = RELEASE_FEATURES[version];
|
||||
const compatibleVersion = getLatestCompatibleVersion(version);
|
||||
const releasedFeatures = RELEASE_FEATURES?.[compatibleVersion];
|
||||
|
||||
if (!releasedFeatures) {
|
||||
const fallback = latestMinorVersion(version);
|
||||
if (!Array.isArray(releasedFeatures)) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
`Feature flags for version ${ version } are not defined. Please upgrade Harvester UI extension and check the support matrix.`
|
||||
);
|
||||
|
||||
releasedFeatures = RELEASE_FEATURES[fallback];
|
||||
return false;
|
||||
}
|
||||
|
||||
return releasedFeatures.includes(featureKey);
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import { INTERNAL_STORAGE_CLASS } from '../config/types';
|
||||
|
||||
export function isInternalStorageClass(name) {
|
||||
return name === INTERNAL_STORAGE_CLASS.VMSTATE_PERSISTENCE ||
|
||||
name === INTERNAL_STORAGE_CLASS.LONGHORN_STATIC;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user