mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-01 11:46:06 +08:00
Compare commits
261
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b754457ec | ||
|
|
2cb838eded | ||
|
|
bdcb34ab14 | ||
|
|
8daf44b3f6 | ||
|
|
8044e74102 | ||
|
|
264d65cc6d | ||
|
|
342a82c8e0 | ||
|
|
e7cd5cc4b0 | ||
|
|
cf405be71e | ||
|
|
911d30d7ba | ||
|
|
1b4bfa0962 | ||
|
|
d0f6f18a8a | ||
|
|
8cd482870b | ||
|
|
57cbd799dd | ||
|
|
3694b316ab | ||
|
|
717258defd | ||
|
|
7aae6264f7 | ||
|
|
c88a083e04 | ||
|
|
52c4556e64 | ||
|
|
90cb147938 | ||
|
|
d9c97de0fd | ||
|
|
fb59b396d1 | ||
|
|
bdcea54eeb | ||
|
|
912ca7883f | ||
|
|
a89678cc81 | ||
|
|
d4d3774c3b | ||
|
|
32ebdc3acd | ||
|
|
703abd7ab9 | ||
|
|
e8f7f0e06b | ||
|
|
feddcd247a | ||
|
|
29b1ab2fb9 | ||
|
|
01528f7429 | ||
|
|
9e015ec3b1 | ||
|
|
7ecc9c320d | ||
|
|
d6da4898b4 | ||
|
|
d023104371 | ||
|
|
cb452b9627 | ||
|
|
dbbad01b0f | ||
|
|
b689e3aacf | ||
|
|
796d98e61a | ||
|
|
fe32b4372b | ||
|
|
3dcec52aff | ||
|
|
6d8f6579c7 | ||
|
|
9e240792e8 | ||
|
|
aa9dd32cce | ||
|
|
4f3e532327 | ||
|
|
cbfcf4dbae | ||
|
|
8877dcf639 | ||
|
|
8d196c4244 | ||
|
|
220e40feaa | ||
|
|
258476876a | ||
|
|
63c4810b99 | ||
|
|
0cd5651c38 | ||
|
|
6f66c80c63 | ||
|
|
0e5a78d8a6 | ||
|
|
d16802365e | ||
|
|
8410f75859 | ||
|
|
f8079c5924 | ||
|
|
1e270268d8 | ||
|
|
43064576d6 | ||
|
|
9dd36bbd87 | ||
|
|
235373045c | ||
|
|
5841508b26 | ||
|
|
b941902088 | ||
|
|
485db3066f | ||
|
|
af43ebbf6f | ||
|
|
74300f24b3 | ||
|
|
2993ddb82c | ||
|
|
a5d4604dce | ||
|
|
daa6d6942b | ||
|
|
eb92642b3b | ||
|
|
78234b9e1e | ||
|
|
3dc6dda1ca | ||
|
|
fca05d8489 | ||
|
|
238c660796 | ||
|
|
19bea97106 | ||
|
|
de4edbbf3b | ||
|
|
9256391627 | ||
|
|
25439aee65 | ||
|
|
f2fe1f5bd6 | ||
|
|
f8d5aa1a21 | ||
|
|
38cf667830 | ||
|
|
18667836c2 | ||
|
|
2940f25fe0 | ||
|
|
a24d01717a | ||
|
|
22a032e56c | ||
|
|
9343813ace | ||
|
|
2dff7b0a93 | ||
|
|
54c5d77198 | ||
|
|
1880043a80 | ||
|
|
d8bee7f4f5 | ||
|
|
90c923b480 | ||
|
|
374b904191 | ||
|
|
f2725e2773 | ||
|
|
487f9abc10 | ||
|
|
1862cfcc08 | ||
|
|
480f95a5cc | ||
|
|
b939df9b7d | ||
|
|
4e831cbc5f | ||
|
|
0283cfb2bb | ||
|
|
4e2562190c | ||
|
|
64b59a0c27 | ||
|
|
3e7b2338ff | ||
|
|
104e98e390 | ||
|
|
72415622d9 | ||
|
|
a861450874 | ||
|
|
cbd5e45200 | ||
|
|
8ad7a57ab4 | ||
|
|
966d4d6709 | ||
|
|
208f1f29f5 | ||
|
|
d923239b9d | ||
|
|
54f85963ea | ||
|
|
36257299e1 | ||
|
|
91d078068f | ||
|
|
8fbbfc90f1 | ||
|
|
efdfdbf0b8 | ||
|
|
8db4de1c4a | ||
|
|
05ff8e4f19 | ||
|
|
8f76d5ad30 | ||
|
|
ff4a865c55 | ||
|
|
30dc56cad5 | ||
|
|
c10573d24a | ||
|
|
ca7eccd688 | ||
|
|
fc606dc4d1 | ||
|
|
046947de32 | ||
|
|
6e5532e497 | ||
|
|
7e00a08e35 | ||
|
|
48ec40fe3d | ||
|
|
50901a2eaf | ||
|
|
ff439e66a9 | ||
|
|
b0b5e5f749 | ||
|
|
be2db9d37c | ||
|
|
9cd3cd6492 | ||
|
|
e07042b9c9 | ||
|
|
799252a789 | ||
|
|
36c68eefb1 | ||
|
|
dea3c098df | ||
|
|
0a782fa4d5 | ||
|
|
5905448b90 | ||
|
|
4553bdc666 | ||
|
|
7f98dfe9c8 | ||
|
|
6a3153a7ec | ||
|
|
188f058df8 | ||
|
|
629cb0c601 | ||
|
|
0c3fe22df7 | ||
|
|
f4edfb8f43 | ||
|
|
66f53c8a00 | ||
|
|
f855a0add5 | ||
|
|
7c2317fae9 | ||
|
|
50fcec0c11 | ||
|
|
4a303e514c | ||
|
|
b828c2f66d | ||
|
|
40794d89a0 | ||
|
|
a6a99520a6 | ||
|
|
e2daa1ea0c | ||
|
|
923f36a5c3 | ||
|
|
b499f62c56 | ||
|
|
9d02623b3a | ||
|
|
762ab100b9 | ||
|
|
cbe66f4660 | ||
|
|
5b39432481 | ||
|
|
9e32052329 | ||
|
|
a55353fd1b | ||
|
|
548bdd2835 | ||
|
|
0153f9d985 | ||
|
|
70a56c75b0 | ||
|
|
31a9abd5c2 | ||
|
|
6d5139a994 | ||
|
|
b92f22fa75 | ||
|
|
f9a277d893 | ||
|
|
3b054b35c6 | ||
|
|
2493287334 | ||
|
|
9af8526ef9 | ||
|
|
07e0dc0d5e | ||
|
|
fd3f5ec1e6 | ||
|
|
72c6741af1 | ||
|
|
375127a78a | ||
|
|
fc09e030a0 | ||
|
|
5f76da4629 | ||
|
|
3a3122f3a1 | ||
|
|
229cbf1f69 | ||
|
|
176946e5e9 | ||
|
|
f3d2d2f6a8 | ||
|
|
a175d1e142 | ||
|
|
26afffc2f1 | ||
|
|
5eb19106ea | ||
|
|
7b3857cb38 | ||
|
|
2232e56ba1 | ||
|
|
d023fa1de4 | ||
|
|
527d31e4d0 | ||
|
|
1651e201e8 | ||
|
|
43b30ec3c3 | ||
|
|
68ba934b5a | ||
|
|
67754427c7 | ||
|
|
cbb66175f6 | ||
|
|
84bacfaff5 | ||
|
|
e8017e14c8 | ||
|
|
d11d9d4e37 | ||
|
|
50bdd53186 | ||
|
|
5c96e90d58 | ||
|
|
5c06287648 | ||
|
|
c75755a527 | ||
|
|
e8f282395e | ||
|
|
2bb65801d9 | ||
|
|
ccd4ff2d7a | ||
|
|
ac6958fe40 | ||
|
|
426e15084e | ||
|
|
8477f0754d | ||
|
|
12cadbf667 | ||
|
|
b9fab8bb57 | ||
|
|
69e639ff96 | ||
|
|
cb4cdcb0c7 | ||
|
|
c4e9a89ba5 | ||
|
|
f2d5172224 | ||
|
|
f0a66774b8 | ||
|
|
e95955fad6 | ||
|
|
fc2be69469 | ||
|
|
fdc635955b | ||
|
|
f56f46a543 | ||
|
|
f5961f3a59 | ||
|
|
8f3a2b202d | ||
|
|
0ea3f56f2c | ||
|
|
6e9350e63d | ||
|
|
1c15ed8003 | ||
|
|
9f3e689727 | ||
|
|
526d1bb875 | ||
|
|
b0c7b1fefe | ||
|
|
6644651fe5 | ||
|
|
2d6bc65b26 | ||
|
|
3565b44e3a | ||
|
|
43b10b250e | ||
|
|
7b6efa624f | ||
|
|
d9db1263d1 | ||
|
|
10a8f18784 | ||
|
|
993706fead | ||
|
|
1098ef8f6a | ||
|
|
4480dbdeed | ||
|
|
a7b4366d7c | ||
|
|
d72857f374 | ||
|
|
57ab9e2830 | ||
|
|
8116358011 | ||
|
|
c791399a61 | ||
|
|
5bd8ad2a93 | ||
|
|
345fe636ec | ||
|
|
7e32ea453b | ||
|
|
8f3b5265de | ||
|
|
4ca6caefb9 | ||
|
|
fb7d604ed6 | ||
|
|
9a60a621de | ||
|
|
7645d6161f | ||
|
|
c3f2665912 | ||
|
|
2a58745550 | ||
|
|
69d7046bc7 | ||
|
|
692c775b51 | ||
|
|
8b43c200b2 | ||
|
|
ab71cb377c | ||
|
|
814d296d75 | ||
|
|
eb667fd56b | ||
|
|
e4b3f84905 | ||
|
|
ea68b95c26 | ||
|
|
da77762b7f |
@@ -0,0 +1,7 @@
|
||||
addAssignees: author
|
||||
addReviewers: true
|
||||
numberOfReviewers: 0
|
||||
reviewers:
|
||||
- a110605
|
||||
- houhoucoop
|
||||
- NickChungSUSE
|
||||
@@ -12,6 +12,19 @@ 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,18 +2,13 @@
|
||||
### Summary
|
||||
|
||||
### PR Checklists
|
||||
- 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?
|
||||
- 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/video
|
||||
### Test screenshot or 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 -->
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"$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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
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
|
||||
@@ -0,0 +1,94 @@
|
||||
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"
|
||||
@@ -11,19 +11,18 @@ defaults:
|
||||
working-directory: ./
|
||||
|
||||
jobs:
|
||||
check-version:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Check package version
|
||||
env:
|
||||
TAG_VERSION: ${{ github.event.release.tag_name }}
|
||||
run: ./scripts/check-version.sh $TAG_VERSION
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/lint
|
||||
|
||||
build-and-push-extension-catalog:
|
||||
needs: check-version
|
||||
needs: lint
|
||||
uses: ./.github/workflows/build-extension-catalog.yml
|
||||
permissions:
|
||||
actions: write
|
||||
|
||||
@@ -4,6 +4,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- 'release-harvester-v*'
|
||||
- 'main'
|
||||
|
||||
jobs:
|
||||
setup-target-branch:
|
||||
@@ -17,14 +18,17 @@ jobs:
|
||||
- name: Determine target branch
|
||||
id: get-version
|
||||
run: |
|
||||
if [[ "${{ github.ref_name }}" =~ ^release-harvester-v([0-9]+\.[0-9]+)$ ]]; then
|
||||
if [[ "${{ github.ref_name }}" == "main" ]]; then
|
||||
TARGET_BRANCH="main-head"
|
||||
elif [[ "${{ github.ref_name }}" =~ ^release-harvester-v([0-9]+\.[0-9]+)$ ]]; then
|
||||
TARGET_BRANCH="v${BASH_REMATCH[1]}-head"
|
||||
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
|
||||
@@ -128,7 +132,7 @@ jobs:
|
||||
git push origin ${{ needs.setup-target-branch.outputs.target_branch }}
|
||||
|
||||
- name: Run Helm chart releaser
|
||||
uses: helm/chart-releaser-action@v1.7.0
|
||||
uses: helm/chart-releaser-action@v1.4.1
|
||||
with:
|
||||
charts_dir: ./charts
|
||||
env:
|
||||
|
||||
@@ -29,20 +29,19 @@ jobs:
|
||||
else
|
||||
echo "Error: invalid tag format." && exit 1
|
||||
fi
|
||||
check-version:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Check package version
|
||||
env:
|
||||
TAG_VERSION: ${{ github.event.release.tag_name }}
|
||||
run: ./scripts/check-version.sh $TAG_VERSION
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/lint
|
||||
build-extension-charts:
|
||||
needs:
|
||||
- setup-release-tag
|
||||
- check-version
|
||||
- lint
|
||||
uses: rancher/dashboard/.github/workflows/build-extension-charts.yml@master
|
||||
permissions:
|
||||
actions: write
|
||||
|
||||
@@ -5,20 +5,9 @@ on:
|
||||
- v[1-9].*
|
||||
|
||||
jobs:
|
||||
check-version:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Check package version
|
||||
env:
|
||||
TAG_VERSION: ${{github.ref_name}}
|
||||
run: ./scripts/check-version.sh $TAG_VERSION
|
||||
|
||||
build:
|
||||
name: Build and Upload Package
|
||||
uses: ./.github/workflows/build-and-publish-standalone.yaml
|
||||
needs: check-version
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
@@ -17,7 +17,37 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
fetch-depth: 0 # Need full history for commit-lint
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/lint
|
||||
|
||||
- name: Validate commit messages
|
||||
shell: bash
|
||||
run: |
|
||||
echo "GITHUB_EVENT_NAME=$GITHUB_EVENT_NAME"
|
||||
echo "GITHUB_BASE_SHA=$GITHUB_BASE_SHA"
|
||||
echo "GITHUB_HEAD_SHA=$GITHUB_HEAD_SHA"
|
||||
echo "GITHUB_BEFORE=$GITHUB_BEFORE"
|
||||
echo "GITHUB_AFTER=$GITHUB_AFTER"
|
||||
|
||||
if [ "$GITHUB_EVENT_NAME" = "pull_request" ] && [ -n "$GITHUB_BASE_SHA" ] && [ -n "$GITHUB_HEAD_SHA" ]; then
|
||||
FROM="$GITHUB_BASE_SHA"
|
||||
TO="$GITHUB_HEAD_SHA"
|
||||
elif [ -n "$GITHUB_BEFORE" ] && [ -n "$GITHUB_AFTER" ]; then
|
||||
FROM="$GITHUB_BEFORE"
|
||||
TO="$GITHUB_AFTER"
|
||||
else
|
||||
echo "No valid commit range found, skipping commitlint."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "FROM=$FROM"
|
||||
echo "TO=$TO"
|
||||
npx commitlint --from "$FROM" --to "$TO" --verbose
|
||||
env:
|
||||
GITHUB_EVENT_NAME: ${{ github.event_name }}
|
||||
GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
GITHUB_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
GITHUB_BEFORE: ${{ github.event.before }}
|
||||
GITHUB_AFTER: ${{ github.event.after }}
|
||||
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
npx --no -- commitlint --edit $1
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
yarn lint
|
||||
@@ -1,62 +1,169 @@
|
||||
# harvester-ui-extension
|
||||
|
||||
Rancher Extension used in [rancher/dashboard](https://github.com/rancher/dashboard) for [Harvester](https://harvesterhci.io) Dashboard UI.
|
||||
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).
|
||||
|
||||
`
|
||||
From Rancher 2.10.0, Harvester integration is provided by this new Harvester UI Extension.
|
||||
`
|
||||
> **Note:**
|
||||
> This extension is available starting from **Rancher 2.10.0**. Ensure your Rancher version is **2.10.0 or later** to access Harvester integration.
|
||||
|
||||
## Installation
|
||||
|
||||
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.
|
||||
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).
|
||||
|
||||
## Running for Development
|
||||
|
||||
This is what you probably want to get started.
|
||||
|
||||
### Prerequisite
|
||||
|
||||
Nodejs version >= 20
|
||||
## Development Setup
|
||||
|
||||
Ensure **Node.js v20 or later** is installed for development and debugging.
|
||||
|
||||
### Standalone Mode
|
||||
|
||||
For development, serve with hot reload at https://localhost:8005
|
||||
Run the extension standalone with hot reload at `https://localhost:8005`.
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
yarn install
|
||||
|
||||
# Use RANCHER_ENV to specify the endpoint for your Harvester API
|
||||
# Start the development server
|
||||
RANCHER_ENV=harvester API=https://your-harvester-ip 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
|
||||
# Example with specific server version
|
||||
RANCHER_ENV=harvester VUE_APP_SERVER_VERSION=v1.5.0 API=https://192.168.1.123 yarn dev
|
||||
```
|
||||
|
||||
# or put your env variables into a .env file
|
||||
You may also define environment variables in a `.env` file:
|
||||
|
||||
# Example
|
||||
RANCHER_ENV=harvester VUE_APP_SERVER_VERSION=v1.4.0 API=https://192.168.1.123 yarn dev
|
||||
```env
|
||||
RANCHER_ENV=harvester
|
||||
VUE_APP_SERVER_VERSION=v1.5.0
|
||||
API=https://192.168.1.123
|
||||
```
|
||||
|
||||
### Rancher Integration Mode
|
||||
|
||||
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).
|
||||
To run as a Rancher extension, follow the [Rancher UI Extension Guide](https://extensions.rancher.io/extensions/next/extensions-getting-started#running-the-app).
|
||||
|
||||
```bash
|
||||
API=https://your-rancher-ip yarn dev
|
||||
|
||||
# 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
|
||||
|
||||
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.
|
||||
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
|
||||
|
||||
License
|
||||
=======
|
||||
Copyright (c) 2014-2025 [SUSE, LLC.](https://www.suse.com/)
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
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': [2, 'always', 72],
|
||||
'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'),
|
||||
],
|
||||
};
|
||||
+19
-14
@@ -1,36 +1,36 @@
|
||||
{
|
||||
"name": "harvester-ui-extension",
|
||||
"version": "1.5.2-rc2",
|
||||
"version": "1.6.0-rc1",
|
||||
"private": false,
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@rancher/shell": "3.0.2-rc.6",
|
||||
"@babel/plugin-transform-class-static-block": "7.26.0",
|
||||
"@rancher/shell": "3.0.5-rc.3",
|
||||
"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",
|
||||
"@babel/plugin-transform-class-static-block": "7.28.3"
|
||||
"yaml": "^2.5.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/node": "~20.19.0",
|
||||
"cronstrue": "2.59.0",
|
||||
"@types/node": "~20.10.0",
|
||||
"cronstrue": "2.50.0",
|
||||
"d3-color": "3.1.0",
|
||||
"ejs": "3.1.10",
|
||||
"follow-redirects": "1.15.11",
|
||||
"follow-redirects": "1.15.6",
|
||||
"glob": "7.2.3",
|
||||
"glob-parent": "6.0.2",
|
||||
"json5": "2.2.3",
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/lodash": "4.17.5",
|
||||
"merge": "2.1.1",
|
||||
"node-forge": "1.3.1",
|
||||
"nth-check": "2.1.1",
|
||||
"qs": "6.14.0",
|
||||
"roarr": "7.21.1",
|
||||
"semver": "7.7.2",
|
||||
"qs": "6.11.1",
|
||||
"roarr": "7.0.4",
|
||||
"semver": "7.5.4",
|
||||
"@vue/cli-service/html-webpack-plugin": "^5.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -41,10 +41,15 @@
|
||||
"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"
|
||||
"parse-tag-name": "./node_modules/@rancher/shell/scripts/extension/parse-tag-name",
|
||||
"commitlint": "commitlint --edit",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^19.8.1",
|
||||
"@commitlint/config-conventional": "^19.8.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^7.1.0"
|
||||
"eslint-plugin-promise": "^7.1.0",
|
||||
"husky": "^9.1.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
<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_SETTING } from '../config/settings';
|
||||
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';
|
||||
|
||||
const CATEGORY = {
|
||||
ui: [
|
||||
@@ -19,6 +22,7 @@ export default {
|
||||
components: {
|
||||
AsyncButton,
|
||||
Banner,
|
||||
ActionMenu
|
||||
},
|
||||
|
||||
props: {
|
||||
@@ -30,15 +34,23 @@ 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)
|
||||
};
|
||||
},
|
||||
|
||||
@@ -48,12 +60,81 @@ export default {
|
||||
settings: {
|
||||
deep: true,
|
||||
handler() {
|
||||
this['categorySettings'] = this.filterCategorySettings();
|
||||
this.categorySettings = this.filterCategorySettings();
|
||||
this.filteredSettings = this.filterSearchSettings(this.categorySettings, this.searchQuery);
|
||||
}
|
||||
},
|
||||
searchQuery: {
|
||||
immediate: true,
|
||||
handler(newQuery) {
|
||||
const filtered = this.filterSearchSettings(this.categorySettings, newQuery);
|
||||
|
||||
this.filteredSettings = newQuery ? this.openJsonSettings(filtered) : filtered.map((s) => ({ ...s, hide: this.originalHideMap[s.id] ?? false }));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
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;
|
||||
}
|
||||
|
||||
const description = this.t(setting.description, this.getDocLinkParams(setting) || {}, true)?.toLowerCase() || '';
|
||||
|
||||
// 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)) {
|
||||
@@ -74,25 +155,21 @@ 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) {
|
||||
this.categorySettings.find((setting) => {
|
||||
if (setting.id === s.id) {
|
||||
setting.hide = !setting.hide;
|
||||
}
|
||||
});
|
||||
const setting = this.filteredSettings.find((setting) => setting.id === s.id);
|
||||
|
||||
if (setting) {
|
||||
setting.hide = !setting.hide;
|
||||
this.originalHideMap[setting.id] = setting.hide;
|
||||
}
|
||||
},
|
||||
|
||||
async testConnect(buttonDone, value) {
|
||||
@@ -118,6 +195,19 @@ 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 {};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -126,7 +216,7 @@ export default {
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-for="(setting, i) in categorySettings"
|
||||
v-for="(setting, i) in filteredSettings"
|
||||
:key="i"
|
||||
class="advanced-setting mb-20"
|
||||
>
|
||||
@@ -148,7 +238,7 @@ export default {
|
||||
Experimental
|
||||
</span>
|
||||
</h1>
|
||||
<h2 v-clean-html="t(setting.description, {}, true)">
|
||||
<h2 v-clean-html="t(setting.description, getDocLinkParams(setting) || {}, true)">
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
@@ -156,15 +246,12 @@ export default {
|
||||
:id="setting.id"
|
||||
class="action"
|
||||
>
|
||||
<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>
|
||||
<ActionMenu
|
||||
:resource="setting.data"
|
||||
:button-aria-label="t('advancedSettings.edit.label')"
|
||||
data-testid="action-button"
|
||||
button-role="tertiary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div value>
|
||||
@@ -221,6 +308,12 @@ 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>
|
||||
|
||||
@@ -271,4 +364,8 @@ export default {
|
||||
padding: 2px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.no-search-match {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,6 +3,9 @@ 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',
|
||||
|
||||
@@ -11,47 +14,76 @@ 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(',') : [];
|
||||
|
||||
if (namespacesStr) {
|
||||
namespaces = namespacesStr.split(',');
|
||||
}
|
||||
|
||||
return { namespaces };
|
||||
return {
|
||||
namespaces,
|
||||
defaultNamespaces: [],
|
||||
loading: true
|
||||
};
|
||||
},
|
||||
|
||||
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() {
|
||||
return this.$store.getters['harvester/all'](NAMESPACE).map((N) => {
|
||||
return {
|
||||
label: N.id,
|
||||
value: N.id
|
||||
};
|
||||
});
|
||||
if (this.availableNamespaces.length === 0) return [];
|
||||
|
||||
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];
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
update() {
|
||||
const namespaceStr = this.namespaces.join(',');
|
||||
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);
|
||||
}
|
||||
|
||||
this.value['value'] = namespaceStr;
|
||||
this.value.value = this.namespaces.join(',');
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
'value.value': {
|
||||
handler(neu) {
|
||||
if (neu === this.value.default || !neu) {
|
||||
this.namespaces = [];
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
'value.value'(newVal) {
|
||||
const raw = newVal || this.value.default || '';
|
||||
|
||||
this.namespaces = raw ? raw.split(',') : [];
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -62,6 +94,7 @@ export default {
|
||||
<div class="col span-12">
|
||||
<LabeledSelect
|
||||
v-model:value="namespaces"
|
||||
:loading="loading"
|
||||
:multiple="true"
|
||||
label-key="nameNsDescription.namespace.label"
|
||||
:mode="mode"
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
<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,8 +1,10 @@
|
||||
// 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`,
|
||||
STORAGE_NETWORK_EXAMPLE: `/advanced/storagenetwork#configuration-example`,
|
||||
KSMTUNED_MODE: `/host/#ksmtuned-mode`,
|
||||
UPGRADE_URL: `/upgrade/index`
|
||||
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`,
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ const FEATURE_FLAGS = {
|
||||
'vmSnapshotQuota',
|
||||
'longhornV2LVMSupport',
|
||||
'improveMaintenanceMode',
|
||||
'upgradeConfigSetting'
|
||||
],
|
||||
'v1.4.1': [],
|
||||
'v1.4.2': [
|
||||
@@ -33,9 +34,14 @@ const FEATURE_FLAGS = {
|
||||
'efiPersistentState',
|
||||
'untaggedNetworkSetting',
|
||||
'skipSingleReplicaDetachedVol',
|
||||
'thirdPartyStorage'
|
||||
'thirdPartyStorage',
|
||||
'liveMigrationProgress'
|
||||
],
|
||||
'v1.5.1': [],
|
||||
'v1.6.0': [
|
||||
'vmMachineTypes',
|
||||
'customSupportBundle'
|
||||
]
|
||||
};
|
||||
|
||||
const generateFeatureFlags = () => {
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
CONFIGURED_PROVIDERS,
|
||||
SUB_TYPE,
|
||||
ADDRESS,
|
||||
DESCRIPTION,
|
||||
} from '@shell/config/table-headers';
|
||||
import { IF_HAVE } from '@shell/store/type-map';
|
||||
import {
|
||||
@@ -32,7 +33,8 @@ import {
|
||||
IMAGE_PROGRESS,
|
||||
SNAPSHOT_TARGET_VOLUME,
|
||||
IMAGE_VIRTUAL_SIZE,
|
||||
IMAGE_STORAGE_CLASS
|
||||
IMAGE_STORAGE_CLASS,
|
||||
HARVESTER_DESCRIPTION
|
||||
} from './table-headers';
|
||||
|
||||
const TEMPLATE = HCI.VM_VERSION;
|
||||
@@ -222,6 +224,7 @@ export function init($plugin, store) {
|
||||
STATE,
|
||||
NAME_COL,
|
||||
NAMESPACE_COL,
|
||||
HARVESTER_DESCRIPTION,
|
||||
IMAGE_STORAGE_CLASS,
|
||||
IMAGE_PROGRESS,
|
||||
IMAGE_DOWNLOAD_SIZE,
|
||||
@@ -255,7 +258,7 @@ export function init($plugin, store) {
|
||||
});
|
||||
|
||||
if (isSingleVirtualCluster) {
|
||||
headers(NAMESPACE, [STATE, NAME_UNLINKED, AGE]);
|
||||
headers(NAMESPACE, [STATE, NAME_UNLINKED, DESCRIPTION, AGE]);
|
||||
basicType([NAMESPACE]);
|
||||
virtualType({
|
||||
labelKey: 'harvester.namespace.label',
|
||||
|
||||
@@ -35,6 +35,7 @@ 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',
|
||||
@@ -66,5 +67,8 @@ 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'
|
||||
K8S_ARCH: 'kubernetes.io/arch',
|
||||
IMAGE_DISPLAY_NAME: 'harvesterhci.io/imageDisplayName',
|
||||
CUSTOM_IP: 'harvesterhci.io/custom-ip',
|
||||
IMPORTED_IMAGE: 'migration.harvesterhci.io/imported'
|
||||
};
|
||||
|
||||
@@ -34,6 +34,7 @@ export const HCI_SETTING = {
|
||||
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',
|
||||
};
|
||||
|
||||
export const HCI_ALLOWED_SETTINGS = {
|
||||
@@ -61,9 +62,9 @@ export const HCI_ALLOWED_SETTINGS = {
|
||||
kind: 'multiline', canReset: true, from: 'import'
|
||||
},
|
||||
[HCI_SETTING.OVERCOMMIT_CONFIG]: { kind: 'json', from: 'import' },
|
||||
[HCI_SETTING.SUPPORT_BUNDLE_TIMEOUT]: {},
|
||||
[HCI_SETTING.SUPPORT_BUNDLE_EXPIRATION]: {},
|
||||
[HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT]: { featureFlag: 'supportBundleNodeCollectionTimeoutSetting' },
|
||||
[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_IMAGE]: { kind: 'json', from: 'import' },
|
||||
[HCI_SETTING.STORAGE_NETWORK]: {
|
||||
kind: 'custom', from: 'import', canReset: true
|
||||
@@ -89,13 +90,19 @@ export const HCI_ALLOWED_SETTINGS = {
|
||||
[HCI_SETTING.NTP_SERVERS]: {
|
||||
kind: 'json', from: 'import', canReset: true
|
||||
},
|
||||
[HCI_SETTING.KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES]: { featureFlag: 'kubeconfigDefaultTokenTTLMinutesSetting' },
|
||||
[HCI_SETTING.KUBECONFIG_DEFAULT_TOKEN_TTL_MINUTES]: { kind: 'number', 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'
|
||||
},
|
||||
};
|
||||
|
||||
export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* Harvester
|
||||
*/
|
||||
import { DESCRIPTION } from '@shell/config/table-headers';
|
||||
|
||||
// image
|
||||
export const IMAGE_DOWNLOAD_SIZE = {
|
||||
@@ -96,5 +97,10 @@ export const IMAGE_STORAGE_CLASS = {
|
||||
sort: 'imageStorageClass',
|
||||
value: 'imageStorageClass',
|
||||
align: 'left',
|
||||
width: 200,
|
||||
width: 150,
|
||||
};
|
||||
|
||||
export const HARVESTER_DESCRIPTION = {
|
||||
...DESCRIPTION,
|
||||
width: 150,
|
||||
};
|
||||
|
||||
@@ -125,6 +125,7 @@ export default {
|
||||
:resource="value"
|
||||
:mode="mode"
|
||||
:apply-hooks="applyHooks"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<Tabbed
|
||||
v-if="spec"
|
||||
|
||||
@@ -118,6 +118,10 @@ export default {
|
||||
imageName() {
|
||||
return this.value?.metadata?.annotations?.[HCI_ANNOTATIONS.IMAGE_NAME] || '-';
|
||||
},
|
||||
|
||||
sourceType() {
|
||||
return this.value?.spec?.sourceType;
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -252,6 +256,16 @@ 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,6 +125,7 @@ export default {
|
||||
:resource="value"
|
||||
:mode="mode"
|
||||
:apply-hooks="applyHooks"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<Tabbed
|
||||
v-if="spec"
|
||||
|
||||
+25
@@ -17,6 +17,13 @@ export default {
|
||||
default: () => {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
vmimResource: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -25,6 +32,12 @@ 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;
|
||||
},
|
||||
@@ -58,6 +71,18 @@ 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,6 +27,8 @@ 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',
|
||||
|
||||
@@ -63,6 +65,7 @@ export default {
|
||||
hasResourceQuotaSchema: false,
|
||||
switchToCloud: false,
|
||||
VM_METRICS_DETAIL_URL,
|
||||
VM_MIGRATION_DETAIL_URL,
|
||||
showVmMetrics: false,
|
||||
};
|
||||
},
|
||||
@@ -78,6 +81,7 @@ 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 }),
|
||||
};
|
||||
|
||||
@@ -131,6 +135,31 @@ 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 migrationUid = this.vmi?.status?.migrationState?.migrationUid;
|
||||
const vmim = vmimList.find((VMIM) => VMIM?.metadata?.uid === migrationUid);
|
||||
|
||||
return vmim;
|
||||
},
|
||||
|
||||
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 || {};
|
||||
@@ -138,7 +167,6 @@ 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;
|
||||
@@ -157,6 +185,10 @@ export default {
|
||||
vm: this.value.name
|
||||
};
|
||||
},
|
||||
|
||||
liveMigrationProgressEnabled() {
|
||||
return this.$store.getters['harvester-common/getFeatureEnabled']('liveMigrationProgress');
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
@@ -346,6 +378,20 @@ 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,4 +1,5 @@
|
||||
<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';
|
||||
@@ -6,7 +7,15 @@ 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',
|
||||
@@ -17,14 +26,36 @@ 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 {
|
||||
url: '',
|
||||
description: '',
|
||||
errors: [],
|
||||
isOpen: false,
|
||||
isOpen: false,
|
||||
errors: [],
|
||||
version: '',
|
||||
clusterName: '',
|
||||
url: '',
|
||||
description: '',
|
||||
namespaces: [],
|
||||
defaultNamespaces: [],
|
||||
timeout: '',
|
||||
expiration: '',
|
||||
nodeTimeout: '',
|
||||
};
|
||||
},
|
||||
|
||||
@@ -39,23 +70,51 @@ 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) {
|
||||
if (show) {
|
||||
this.$nextTick(() => {
|
||||
this.isOpen = true;
|
||||
});
|
||||
} else {
|
||||
this.isOpen = false;
|
||||
this.url = '';
|
||||
this.description = '';
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
this.isOpen = show;
|
||||
}
|
||||
},
|
||||
|
||||
isOpen(newVal) {
|
||||
if (newVal) {
|
||||
this.loadDefaultSettings();
|
||||
} else {
|
||||
this.resetForm();
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -65,33 +124,93 @@ export default {
|
||||
close() {
|
||||
this.isOpen = false;
|
||||
this.$store.commit('harvester-common/toggleBundleModal', false);
|
||||
this.backUpName = '';
|
||||
},
|
||||
|
||||
loadDefaultSettings() {
|
||||
const cluster = this.$store.getters['currentCluster'];
|
||||
const versionSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SERVER_VERSION);
|
||||
const namespacesSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_NAMESPACES);
|
||||
const timeoutSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_TIMEOUT);
|
||||
const expirationSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_EXPIRATION);
|
||||
const nodeTimeoutSetting = this.$store.getters['harvester/byId'](HCI.SETTING, HCI_SETTING.SUPPORT_BUNDLE_NODE_COLLECTION_TIMEOUT);
|
||||
|
||||
this.version = versionSetting?.currentVersion || '';
|
||||
this.clusterName = cluster?.id || '';
|
||||
this.namespaces = (namespacesSetting?.value ?? namespacesSetting?.default ?? '').split(',').map((ns) => ns.trim()).filter((ns) => ns);
|
||||
this.timeout = timeoutSetting?.value ?? timeoutSetting?.default ?? '';
|
||||
this.expiration = expirationSetting?.value ?? expirationSetting?.default ?? '';
|
||||
this.nodeTimeout = nodeTimeoutSetting?.value ?? nodeTimeoutSetting?.default ?? '';
|
||||
this.url = '';
|
||||
this.description = '';
|
||||
this.errors = [];
|
||||
},
|
||||
|
||||
resetForm() {
|
||||
this.url = '';
|
||||
this.description = '';
|
||||
this.namespaces = [];
|
||||
this.timeout = '';
|
||||
this.expiration = '';
|
||||
this.nodeTimeout = '';
|
||||
this.errors = [];
|
||||
},
|
||||
|
||||
updateNamespaces(selected) {
|
||||
if (selected.includes(SELECT_ALL)) {
|
||||
this.namespaces = [...this.availableNamespaces];
|
||||
} else if (selected.includes(UNSELECT_ALL)) {
|
||||
this.namespaces = [];
|
||||
} else {
|
||||
this.namespaces = selected.filter((val) => val !== SELECT_ALL && val !== UNSELECT_ALL);
|
||||
}
|
||||
},
|
||||
|
||||
updateNumberValue(field, value) {
|
||||
if (value === '' || value === null || isNaN(value)) {
|
||||
this[field] = '';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const num = Number(value);
|
||||
const isValid = Number.isInteger(num) && num >= 0;
|
||||
|
||||
this[field] = isValid ? String(num) : '';
|
||||
},
|
||||
|
||||
onKeyDown(e) {
|
||||
if (['e', 'E', '+', '-', '.'].includes(e.key)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
|
||||
async save(buttonCb) {
|
||||
this.errors = [];
|
||||
|
||||
const name = `bundle-${ randomStr(5).toLowerCase() }`;
|
||||
const name = `bundle-${ this.clusterName }-${ this.version }-${ 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: {
|
||||
issueURL: this.url,
|
||||
description: this.description
|
||||
}
|
||||
metadata: { name, namespace },
|
||||
spec,
|
||||
};
|
||||
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const bundleValue = await this.$store.dispatch(`${ inStore }/create`, bundleCrd);
|
||||
|
||||
try {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const bundleValue = await this.$store.dispatch(`${ inStore }/create`, bundleCrd);
|
||||
|
||||
await bundleValue.save();
|
||||
|
||||
this.$store.commit('harvester-common/setLatestBundleId', `${ namespace }/${ name }`, { root: true });
|
||||
@@ -118,69 +237,110 @@ export default {
|
||||
@close="close"
|
||||
>
|
||||
<div class="p-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">
|
||||
<h2>{{ t('harvester.modal.bundle.title') }}</h2>
|
||||
<div class="content">
|
||||
<div
|
||||
v-if="bundlePending"
|
||||
class="circle mb-20"
|
||||
>
|
||||
<GraphCircle
|
||||
primary-stroke-color="green"
|
||||
secondary-stroke-color="white"
|
||||
secondary-stroke-color="lightgrey"
|
||||
:stroke-width="6"
|
||||
:percentage="percentage"
|
||||
:show-text="true"
|
||||
/>
|
||||
</div>
|
||||
</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
|
||||
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"
|
||||
<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"
|
||||
>
|
||||
{{ t('generic.close') }}
|
||||
</button>
|
||||
|
||||
<AsyncButton
|
||||
type="submit"
|
||||
mode="generate"
|
||||
class="btn btn-sm bg-primary"
|
||||
:disabled="bundlePending"
|
||||
@click="save"
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</app-modal>
|
||||
@@ -194,6 +354,10 @@ 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);
|
||||
@@ -204,10 +368,8 @@ export default {
|
||||
}
|
||||
|
||||
.content {
|
||||
height: 218px;
|
||||
|
||||
.circle {
|
||||
padding-top: 20px;
|
||||
padding: 10px 0;
|
||||
height: 160px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,8 +73,10 @@ 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: `/v1/harvester/${ HCI.VLAN_CONFIG }s/${ data.id }`,
|
||||
url,
|
||||
method: 'PUT',
|
||||
data,
|
||||
});
|
||||
|
||||
@@ -38,6 +38,7 @@ export default {
|
||||
:mode="mode"
|
||||
:errors="errors"
|
||||
@finish="save"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<component
|
||||
:is="currentComponent"
|
||||
|
||||
@@ -80,6 +80,7 @@ export default {
|
||||
:apply-hooks="applyHooks"
|
||||
@finish="save"
|
||||
@cancel="done"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<NameNsDescription
|
||||
:value="value"
|
||||
|
||||
@@ -75,6 +75,7 @@ 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"
|
||||
>
|
||||
|
||||
@@ -190,6 +190,7 @@ export default {
|
||||
:errors="errors"
|
||||
:apply-hooks="applyHooks"
|
||||
@finish="saveNetwork"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<NameNsDescription
|
||||
ref="nd"
|
||||
|
||||
@@ -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,6 +84,10 @@ export default {
|
||||
this.value['_type'] = TYPES.OPAQUE;
|
||||
}
|
||||
|
||||
if ( this.realMode === _CLONE) {
|
||||
this.value['_type'] = this.liveValue._type;
|
||||
}
|
||||
|
||||
return {
|
||||
isCloud,
|
||||
nodeDrivers: null,
|
||||
@@ -234,7 +238,7 @@ export default {
|
||||
:resource="value"
|
||||
:errors="errors"
|
||||
@finish="saveSecret"
|
||||
@error="e=>errors = e"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<NameNsDescription
|
||||
:value="value"
|
||||
|
||||
@@ -4,8 +4,10 @@ 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';
|
||||
|
||||
@@ -15,7 +17,8 @@ export default {
|
||||
LabeledInput,
|
||||
LabeledSelect,
|
||||
RadioGroup,
|
||||
TextAreaAutoGrow
|
||||
TextAreaAutoGrow,
|
||||
UnitInput
|
||||
},
|
||||
|
||||
mixins: [CreateEditView],
|
||||
@@ -57,7 +60,13 @@ export default {
|
||||
|
||||
return {
|
||||
setting,
|
||||
description: isHarvester ? t(`advancedSettings.descriptions.harv-${ this.value.id }`) : t(`advancedSettings.descriptions.${ this.value.id }`),
|
||||
description: isHarvester ? t(
|
||||
`advancedSettings.descriptions.harv-${ this.value.id }`,
|
||||
this.getDocLinkParams()
|
||||
) : t(
|
||||
`advancedSettings.descriptions.${ this.value.id }`,
|
||||
this.getDocLinkParams()
|
||||
),
|
||||
editHelp: t(`advancedSettings.editHelp.${ this.value.id }`),
|
||||
enumOptions,
|
||||
canReset,
|
||||
@@ -128,6 +137,13 @@ 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);
|
||||
},
|
||||
|
||||
@@ -163,6 +179,18 @@ 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 {};
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -177,7 +205,7 @@ export default {
|
||||
:subtypes="[]"
|
||||
:can-yaml="false"
|
||||
:cancel-event="true"
|
||||
@error="e => (errors = e)"
|
||||
@error="e=>errors=e"
|
||||
@finish="saveSettings"
|
||||
@cancel="done"
|
||||
>
|
||||
@@ -238,6 +266,13 @@ 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"
|
||||
|
||||
@@ -286,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"
|
||||
>
|
||||
|
||||
@@ -110,6 +110,7 @@ export default {
|
||||
headers: {},
|
||||
fileUrl: '',
|
||||
file: '',
|
||||
HCI_ANNOTATIONS,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -595,6 +596,8 @@ 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)"
|
||||
>
|
||||
@@ -611,7 +614,7 @@ export default {
|
||||
<input
|
||||
v-else
|
||||
v-model="row[valueName]"
|
||||
:disabled="isView"
|
||||
:disabled="isView || row[keyName] === HCI_ANNOTATIONS.IMAGE_DISPLAY_NAME"
|
||||
:type="'text'"
|
||||
:placeholder="t('keyValue.valuePlaceholder')"
|
||||
autocorrect="off"
|
||||
|
||||
@@ -189,6 +189,16 @@ 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);
|
||||
@@ -219,6 +229,7 @@ export default {
|
||||
:errors="errors"
|
||||
:apply-hooks="applyHooks"
|
||||
@finish="saveVMT"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<NameNsDescription
|
||||
:value="templateValue"
|
||||
@@ -358,6 +369,26 @@ 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')"
|
||||
|
||||
@@ -304,6 +304,10 @@ 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);
|
||||
|
||||
@@ -374,10 +378,12 @@ 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,6 +64,7 @@ export default {
|
||||
:errors="errors"
|
||||
:apply-hooks="applyHooks"
|
||||
@finish="save"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<NameNsDescription
|
||||
:value="value"
|
||||
|
||||
+10
@@ -20,6 +20,10 @@ export default {
|
||||
type: String,
|
||||
default: 'create'
|
||||
},
|
||||
osType: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
@@ -64,6 +68,12 @@ export default {
|
||||
},
|
||||
|
||||
watch: {
|
||||
osType(neu) {
|
||||
if (neu === 'windows') {
|
||||
this.id = '';
|
||||
}
|
||||
},
|
||||
|
||||
value(neu) {
|
||||
this.yamlScript = neu;
|
||||
},
|
||||
|
||||
@@ -28,6 +28,10 @@ export default {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
osType: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
userScript: {
|
||||
type: String,
|
||||
default: ''
|
||||
@@ -203,6 +207,7 @@ export default {
|
||||
<DataTemplate
|
||||
ref="userTemplate"
|
||||
:value="userScript"
|
||||
:os-type="osType"
|
||||
type="user"
|
||||
:view-code="viewCode"
|
||||
:mode="mode"
|
||||
@@ -219,6 +224,7 @@ export default {
|
||||
ref="networkTemplate"
|
||||
:value="networkScript"
|
||||
type="network"
|
||||
:os-type="osType"
|
||||
:view-code="viewCode"
|
||||
:mode="mode"
|
||||
:config-id="configNetworkId"
|
||||
|
||||
+19
-6
@@ -8,6 +8,7 @@ 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';
|
||||
@@ -70,6 +71,7 @@ export default {
|
||||
return {
|
||||
GIBIBYTE,
|
||||
VOLUME_TYPE,
|
||||
HCI_ANNOTATIONS,
|
||||
InterfaceOption,
|
||||
loading: false,
|
||||
images: [],
|
||||
@@ -90,12 +92,23 @@ export default {
|
||||
},
|
||||
|
||||
imagesOption() {
|
||||
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
|
||||
};
|
||||
});
|
||||
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
|
||||
}));
|
||||
},
|
||||
|
||||
imageName() {
|
||||
|
||||
@@ -114,13 +114,11 @@ export default {
|
||||
},
|
||||
|
||||
machineTypeOptions() {
|
||||
return [{
|
||||
label: 'None',
|
||||
value: ''
|
||||
}, {
|
||||
label: 'q35',
|
||||
value: 'q35'
|
||||
}];
|
||||
return this.machineTypes.map((type) => {
|
||||
if (!type) return { label: 'None', value: '' };
|
||||
|
||||
return { label: type, value: type };
|
||||
});
|
||||
},
|
||||
|
||||
templateOptions() {
|
||||
@@ -257,6 +255,11 @@ export default {
|
||||
|
||||
cloneVersionVM.metadata.annotations[HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE] = JSON.stringify(deleteDataSource);
|
||||
|
||||
// Update labels, instance labels and annotations
|
||||
this.value.metadata.labels = cloneVersionVM.metadata.labels;
|
||||
this.value.spec.template.metadata.labels = cloneVersionVM.spec.template.metadata.labels;
|
||||
this.value.metadata.annotations = cloneVersionVM.metadata.annotations;
|
||||
|
||||
this.getInitConfig({
|
||||
value: cloneVersionVM, existUserData: true, fromTemplate: true
|
||||
});
|
||||
@@ -519,6 +522,7 @@ export default {
|
||||
:apply-hooks="applyHooks"
|
||||
@finish="saveVM"
|
||||
@cancel="cancelAction"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<RadioGroup
|
||||
v-if="isCreate"
|
||||
@@ -761,10 +765,31 @@ 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="-11"
|
||||
:weight="-12"
|
||||
>
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
@@ -862,6 +887,7 @@ export default {
|
||||
ref="yamlEditor"
|
||||
:user-script="userScript"
|
||||
:mode="mode"
|
||||
:os-type="osType"
|
||||
:view-code="isWindows"
|
||||
:namespace="value.metadata.namespace"
|
||||
:network-script="networkScript"
|
||||
|
||||
@@ -81,6 +81,7 @@ export default {
|
||||
:apply-hooks="applyHooks"
|
||||
:yaml-modifiers="yamlModifiers"
|
||||
@finish="save"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<NameNsDescription
|
||||
:value="value"
|
||||
|
||||
@@ -235,6 +235,7 @@ 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,6 +67,7 @@ export default {
|
||||
:mode="mode"
|
||||
:errors="errors"
|
||||
@finish="save"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<NameNsDescription
|
||||
:value="value"
|
||||
|
||||
@@ -373,6 +373,7 @@ export default {
|
||||
:mode="mode"
|
||||
:errors="errors"
|
||||
@finish="save"
|
||||
@error="e=>errors=e"
|
||||
>
|
||||
<NameNsDescription
|
||||
:value="value"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { NORMAN } from '@shell/config/types';
|
||||
import { HCI } from '../types';
|
||||
|
||||
export default {
|
||||
@@ -13,10 +14,24 @@ 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() {
|
||||
return { harvesterSettings: [] };
|
||||
const user = this.$store.getters['auth/v3User'];
|
||||
|
||||
return {
|
||||
harvesterSettings: [],
|
||||
clusterRoleTemplateBinding: [],
|
||||
user,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
@@ -31,19 +46,28 @@ 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 v-if="isMatch">
|
||||
{{ value }}
|
||||
</div>
|
||||
<div v-else>
|
||||
<div>
|
||||
{{ value }}
|
||||
<p
|
||||
v-if="value"
|
||||
v-if="shouldShowError"
|
||||
class="text-error"
|
||||
>
|
||||
{{ t('harvester.backup.matchTarget') }}
|
||||
|
||||
@@ -35,11 +35,26 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
// Return VM instance IP and VM annotation IP
|
||||
ips() {
|
||||
return [...this.vmiIp, ...this.networkAnnotationIP]
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => a.ip < b.ip ? -1 : 1);
|
||||
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 [];
|
||||
},
|
||||
|
||||
networkAnnotationIP() {
|
||||
@@ -97,12 +112,13 @@ export default {
|
||||
<template>
|
||||
<div v-if="showIP">
|
||||
<span
|
||||
v-for="{ip, name} in ips"
|
||||
:key="ip"
|
||||
v-for="{ ip, name, isCustom } in ips"
|
||||
:key="`${ip}-${name}`"
|
||||
>
|
||||
<CopyToClipboardText
|
||||
v-clean-tooltip="name"
|
||||
v-clean-tooltip="isCustom ? t('harvester.formatters.harvesterIpAddress.customIpTooltip') : name"
|
||||
:text="ip"
|
||||
:plain="isCustom"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -49,9 +49,13 @@ dialog:
|
||||
}
|
||||
protip: "Tip: Hold the {alternateLabel} key while clicking action to bypass this confirmation"
|
||||
stop:
|
||||
message: "Are you sure you want to continue stop the {type} {names}?"
|
||||
message: "Are you sure you want to stop the {type} {names}?"
|
||||
pause:
|
||||
message: "Are you sure you want to continue pause the {type} {names}?"
|
||||
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}?"
|
||||
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?"
|
||||
@@ -66,7 +70,6 @@ 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
|
||||
@@ -133,10 +136,24 @@ 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
|
||||
requiredDesc: Description is required!
|
||||
titleDescription: Collect system-related logs in Harvester to help with troubleshooting and support.
|
||||
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.
|
||||
hotplug:
|
||||
success: 'Volume { diskName } is mounted to the virtual machine { vm }.'
|
||||
title: Add Volume
|
||||
@@ -229,6 +246,8 @@ harvester:
|
||||
{count, plural,
|
||||
=1 {core}
|
||||
other {cores}}
|
||||
harvesterIpAddress:
|
||||
customIpTooltip: "Custom IP (set via annotation)"
|
||||
tableHeaders:
|
||||
imageEncryption: Encryption
|
||||
size: Size
|
||||
@@ -276,6 +295,7 @@ harvester:
|
||||
quotas: Quotas
|
||||
snapshots: Snapshots
|
||||
instanceLabel: Instance Labels
|
||||
annotations: Annotations
|
||||
fields:
|
||||
version: Version
|
||||
name: Name
|
||||
@@ -674,7 +694,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'
|
||||
@@ -754,6 +774,7 @@ harvester:
|
||||
dedicatedResources: Dedicated Resources
|
||||
down: Virtual machine not running
|
||||
affinityRules: Affinity Rules
|
||||
phase: Phase
|
||||
sourceNode: Source Node
|
||||
targetNode: Target Node
|
||||
started: Started
|
||||
@@ -791,6 +812,8 @@ 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
|
||||
@@ -855,6 +878,8 @@ 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
|
||||
@@ -891,7 +916,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
|
||||
@@ -1032,6 +1057,7 @@ 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'.
|
||||
@@ -1104,6 +1130,10 @@ 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:
|
||||
@@ -1587,7 +1617,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': Specify resources in other namespaces to be collected by the support package.
|
||||
'harv-support-bundle-namespaces': Select additional namespaces to include in the support bundle.
|
||||
'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.
|
||||
@@ -1597,6 +1627,7 @@ 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>'
|
||||
|
||||
typeLabel:
|
||||
kubevirt.io.virtualmachine: |-
|
||||
|
||||
@@ -85,7 +85,7 @@ export default {
|
||||
},
|
||||
|
||||
data() {
|
||||
return { initSettings: [] };
|
||||
return { initSettings: [], searchQuery: '' };
|
||||
},
|
||||
|
||||
computed: {
|
||||
@@ -116,7 +116,15 @@ export default {
|
||||
return {
|
||||
...s,
|
||||
description: isHarvester ? `advancedSettings.descriptions.harv-${ s.id }` : `advancedSettings.descriptions.${ s.id }`,
|
||||
customized: (!s.readOnly && s.data.value && s.data.value !== s.data.default) || s.data.hasCustomized
|
||||
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
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -135,9 +143,19 @@ 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-30"
|
||||
class="mt-15"
|
||||
>
|
||||
<Tab
|
||||
name="advanced"
|
||||
@@ -146,6 +164,7 @@ export default {
|
||||
>
|
||||
<Settings
|
||||
:settings="settings"
|
||||
:search-query="searchQuery"
|
||||
category="advanced"
|
||||
/>
|
||||
</Tab>
|
||||
@@ -156,6 +175,7 @@ export default {
|
||||
>
|
||||
<Settings
|
||||
:settings="settings"
|
||||
:search-query="searchQuery"
|
||||
category="ui"
|
||||
/>
|
||||
</Tab>
|
||||
@@ -167,4 +187,20 @@ 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>
|
||||
|
||||
@@ -90,6 +90,11 @@ 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 }}
|
||||
|
||||
@@ -157,6 +157,7 @@ export default {
|
||||
diskRows: [],
|
||||
networkRows: [],
|
||||
machineType: '',
|
||||
machineTypes: [],
|
||||
secretName: '',
|
||||
secretRef: null,
|
||||
showAdvanced: false,
|
||||
@@ -295,6 +296,20 @@ 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 });
|
||||
},
|
||||
|
||||
@@ -331,7 +346,8 @@ export default {
|
||||
const maintenanceStrategy = vm.metadata.labels?.[HCI_ANNOTATIONS.VM_MAINTENANCE_MODE_STRATEGY] || 'Migrate';
|
||||
|
||||
const runStrategy = spec.runStrategy || 'RerunOnFailure';
|
||||
const machineType = value.machineType;
|
||||
const machineType = spec.template.spec.domain?.machine?.type || this.machineTypes[0];
|
||||
|
||||
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];
|
||||
@@ -1533,6 +1549,7 @@ export default {
|
||||
if (val) {
|
||||
this['sshKey'] = [];
|
||||
this['userScript'] = undefined;
|
||||
this['networkScript'] = undefined;
|
||||
this['installAgent'] = false;
|
||||
}
|
||||
},
|
||||
@@ -1590,8 +1607,9 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
osType(neu) {
|
||||
const out = this.getUserData({ installAgent: this.installAgent, osType: neu });
|
||||
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 });
|
||||
|
||||
this['userScript'] = out;
|
||||
this.refreshYamlEditor();
|
||||
|
||||
@@ -109,15 +109,18 @@ export default class HciSetting extends HarvesterResource {
|
||||
}
|
||||
|
||||
get parseValue() {
|
||||
let parseDefaultValue = {};
|
||||
|
||||
try {
|
||||
parseDefaultValue = JSON.parse(this.value);
|
||||
if (this.value) {
|
||||
return JSON.parse(this.value);
|
||||
} else if (this.default) {
|
||||
return JSON.parse(this.default);
|
||||
}
|
||||
} catch (err) {
|
||||
parseDefaultValue = JSON.parse(this.default);
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to parse setting value or default:', err);
|
||||
}
|
||||
|
||||
return parseDefaultValue;
|
||||
return {};
|
||||
}
|
||||
|
||||
get isS3() {
|
||||
|
||||
@@ -43,6 +43,11 @@ 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;
|
||||
|
||||
@@ -52,40 +57,42 @@ 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'),
|
||||
}
|
||||
] : [];
|
||||
|
||||
let filteredOut;
|
||||
// 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));
|
||||
|
||||
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;
|
||||
return custom ? [custom, { divider: true }, ...filtered] : filtered;
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -206,6 +213,10 @@ 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]: '[]' } },
|
||||
metadata: { annotations: { [HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE]: '[]' }, labels: {} },
|
||||
spec: {
|
||||
runStrategy: 'RerunOnFailure',
|
||||
template: {
|
||||
metadata: { annotations: {} },
|
||||
metadata: { annotations: {}, labels: {} },
|
||||
spec: {
|
||||
domain: {
|
||||
machine: { type: '' },
|
||||
@@ -284,4 +284,10 @@ 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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,11 +131,12 @@ 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
|
||||
action: 'restartVM',
|
||||
enabled: !!this.actions?.restart,
|
||||
icon: 'icon icon-refresh',
|
||||
label: this.t('harvester.action.restart'),
|
||||
bulkable: true,
|
||||
bulkAction: 'restartVM'
|
||||
},
|
||||
{
|
||||
action: 'softrebootVM',
|
||||
@@ -233,7 +234,7 @@ export default class VirtVm extends HarvesterResource {
|
||||
const spec = {
|
||||
runStrategy: 'RerunOnFailure',
|
||||
template: {
|
||||
metadata: { annotations: {} },
|
||||
metadata: { annotations: {}, labels: {} },
|
||||
spec: {
|
||||
domain: {
|
||||
machine: { type: '' },
|
||||
@@ -283,6 +284,7 @@ export default class VirtVm extends HarvesterResource {
|
||||
|
||||
if (realMode !== _CLONE) {
|
||||
this.metadata['annotations'] = { [HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE]: '[]' };
|
||||
this.metadata['labels'] = {};
|
||||
this['spec'] = spec;
|
||||
}
|
||||
}
|
||||
@@ -311,12 +313,22 @@ export default class VirtVm extends HarvesterResource {
|
||||
this.metadata.annotations[HCI_ANNOTATIONS.VOLUME_CLAIM_TEMPLATE] = JSON.stringify(deleteDataSource);
|
||||
}
|
||||
|
||||
restartVM() {
|
||||
this.doActionGrowl('restart', {});
|
||||
restartVM(resources = this) {
|
||||
this.$dispatch('promptModal', {
|
||||
resources,
|
||||
action: 'restart',
|
||||
warningMessageKey: 'dialog.confirmExecution.restart.message',
|
||||
component: 'ConfirmExecutionDialog'
|
||||
});
|
||||
}
|
||||
|
||||
softrebootVM() {
|
||||
this.doActionGrowl('softreboot', {});
|
||||
softrebootVM(resources = this) {
|
||||
this.$dispatch('promptModal', {
|
||||
resources,
|
||||
action: 'softreboot',
|
||||
warningMessageKey: 'dialog.confirmExecution.softreboot.message',
|
||||
component: 'ConfirmExecutionDialog'
|
||||
});
|
||||
}
|
||||
|
||||
openLogs() {
|
||||
@@ -1205,6 +1217,10 @@ 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,7 +1,7 @@
|
||||
{
|
||||
"name": "harvester",
|
||||
"description": "Rancher UI Extension for Harvester",
|
||||
"version": "1.5.2-rc2",
|
||||
"version": "1.6.0-rc1",
|
||||
"private": false,
|
||||
"rancher": {
|
||||
"annotations": {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
<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, t)"></span>
|
||||
<span v-clean-html="resourceNames(names, null, t)"></span>
|
||||
|
||||
<div class="mt-10">
|
||||
{{ t('harvester.virtualMachine.promptRemove.title') }}
|
||||
|
||||
@@ -8,6 +8,7 @@ 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;
|
||||
}
|
||||
@@ -17,21 +18,20 @@ export function getVersion(v) {
|
||||
return `v${ semver.major(v) }.${ semver.minor(v) }.${ semver.patch(v) }`;
|
||||
} catch (error) {
|
||||
// fallback to the latest version
|
||||
return getLatestVersion();
|
||||
return latestVersion(Object.keys(RELEASE_FEATURES));
|
||||
}
|
||||
}
|
||||
|
||||
function getLatestVersion() {
|
||||
const allVersions = Object.keys(RELEASE_FEATURES).filter(semver.valid).sort(semver.rcompare);
|
||||
|
||||
return allVersions[0] || '';
|
||||
function latestVersion(versions) {
|
||||
return versions.sort((a, b) => semver.compare(a, b)).pop();
|
||||
}
|
||||
|
||||
function getLatestCompatibleVersion(version) {
|
||||
const allVersions = Object.keys(RELEASE_FEATURES).filter(semver.valid);
|
||||
const compatible = allVersions.filter((v) => semver.lte(v, version)).sort(semver.rcompare);
|
||||
// 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));
|
||||
|
||||
return compatible[0] || getLatestVersion();
|
||||
return latestVersion(minorVersions);
|
||||
}
|
||||
|
||||
export const featureEnabled = (featureKey, serverVersion) => {
|
||||
@@ -46,16 +46,12 @@ export const featureEnabled = (featureKey, serverVersion) => {
|
||||
return false;
|
||||
}
|
||||
|
||||
const compatibleVersion = getLatestCompatibleVersion(version);
|
||||
const releasedFeatures = RELEASE_FEATURES?.[compatibleVersion];
|
||||
let releasedFeatures = RELEASE_FEATURES[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.`
|
||||
);
|
||||
if (!releasedFeatures) {
|
||||
const fallback = latestMinorVersion(version);
|
||||
|
||||
return false;
|
||||
releasedFeatures = RELEASE_FEATURES[fallback];
|
||||
}
|
||||
|
||||
return releasedFeatures.includes(featureKey);
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
#!/bin/bash
|
||||
TAG_VERSION=$1
|
||||
|
||||
if [[ -z "$TAG_VERSION" ]]; then
|
||||
echo "No tag version provided"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION=$(jq -r .version package.json)
|
||||
EXTENSION_VERSION=$(jq -r .version pkg/harvester/package.json)
|
||||
PKG_VERSION="v${EXTENSION_VERSION}"
|
||||
|
||||
if [[ "$VERSION" != "$EXTENSION_VERSION" ]]; then
|
||||
echo "Package version mismatch: $VERSION vs $EXTENSION_VERSION"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${TAG_VERSION}" != "$PKG_VERSION" ]]; then
|
||||
echo "Package version $PKG_VERSION in pkg/harvester/package.json does not match tag version ${TAG_VERSION}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Package version and tag version match: ${TAG_VERSION}"
|
||||
Reference in New Issue
Block a user