mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-03 20:56:06 +08:00
Compare commits
16
Commits
v1.9.0-rc2
...
v1.9.0-rc3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8815c70361 | ||
|
|
eb4c0504c8 | ||
|
|
82e7b0da19 | ||
|
|
92f34fa64f | ||
|
|
6039f21320 | ||
|
|
3cbaf97399 | ||
|
|
bcde1744e1 | ||
|
|
e991c9bcc8 | ||
|
|
6e709bc00f | ||
|
|
20d40860ce | ||
|
|
1496c17f3d | ||
|
|
e99e4ef34f | ||
|
|
73e022172b | ||
|
|
7e86c9fae0 | ||
|
|
b4b49222c3 | ||
|
|
b937a2147b |
@@ -1,14 +1,19 @@
|
|||||||
<!-- This template is for Devs to give QA details before moving the issue To-Test -->
|
|
||||||
### Summary
|
### Summary
|
||||||
|
<!-- Give the fix root cause or feature requirement, at least list what this PR changes -->
|
||||||
|
|
||||||
### PR Checklists
|
|
||||||
|
### PR Checklist
|
||||||
|
<!-- Check Yes if there is backend PR related to this UI PR, tag the backend owner's Github account -->
|
||||||
- Are backend engineers aware of UI changes ?
|
- Are backend engineers aware of UI changes ?
|
||||||
- [ ] Yes, the backend owner is:
|
- [ ] Yes, the backend owner is @
|
||||||
|
- [ ] No, frontend-only.
|
||||||
|
|
||||||
### Related Issue #
|
### Related Issue #
|
||||||
<!-- Define findings related to the feature or bug issue. -->
|
<!-- Link the issue as harvester/harvester#<issue_number> -->
|
||||||
|
harvester/harvester#<issue_number>
|
||||||
|
|
||||||
### Test screenshot or video
|
|
||||||
|
### Test screenshot or video (Required)
|
||||||
<!-- Attach screenshot or video of the changes and eventual comparison if you find it necessary -->
|
<!-- Attach screenshot or video of the changes and eventual comparison if you find it necessary -->
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
> This `./AGENTS.md` file is generated by running `yarn agents:generate`.
|
> This `./AGENTS.md` file is generated by running `yarn agents:generate`. `CLAUDE.md` and `GEMINI.md` are symlinks to this file so Claude Code, GitHub Copilot and Gemini all read the same content.
|
||||||
|
|
||||||
|
|
||||||
# Project Overview
|
# Project Overview
|
||||||
@@ -28,9 +28,7 @@ You are an expert Senior Software Engineer specializing in Vue.js and TypeScript
|
|||||||
- Run `yarn lint:fix` before commits.
|
- Run `yarn lint:fix` before commits.
|
||||||
- Use conventional commit format:
|
- Use conventional commit format:
|
||||||
```
|
```
|
||||||
<type>:
|
<type>: <description>
|
||||||
|
|
||||||
<description>
|
|
||||||
```
|
```
|
||||||
- Follow existing naming conventions (PascalCase for components, camelCase for functions).
|
- Follow existing naming conventions (PascalCase for components, camelCase for functions).
|
||||||
- After changing a Vue, JS, or TS file, make sure it's automatically formatted with ESLint.
|
- After changing a Vue, JS, or TS file, make sure it's automatically formatted with ESLint.
|
||||||
@@ -130,8 +128,8 @@ To get started, follow the `Development Setup` section.
|
|||||||
- `Vue.js`:
|
- `Vue.js`:
|
||||||
- Composition API components are preferred over Options API.
|
- Composition API components are preferred over Options API.
|
||||||
- Large pages with lots of code and styles should be avoided by breaking the page up into smaller Vue components.
|
- Large pages with lots of code and styles should be avoided by breaking the page up into smaller Vue components.
|
||||||
- Place source tag above template above style.
|
- Order the Single File Component blocks as `<script>`, then `<template>`, then `<style>`.
|
||||||
- style tag should contain `lang='scss' scoped`.
|
- The `<style>` tag should include `lang="scss" scoped`.
|
||||||
- `Linting`: Follow the ESLint configuration in the root.
|
- `Linting`: Follow the ESLint configuration in the root.
|
||||||
|
|
||||||
- **File Structure:**
|
- **File Structure:**
|
||||||
@@ -163,24 +161,26 @@ To get started, follow the `Development Setup` section.
|
|||||||
|
|
||||||
## Harvester UI Extension Development Guide
|
## Harvester UI Extension Development Guide
|
||||||
|
|
||||||
1. Backward Compatibility
|
### Backward Compatibility
|
||||||
The Harvester UI Extension supports earlier cluster versions (e.g., UI Ext v1.8.0 works with clusters v1.7.0 and v1.6.0). It uses Feature Flags defined in pkg/config/feature-flags to ensure the UI matches the cluster's specific version.
|
|
||||||
|
The Harvester UI Extension supports earlier cluster versions (e.g., UI Ext v1.8.0 works with clusters v1.7.0 and v1.6.0). It uses Feature Flags defined in `pkg/harvester/config/` to ensure the UI matches the cluster's specific version.
|
||||||
|
|
||||||
|
### Implementation Steps for New Features
|
||||||
|
|
||||||
2. Implementation Steps for New Features
|
|
||||||
To add a feature in a new release, follow these steps:
|
To add a feature in a new release, follow these steps:
|
||||||
|
|
||||||
Register: Add a unique [Feature Name] to the corresponding release array in the configuration.
|
1. **Register**: Add a unique `[Feature Name]` to the corresponding release array in the configuration.
|
||||||
|
2. **Check**: Use the following getter to verify if the feature is enabled for the current version:
|
||||||
|
|
||||||
Check: Use the following getter to verify if the feature is enabled for the current version:
|
```js
|
||||||
|
computed: {
|
||||||
|
newFeatureEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('[Feature Name]');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
```
|
3. **Render**: Use the result of the check to conditionally render the UI components.
|
||||||
computed: {
|
|
||||||
newFeatureEnabled() {
|
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('[Feature Name]');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
```
|
|
||||||
Render: Use the result of the check to conditionally render the UI components.
|
|
||||||
|
|
||||||
|
|
||||||
## E2E Tests (Cypress)
|
## E2E Tests (Cypress)
|
||||||
@@ -228,7 +228,7 @@ For the Renovate config, see `.github/renovate.json`.
|
|||||||
### Commit Message Format
|
### Commit Message Format
|
||||||
All commit messages must follow the conventional commit format:
|
All commit messages must follow the conventional commit format:
|
||||||
```
|
```
|
||||||
<type>[optional scope]: <description>
|
<type>: <description>
|
||||||
|
|
||||||
[optional body]
|
[optional body]
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ For Harvester UI extension installation instructions, please refer to the page *
|
|||||||
|
|
||||||
## Development Setup
|
## Development Setup
|
||||||
|
|
||||||
Ensure **Node.js v20 or later** is installed for development and debugging.
|
Ensure **Node.js v24 or later** is installed for development and debugging.
|
||||||
|
|
||||||
### Standalone Mode
|
### Standalone Mode
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ module.exports = {
|
|||||||
'subject-empty': [2, 'never'],
|
'subject-empty': [2, 'never'],
|
||||||
'subject-full-stop': [2, 'never', '.'],
|
'subject-full-stop': [2, 'never', '.'],
|
||||||
'subject-max-length': [0, 'never'],
|
'subject-max-length': [0, 'never'],
|
||||||
|
'header-max-length': [0, 'always', 100],
|
||||||
'body-leading-blank': [2, 'always'],
|
'body-leading-blank': [2, 'always'],
|
||||||
'body-max-line-length': [0, 'always', 100],
|
'body-max-line-length': [0, 'always', 100],
|
||||||
'footer-leading-blank': [2, 'always'],
|
'footer-leading-blank': [2, 'always'],
|
||||||
|
|||||||
@@ -6,9 +6,7 @@
|
|||||||
- Run `yarn lint:fix` before commits.
|
- Run `yarn lint:fix` before commits.
|
||||||
- Use conventional commit format:
|
- Use conventional commit format:
|
||||||
```
|
```
|
||||||
<type>:
|
<type>: <description>
|
||||||
|
|
||||||
<description>
|
|
||||||
```
|
```
|
||||||
- Follow existing naming conventions (PascalCase for components, camelCase for functions).
|
- Follow existing naming conventions (PascalCase for components, camelCase for functions).
|
||||||
- After changing a Vue, JS, or TS file, make sure it's automatically formatted with ESLint.
|
- After changing a Vue, JS, or TS file, make sure it's automatically formatted with ESLint.
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ To get started, follow the `Development Setup` section.
|
|||||||
- `Vue.js`:
|
- `Vue.js`:
|
||||||
- Composition API components are preferred over Options API.
|
- Composition API components are preferred over Options API.
|
||||||
- Large pages with lots of code and styles should be avoided by breaking the page up into smaller Vue components.
|
- Large pages with lots of code and styles should be avoided by breaking the page up into smaller Vue components.
|
||||||
- Place source tag above template above style.
|
- Order the Single File Component blocks as `<script>`, then `<template>`, then `<style>`.
|
||||||
- style tag should contain `lang='scss' scoped`.
|
- The `<style>` tag should include `lang="scss" scoped`.
|
||||||
- `Linting`: Follow the ESLint configuration in the root.
|
- `Linting`: Follow the ESLint configuration in the root.
|
||||||
|
|
||||||
- **File Structure:**
|
- **File Structure:**
|
||||||
@@ -49,22 +49,24 @@ To get started, follow the `Development Setup` section.
|
|||||||
|
|
||||||
## Harvester UI Extension Development Guide
|
## Harvester UI Extension Development Guide
|
||||||
|
|
||||||
1. Backward Compatibility
|
### Backward Compatibility
|
||||||
The Harvester UI Extension supports earlier cluster versions (e.g., UI Ext v1.8.0 works with clusters v1.7.0 and v1.6.0). It uses Feature Flags defined in pkg/config/feature-flags to ensure the UI matches the cluster's specific version.
|
|
||||||
|
The Harvester UI Extension supports earlier cluster versions (e.g., UI Ext v1.8.0 works with clusters v1.7.0 and v1.6.0). It uses Feature Flags defined in `pkg/harvester/config/` to ensure the UI matches the cluster's specific version.
|
||||||
|
|
||||||
|
### Implementation Steps for New Features
|
||||||
|
|
||||||
2. Implementation Steps for New Features
|
|
||||||
To add a feature in a new release, follow these steps:
|
To add a feature in a new release, follow these steps:
|
||||||
|
|
||||||
Register: Add a unique [Feature Name] to the corresponding release array in the configuration.
|
1. **Register**: Add a unique `[Feature Name]` to the corresponding release array in the configuration.
|
||||||
|
2. **Check**: Use the following getter to verify if the feature is enabled for the current version:
|
||||||
|
|
||||||
Check: Use the following getter to verify if the feature is enabled for the current version:
|
```js
|
||||||
|
computed: {
|
||||||
|
newFeatureEnabled() {
|
||||||
|
return this.$store.getters['harvester-common/getFeatureEnabled']('[Feature Name]');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
```
|
3. **Render**: Use the result of the check to conditionally render the UI components.
|
||||||
computed: {
|
|
||||||
newFeatureEnabled() {
|
|
||||||
return this.$store.getters['harvester-common/getFeatureEnabled']('[Feature Name]');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
```
|
|
||||||
Render: Use the result of the check to conditionally render the UI components.
|
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
### Commit Message Format
|
### Commit Message Format
|
||||||
All commit messages must follow the conventional commit format:
|
All commit messages must follow the conventional commit format:
|
||||||
```
|
```
|
||||||
<type>[optional scope]: <description>
|
<type>: <description>
|
||||||
|
|
||||||
[optional body]
|
[optional body]
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
> This `./AGENTS.md` file is generated by running `yarn agents:generate`.
|
> This `./AGENTS.md` file is generated by running `yarn agents:generate`. `CLAUDE.md` and `GEMINI.md` are symlinks to this file so Claude Code, GitHub Copilot and Gemini all read the same content.
|
||||||
|
|
||||||
|
|
||||||
# Project Overview
|
# Project Overview
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "harvester-ui-extension",
|
"name": "harvester-ui-extension",
|
||||||
"version": "1.9.0-rc2",
|
"version": "1.9.0-rc3",
|
||||||
"private": false,
|
"private": false,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=24.0.0"
|
"node": ">=24.0.0"
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
"yaml": "^2.5.1"
|
"yaml": "^2.5.1"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@types/node": "25.9.4",
|
"@types/node": "25.9.5",
|
||||||
"cronstrue": "2.59.0",
|
"cronstrue": "2.59.0",
|
||||||
"d3-color": "3.1.0",
|
"d3-color": "3.1.0",
|
||||||
"ejs": "3.1.10",
|
"ejs": "3.1.10",
|
||||||
|
|||||||
@@ -55,8 +55,12 @@ export default {
|
|||||||
!this.latestUpgrade.isUpgradeFailed;
|
!this.latestUpgrade.isUpgradeFailed;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
canUpgrade() {
|
||||||
|
return this.versionOptions.length > 0 && !this.isUpgradeInProgress;
|
||||||
|
},
|
||||||
|
|
||||||
versionOptions() {
|
versionOptions() {
|
||||||
const versions = this.$store.getters['harvester/all'](HCI.VERSION);
|
const versions = this.$store.getters['harvester/all'](HCI.VERSION) || [];
|
||||||
|
|
||||||
return versions.map((V) => V.metadata.name);
|
return versions.map((V) => V.metadata.name);
|
||||||
},
|
},
|
||||||
@@ -154,14 +158,23 @@ export default {
|
|||||||
:cluster="currentCluster.nameDisplay"
|
:cluster="currentCluster.nameDisplay"
|
||||||
/>
|
/>
|
||||||
</h1>
|
</h1>
|
||||||
<button
|
<span
|
||||||
v-if="versionOptions.length && !isUpgradeInProgress"
|
v-clean-tooltip="{
|
||||||
type="button"
|
content: canUpgrade ? t('harvester.upgradePage.upgradeAvailable') : t('harvester.upgradePage.upgradeUnavailable'),
|
||||||
class="btn bg-warning btn-sm"
|
triggers: ['hover', 'focus', 'touch'],
|
||||||
@click="open"
|
}"
|
||||||
|
class="upgrade-btn-wrap"
|
||||||
|
:tabindex="canUpgrade ? -1 : 0"
|
||||||
>
|
>
|
||||||
<t k="harvester.upgradePage.upgrade" />
|
<button
|
||||||
</button>
|
:disabled="!canUpgrade"
|
||||||
|
type="button"
|
||||||
|
class="btn bg-warning btn-sm"
|
||||||
|
@click="open"
|
||||||
|
>
|
||||||
|
<t k="harvester.upgradePage.upgrade" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<ModalWithCard
|
<ModalWithCard
|
||||||
@@ -296,6 +309,18 @@ export default {
|
|||||||
line-height: 23px;
|
line-height: 23px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.upgrade-btn-wrap {
|
||||||
|
display: inline-flex;
|
||||||
|
|
||||||
|
button {
|
||||||
|
color: #000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
color: #000 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.currentVersion {
|
.currentVersion {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import Loading from '@shell/components/Loading';
|
|||||||
import { Banner } from '@components/Banner';
|
import { Banner } from '@components/Banner';
|
||||||
import { RcItemCard } from '@components/RcItemCard';
|
import { RcItemCard } from '@components/RcItemCard';
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
import MappingsCell from '../MappingsCell';
|
import MappingsCell from '../MappingsCell';
|
||||||
import { useI18n } from '@shell/composables/useI18n';
|
import { useI18n } from '@shell/composables/useI18n';
|
||||||
|
import { NAMESPACE } from '@shell/config/types';
|
||||||
import { HCI } from '../../types';
|
import { HCI } from '../../types';
|
||||||
import { FORKLIFT_NAMESPACE } from '../../config/harvester-map';
|
import { FORKLIFT_NAMESPACE } from '../../config/harvester-map';
|
||||||
import {
|
import {
|
||||||
@@ -30,20 +32,41 @@ const vms = ref([]);
|
|||||||
const networkMappings = ref([]);
|
const networkMappings = ref([]);
|
||||||
const storageMappings = ref([]);
|
const storageMappings = ref([]);
|
||||||
const planName = ref('');
|
const planName = ref('');
|
||||||
|
const targetNamespace = ref('');
|
||||||
|
const namespaceOptions = ref([]);
|
||||||
const errors = ref([]);
|
const errors = ref([]);
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
|
|
||||||
// Restore persisted state
|
// Restore persisted state
|
||||||
planName.value = props.stepData.planName;
|
planName.value = props.stepData.planName;
|
||||||
|
targetNamespace.value = props.stepData.targetNamespace || '';
|
||||||
|
|
||||||
const NAMESPACE = FORKLIFT_NAMESPACE;
|
const NS = FORKLIFT_NAMESPACE;
|
||||||
const TARGET_NAMESPACE = 'default';
|
|
||||||
|
|
||||||
watch(planName, (val) => {
|
watch(planName, (val) => {
|
||||||
props.stepData.planName = val;
|
props.stepData.planName = val;
|
||||||
emit('ready', !!val);
|
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
|
watch(targetNamespace, (val) => {
|
||||||
|
props.stepData.targetNamespace = val;
|
||||||
|
});
|
||||||
|
|
||||||
|
watch([planName, targetNamespace], ([name, namespace]) => {
|
||||||
|
emit('ready', !!name && !!namespace);
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
const formatNetworkTarget = (target = '') => {
|
||||||
|
if (target === 'pod') {
|
||||||
|
return t('harvester.addons.vmMigration.configureMappings.networkMapping.options.podNetworking');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target === 'ignored') {
|
||||||
|
return t('harvester.addons.vmMigration.generic.ignored');
|
||||||
|
}
|
||||||
|
|
||||||
|
return target;
|
||||||
|
};
|
||||||
|
|
||||||
const totalVCpu = computed(() => vms.value.reduce((sum, vm) => sum + (vm.cpuCount || vm.numCPU || 0), 0));
|
const totalVCpu = computed(() => vms.value.reduce((sum, vm) => sum + (vm.cpuCount || vm.numCPU || 0), 0));
|
||||||
|
|
||||||
const totalMemoryGB = computed(() => {
|
const totalMemoryGB = computed(() => {
|
||||||
@@ -103,13 +126,13 @@ const startMigrationAction = async() => {
|
|||||||
apiVersion: FORKLIFT_API_VERSION,
|
apiVersion: FORKLIFT_API_VERSION,
|
||||||
kind: 'Provider',
|
kind: 'Provider',
|
||||||
name: props.providerName,
|
name: props.providerName,
|
||||||
namespace: NAMESPACE,
|
namespace: NS,
|
||||||
},
|
},
|
||||||
destination: {
|
destination: {
|
||||||
apiVersion: FORKLIFT_API_VERSION,
|
apiVersion: FORKLIFT_API_VERSION,
|
||||||
kind: 'Provider',
|
kind: 'Provider',
|
||||||
name: 'host',
|
name: 'host',
|
||||||
namespace: NAMESPACE,
|
namespace: NS,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -118,15 +141,15 @@ const startMigrationAction = async() => {
|
|||||||
|
|
||||||
const networkMap = await store.dispatch(`${ inStore }/create`, {
|
const networkMap = await store.dispatch(`${ inStore }/create`, {
|
||||||
type: HCI.FORKLIFT_NETWORK_MAP,
|
type: HCI.FORKLIFT_NETWORK_MAP,
|
||||||
metadata: { name: networkMapName, namespace: NAMESPACE },
|
metadata: { name: networkMapName, namespace: NS },
|
||||||
spec: { map: buildNetworkMapEntries(props.mappingEntries?.networkEntries || [], NAMESPACE), provider: providerRef },
|
spec: { map: buildNetworkMapEntries(props.mappingEntries?.networkEntries || [], NS), provider: providerRef },
|
||||||
});
|
});
|
||||||
|
|
||||||
await networkMap.save();
|
await networkMap.save();
|
||||||
|
|
||||||
const storageMap = await store.dispatch(`${ inStore }/create`, {
|
const storageMap = await store.dispatch(`${ inStore }/create`, {
|
||||||
type: HCI.FORKLIFT_STORAGE_MAP,
|
type: HCI.FORKLIFT_STORAGE_MAP,
|
||||||
metadata: { name: storageMapName, namespace: NAMESPACE },
|
metadata: { name: storageMapName, namespace: NS },
|
||||||
spec: { map: buildStorageMapEntries(props.mappingEntries?.storageEntries || []), provider: providerRef },
|
spec: { map: buildStorageMapEntries(props.mappingEntries?.storageEntries || []), provider: providerRef },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -134,7 +157,7 @@ const startMigrationAction = async() => {
|
|||||||
|
|
||||||
const plan = await store.dispatch(`${ inStore }/create`, {
|
const plan = await store.dispatch(`${ inStore }/create`, {
|
||||||
type: HCI.FORKLIFT_PLAN,
|
type: HCI.FORKLIFT_PLAN,
|
||||||
metadata: { name: planName.value, namespace: NAMESPACE },
|
metadata: { name: planName.value, namespace: NS },
|
||||||
spec: {
|
spec: {
|
||||||
provider: providerRef,
|
provider: providerRef,
|
||||||
map: {
|
map: {
|
||||||
@@ -142,16 +165,16 @@ const startMigrationAction = async() => {
|
|||||||
apiVersion: FORKLIFT_API_VERSION,
|
apiVersion: FORKLIFT_API_VERSION,
|
||||||
kind: 'NetworkMap',
|
kind: 'NetworkMap',
|
||||||
name: networkMapName,
|
name: networkMapName,
|
||||||
namespace: NAMESPACE,
|
namespace: NS,
|
||||||
},
|
},
|
||||||
storage: {
|
storage: {
|
||||||
apiVersion: FORKLIFT_API_VERSION,
|
apiVersion: FORKLIFT_API_VERSION,
|
||||||
kind: 'StorageMap',
|
kind: 'StorageMap',
|
||||||
name: storageMapName,
|
name: storageMapName,
|
||||||
namespace: NAMESPACE,
|
namespace: NS,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
targetNamespace: TARGET_NAMESPACE,
|
targetNamespace: targetNamespace.value,
|
||||||
vms: vms.value.map((vm) => ({ id: vm.id, name: vm.name || vm.id })),
|
vms: vms.value.map((vm) => ({ id: vm.id, name: vm.name || vm.id })),
|
||||||
warm: false,
|
warm: false,
|
||||||
},
|
},
|
||||||
@@ -177,9 +200,19 @@ const startMigrationAction = async() => {
|
|||||||
await plan.startMigration();
|
await plan.startMigration();
|
||||||
};
|
};
|
||||||
|
|
||||||
const init = () => {
|
const init = async() => {
|
||||||
vms.value = props.selectedVms;
|
vms.value = props.selectedVms;
|
||||||
|
|
||||||
|
const inStore = store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const namespaces = await store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE });
|
||||||
|
|
||||||
|
namespaceOptions.value = namespaces.filter((ns) => !ns.isSystem).map((ns) => ns.metadata.name).sort().map((name) => ({ label: name, value: name }));
|
||||||
|
} catch (e) {
|
||||||
|
namespaceOptions.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
if (props.mappingEntries) {
|
if (props.mappingEntries) {
|
||||||
networkMappings.value = (props.mappingEntries.networkEntries || []).map((entry) => ({
|
networkMappings.value = (props.mappingEntries.networkEntries || []).map((entry) => ({
|
||||||
source: entry.name || entry.id,
|
source: entry.name || entry.id,
|
||||||
@@ -216,9 +249,18 @@ defineExpose({ startMigration: startMigrationAction });
|
|||||||
<h3 class="section-title m-0">
|
<h3 class="section-title m-0">
|
||||||
{{ t('harvester.addons.vmMigration.reviewMigration.migrationDetails') }}
|
{{ t('harvester.addons.vmMigration.reviewMigration.migrationDetails') }}
|
||||||
</h3>
|
</h3>
|
||||||
<div class="span-9">
|
<div class="name-row">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="targetNamespace"
|
||||||
|
class="namespace-select"
|
||||||
|
:label="t('harvester.addons.vmMigration.reviewMigration.targetNamespace')"
|
||||||
|
:placeholder="t('harvester.addons.vmMigration.reviewMigration.targetNamespacePlaceholder')"
|
||||||
|
:options="namespaceOptions"
|
||||||
|
required
|
||||||
|
/>
|
||||||
<LabeledInput
|
<LabeledInput
|
||||||
v-model:value="planName"
|
v-model:value="planName"
|
||||||
|
class="name-input"
|
||||||
:label="t('harvester.addons.vmMigration.reviewMigration.planName')"
|
:label="t('harvester.addons.vmMigration.reviewMigration.planName')"
|
||||||
:placeholder="t('harvester.addons.vmMigration.reviewMigration.planNamePlaceholder')"
|
:placeholder="t('harvester.addons.vmMigration.reviewMigration.planNamePlaceholder')"
|
||||||
required
|
required
|
||||||
@@ -244,7 +286,7 @@ defineExpose({ startMigration: startMigrationAction });
|
|||||||
<span class="detail-label">{{ t('harvester.addons.vmMigration.reviewMigration.targetNamespace') }}</span>
|
<span class="detail-label">{{ t('harvester.addons.vmMigration.reviewMigration.targetNamespace') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-item">
|
<div class="detail-item">
|
||||||
<span class="detail-value">{{ TARGET_NAMESPACE }}</span>
|
<span class="detail-value">{{ targetNamespace }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-column">
|
<div class="detail-column">
|
||||||
@@ -301,7 +343,7 @@ defineExpose({ startMigration: startMigrationAction });
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<MappingsCell
|
<MappingsCell
|
||||||
:network-entries="vm.networkMappings.map(m => `${m.source} → ${m.target}`)"
|
:network-entries="vm.networkMappings.map(m => `${m.source} → ${formatNetworkTarget(m.target)}`)"
|
||||||
:storage-entries="vm.storageMappings.map(m => `${m.source} → ${m.target}`)"
|
:storage-entries="vm.storageMappings.map(m => `${m.source} → ${m.target}`)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -347,6 +389,22 @@ defineExpose({ startMigration: startMigrationAction });
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.name-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.namespace-select {
|
||||||
|
max-width: 280px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.details-grid {
|
.details-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(230px, 1fr) 1fr 1fr;
|
grid-template-columns: minmax(230px, 1fr) 1fr 1fr;
|
||||||
|
|||||||
@@ -86,4 +86,5 @@ export const HCI = {
|
|||||||
CDI_POPULATOR_KIND: 'cdi.kubevirt.io/storage.populator.kind',
|
CDI_POPULATOR_KIND: 'cdi.kubevirt.io/storage.populator.kind',
|
||||||
CNI_NETWORKS: 'k8s.v1.cni.cncf.io/networks',
|
CNI_NETWORKS: 'k8s.v1.cni.cncf.io/networks',
|
||||||
WINDOWS_SYSPREP: 'harvesterhci.io/windows-sysprep',
|
WINDOWS_SYSPREP: 'harvesterhci.io/windows-sysprep',
|
||||||
|
SKIP_MIG_CONFIGURATION: 'harvesterhci.io/skip-mig-configuration',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -286,7 +286,7 @@ export const FORKLIFT_PLAN_VM_COUNT = {
|
|||||||
labelKey: 'harvester.tableHeaders.vmMigrationPlanVmCount',
|
labelKey: 'harvester.tableHeaders.vmMigrationPlanVmCount',
|
||||||
value: 'spec.vms.length',
|
value: 'spec.vms.length',
|
||||||
sort: 'spec.vms.length',
|
sort: 'spec.vms.length',
|
||||||
align: 'left',
|
align: 'center',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Plan reference column in forklift.konveyor.io.migration list page
|
// Plan reference column in forklift.konveyor.io.migration list page
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import { Card } from '@components/Card';
|
||||||
|
import AsyncButton from '@shell/components/AsyncButton';
|
||||||
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
|
import { escapeHtml } from '@shell/utils/string';
|
||||||
|
import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HarvesterEnableSriovGpuDevice',
|
||||||
|
|
||||||
|
emits: ['close'],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
AsyncButton,
|
||||||
|
Card,
|
||||||
|
Checkbox,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
resources: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return { skipMigConfiguration: this.resources[0]?.metadata?.annotations?.[HCI_ANNOTATIONS.SKIP_MIG_CONFIGURATION] === 'true' };
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: { ...mapGetters({ t: 'i18n/t' }) },
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
close() {
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
|
||||||
|
async save(buttonCb) {
|
||||||
|
const actionResource = this.resources[0];
|
||||||
|
const prevEnabled = actionResource.spec.enabled;
|
||||||
|
const prevAnnotation = actionResource.metadata.annotations?.[HCI_ANNOTATIONS.SKIP_MIG_CONFIGURATION];
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (this.skipMigConfiguration) {
|
||||||
|
if (!actionResource.metadata.annotations) {
|
||||||
|
actionResource.metadata.annotations = {};
|
||||||
|
}
|
||||||
|
actionResource.metadata.annotations[HCI_ANNOTATIONS.SKIP_MIG_CONFIGURATION] = 'true';
|
||||||
|
} else {
|
||||||
|
if (actionResource.metadata.annotations) {
|
||||||
|
delete actionResource.metadata.annotations[HCI_ANNOTATIONS.SKIP_MIG_CONFIGURATION];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actionResource.spec.enabled = true;
|
||||||
|
await actionResource.save();
|
||||||
|
buttonCb(true);
|
||||||
|
this.close();
|
||||||
|
} catch (err) {
|
||||||
|
actionResource.spec.enabled = prevEnabled;
|
||||||
|
if (prevAnnotation !== undefined) {
|
||||||
|
actionResource.metadata.annotations[HCI_ANNOTATIONS.SKIP_MIG_CONFIGURATION] = prevAnnotation;
|
||||||
|
} else if (actionResource.metadata.annotations) {
|
||||||
|
delete actionResource.metadata.annotations[HCI_ANNOTATIONS.SKIP_MIG_CONFIGURATION];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$store.dispatch('growl/fromError', {
|
||||||
|
title: this.t('generic.notification.title.error', { name: escapeHtml(actionResource.metadata.name) }),
|
||||||
|
err,
|
||||||
|
}, { root: true });
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card :show-highlight-border="false">
|
||||||
|
<template #title>
|
||||||
|
<h4
|
||||||
|
v-clean-html="t('harvester.sriovgpu.enable.title')"
|
||||||
|
class="text-default-text"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<div class="body">
|
||||||
|
<Checkbox
|
||||||
|
v-model:value="skipMigConfiguration"
|
||||||
|
:label="t('harvester.sriovgpu.enable.skipMigConfiguration')"
|
||||||
|
/>
|
||||||
|
<p class="note mt-10">
|
||||||
|
{{ t('harvester.sriovgpu.enable.note') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #actions>
|
||||||
|
<div class="buttons actions">
|
||||||
|
<button
|
||||||
|
class="btn role-secondary mr-10"
|
||||||
|
@click="close"
|
||||||
|
>
|
||||||
|
{{ t('generic.cancel') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AsyncButton
|
||||||
|
mode="enable"
|
||||||
|
@click="save"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -93,6 +93,9 @@ export default {
|
|||||||
|
|
||||||
const hostname = this.value.spec.template.spec.hostname || '';
|
const hostname = this.value.spec.template.spec.hostname || '';
|
||||||
|
|
||||||
|
// Display name can contain arbitrary strings. There is no XSS risk because the value is
|
||||||
|
// rendered via Vue's {{ }} interpolation which auto-escapes HTML; v-html is never used for
|
||||||
|
// this field. See harvester/harvester#10423 for details.
|
||||||
const customizeDisplayName = !!(this.value.metadata?.annotations?.[HCI_ANNOTATIONS.VM_DISPLAY_NAME]);
|
const customizeDisplayName = !!(this.value.metadata?.annotations?.[HCI_ANNOTATIONS.VM_DISPLAY_NAME]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -484,6 +487,20 @@ export default {
|
|||||||
await value.save();
|
await value.save();
|
||||||
await this.applyHooks(AFTER_SAVE_HOOKS);
|
await this.applyHooks(AFTER_SAVE_HOOKS);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
// Reset the generated secret name(s) so a retried create (e.g. after correcting an
|
||||||
|
// invalid/duplicate VM name) always regenerates them from the current name instead of
|
||||||
|
// reusing names derived from this failed attempt. Only do this for create: in edit mode
|
||||||
|
// secretName/sysprep.secretName may reference an existing secret loaded from the VM,
|
||||||
|
// which must not be discarded. See harvester/harvester#11174.
|
||||||
|
if (this.isCreate) {
|
||||||
|
this.secretName = '';
|
||||||
|
this.secretNamePrefixUsed = '';
|
||||||
|
if (this.sysprepSecretNamePrefixUsed !== '') {
|
||||||
|
this.sysprep.secretName = '';
|
||||||
|
this.sysprepSecretNamePrefixUsed = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.errors.push(...exceptionToErrorsArray(e));
|
this.errors.push(...exceptionToErrorsArray(e));
|
||||||
buttonCb(false);
|
buttonCb(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<script>
|
<script>
|
||||||
import ConsumptionGauge from '@shell/components/ConsumptionGauge';
|
import PercentageBar from '@shell/components/PercentageBar';
|
||||||
import { METRIC, NODE } from '@shell/config/types';
|
import { METRIC, NODE } from '@shell/config/types';
|
||||||
import { parseSi } from '@shell/utils/units';
|
import { parseSi } from '@shell/utils/units';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterCpuUsed',
|
name: 'HarvesterCpuUsed',
|
||||||
components: { ConsumptionGauge },
|
components: { PercentageBar },
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
value: {
|
value: {
|
||||||
@@ -74,54 +74,89 @@ export default {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
|
||||||
|
reservedPercentage() {
|
||||||
|
return this.cpuTotal ? (this.reserved * 100) / this.cpuTotal : 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
usedPercentage() {
|
||||||
|
return this.cpuTotal ? (this.used * 100) / this.cpuTotal : 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
reservedAmountTemplateValues() {
|
||||||
|
return {
|
||||||
|
used: this.numberFormatter(this.reserved || 0),
|
||||||
|
total: this.numberFormatter(this.cpuTotal || 0),
|
||||||
|
unit: ` ${ this.cpuUnits }`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
usedAmountTemplateValues() {
|
||||||
|
return {
|
||||||
|
used: this.numberFormatter(this.used || 0),
|
||||||
|
total: this.numberFormatter(this.cpuTotal || 0),
|
||||||
|
unit: ` ${ this.cpuUnits }`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
numberFormatter(value) {
|
||||||
|
return Number.isInteger(value) ? value : value.toFixed(2);
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<ConsumptionGauge
|
<div class="consumption-gauge">
|
||||||
:capacity="cpuTotal"
|
<div class="numbers">
|
||||||
:used="reserved"
|
<span class="mr-5">
|
||||||
:units="cpuUnits"
|
|
||||||
:resource-name="resourceName"
|
|
||||||
>
|
|
||||||
<template #title="{amountTemplateValues, formattedPercentage}">
|
|
||||||
<span>
|
|
||||||
{{ t('harvester.formatters.hardwareResourceGauge.reserved') }}
|
{{ t('harvester.formatters.hardwareResourceGauge.reserved') }}
|
||||||
</span>
|
</span>
|
||||||
<span class="precent-data">
|
<span class="precent-data">
|
||||||
{{ t('node.detail.glance.consumptionGauge.amount', amountTemplateValues) }}
|
{{ t('node.detail.glance.consumptionGauge.amount', reservedAmountTemplateValues) }}
|
||||||
<span class="ml-10 percentage">/ {{ formattedPercentage }}
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</div>
|
||||||
</ConsumptionGauge>
|
<div class="mt-10">
|
||||||
|
<PercentageBar
|
||||||
|
:model-value="reservedPercentage"
|
||||||
|
show-percentage
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="showUsed"
|
v-if="showUsed"
|
||||||
class="mt-10"
|
class="consumption-gauge mt-10"
|
||||||
>
|
>
|
||||||
<ConsumptionGauge
|
<div class="numbers">
|
||||||
:capacity="cpuTotal"
|
<span>
|
||||||
:used="used"
|
{{ t('harvester.formatters.hardwareResourceGauge.used') }}
|
||||||
:units="cpuUnits"
|
</span>
|
||||||
>
|
<span class="precent-data">
|
||||||
<template #title="{amountTemplateValues, formattedPercentage}">
|
{{ t('node.detail.glance.consumptionGauge.amount', usedAmountTemplateValues) }}
|
||||||
<span>
|
</span>
|
||||||
{{ t('harvester.formatters.hardwareResourceGauge.used') }}
|
</div>
|
||||||
</span>
|
<div class="mt-10">
|
||||||
<span class="precent-data">
|
<PercentageBar
|
||||||
{{ t('node.detail.glance.consumptionGauge.amount', amountTemplateValues) }}
|
:model-value="usedPercentage"
|
||||||
<span class="ml-10 percentage">/ {{ formattedPercentage }}
|
show-percentage
|
||||||
</span>
|
/>
|
||||||
</span>
|
</div>
|
||||||
</template>
|
|
||||||
</ConsumptionGauge>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.consumption-gauge {
|
||||||
|
.numbers {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.precent-data {
|
.precent-data {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<script>
|
<script>
|
||||||
import ConsumptionGauge from '@shell/components/ConsumptionGauge';
|
import PercentageBar from '@shell/components/PercentageBar';
|
||||||
import { METRIC, NODE } from '@shell/config/types';
|
import { METRIC, NODE } from '@shell/config/types';
|
||||||
import { formatSi, exponentNeeded, UNITS, parseSi } from '@shell/utils/units';
|
import { formatSi, exponentNeeded, UNITS, parseSi } from '@shell/utils/units';
|
||||||
import { UNIT_SUFFIX } from '../utils/unit';
|
import { UNIT_SUFFIX } from '../utils/unit';
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterMemoryUsed',
|
name: 'HarvesterMemoryUsed',
|
||||||
components: { ConsumptionGauge },
|
components: { PercentageBar },
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
value: {
|
value: {
|
||||||
@@ -76,6 +76,30 @@ export default {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
reservedPercentage() {
|
||||||
|
return this.memoryTotal ? (this.reserved * 100) / this.memoryTotal : 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
usedPercentage() {
|
||||||
|
return this.memoryTotal ? (this.used * 100) / this.memoryTotal : 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
reservedAmountTemplateValues() {
|
||||||
|
return {
|
||||||
|
used: this.memoryFormatter(this.reserved || 0),
|
||||||
|
total: this.memoryFormatter(this.memoryTotal || 0),
|
||||||
|
unit: ` ${ this.memoryUnits }`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
usedAmountTemplateValues() {
|
||||||
|
return {
|
||||||
|
used: this.memoryFormatter(this.used || 0),
|
||||||
|
total: this.memoryFormatter(this.memoryTotal || 0),
|
||||||
|
unit: ` ${ this.memoryUnits }`,
|
||||||
|
};
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@@ -96,50 +120,53 @@ export default {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<ConsumptionGauge
|
<div class="consumption-gauge">
|
||||||
:capacity="memoryTotal"
|
<div class="numbers">
|
||||||
:used="reserved"
|
<span class="mr-10">
|
||||||
:units="memoryUnits"
|
|
||||||
:number-formatter="memoryFormatter"
|
|
||||||
:resource-name="resourceName"
|
|
||||||
>
|
|
||||||
<template #title="{amountTemplateValues, formattedPercentage}">
|
|
||||||
<span>
|
|
||||||
{{ t('harvester.formatters.hardwareResourceGauge.reserved') }}
|
{{ t('harvester.formatters.hardwareResourceGauge.reserved') }}
|
||||||
</span>
|
</span>
|
||||||
<span class="precent-data">
|
<span class="precent-data">
|
||||||
{{ t('node.detail.glance.consumptionGauge.amount', amountTemplateValues) }}
|
{{ t('node.detail.glance.consumptionGauge.amount', reservedAmountTemplateValues) }}
|
||||||
<span class="ml-10 percentage">/ {{ formattedPercentage }}
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</div>
|
||||||
</ConsumptionGauge>
|
<div class="mt-10">
|
||||||
|
<PercentageBar
|
||||||
|
:model-value="reservedPercentage"
|
||||||
|
show-percentage
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="showUsed"
|
v-if="showUsed"
|
||||||
class="mt-10"
|
class="consumption-gauge mt-10"
|
||||||
>
|
>
|
||||||
<ConsumptionGauge
|
<div class="numbers">
|
||||||
:capacity="memoryTotal"
|
<span>
|
||||||
:used="used"
|
{{ t('harvester.formatters.hardwareResourceGauge.used') }}
|
||||||
:units="memoryUnits"
|
</span>
|
||||||
:number-formatter="memoryFormatter"
|
<span class="precent-data">
|
||||||
>
|
{{ t('node.detail.glance.consumptionGauge.amount', usedAmountTemplateValues) }}
|
||||||
<template #title="{amountTemplateValues, formattedPercentage}">
|
</span>
|
||||||
<span>
|
</div>
|
||||||
{{ t('harvester.formatters.hardwareResourceGauge.used') }}
|
<div class="mt-10">
|
||||||
</span>
|
<PercentageBar
|
||||||
<span class="precent-data">
|
:model-value="usedPercentage"
|
||||||
{{ t('node.detail.glance.consumptionGauge.amount', amountTemplateValues) }}
|
show-percentage
|
||||||
<span class="ml-10 percentage">/ {{ formattedPercentage }}
|
/>
|
||||||
</span>
|
</div>
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</ConsumptionGauge>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.consumption-gauge {
|
||||||
|
.numbers {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.precent-data {
|
.precent-data {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<script>
|
<script>
|
||||||
import ConsumptionGauge from '@shell/components/ConsumptionGauge';
|
import PercentageBar from '@shell/components/PercentageBar';
|
||||||
import { LONGHORN } from '@shell/config/types';
|
import { LONGHORN } from '@shell/config/types';
|
||||||
import { formatSi, exponentNeeded, UNITS } from '@shell/utils/units';
|
import { formatSi, exponentNeeded, UNITS } from '@shell/utils/units';
|
||||||
import { UNIT_SUFFIX } from '../utils/unit';
|
import { UNIT_SUFFIX } from '../utils/unit';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HarvesterStorageUsed',
|
name: 'HarvesterStorageUsed',
|
||||||
components: { ConsumptionGauge },
|
components: { PercentageBar },
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
value: {
|
value: {
|
||||||
@@ -120,6 +120,14 @@ export default {
|
|||||||
unit: this.allocatedUnits,
|
unit: this.allocatedUnits,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
usedPercentage() {
|
||||||
|
return this.storageStats.maximum ? (this.storageStats.used * 100) / this.storageStats.maximum : 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
allocatedPercentage() {
|
||||||
|
return this.storageStats.total ? (this.storageStats.scheduled * 100) / this.storageStats.total : 0;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@@ -144,53 +152,54 @@ export default {
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
v-if="showAllocated"
|
v-if="showAllocated"
|
||||||
|
class="consumption-gauge"
|
||||||
>
|
>
|
||||||
<ConsumptionGauge
|
<div class="numbers">
|
||||||
:capacity="storageStats.total"
|
<span class="mr-10">
|
||||||
:used="storageStats.scheduled"
|
{{ t('harvester.dashboard.hardwareResourceGauge.allocated') }}
|
||||||
:units="allocatedUnits"
|
</span>
|
||||||
:number-formatter="formatter"
|
<span class="precent-data">
|
||||||
:resource-name="resourceName"
|
{{ t('node.detail.glance.consumptionGauge.amount', allocatedAmountTemplateValues) }}
|
||||||
>
|
</span>
|
||||||
<template #title="{formattedPercentage}">
|
</div>
|
||||||
<span>
|
<div class="mt-10">
|
||||||
{{ t('harvester.dashboard.hardwareResourceGauge.allocated') }}
|
<PercentageBar
|
||||||
</span>
|
:model-value="allocatedPercentage"
|
||||||
<span class="precent-data">
|
show-percentage
|
||||||
{{ t('node.detail.glance.consumptionGauge.amount', allocatedAmountTemplateValues) }}
|
/>
|
||||||
<span class="ml-10 percentage">
|
</div>
|
||||||
/ {{ formattedPercentage }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</ConsumptionGauge>
|
|
||||||
</div>
|
</div>
|
||||||
<ConsumptionGauge
|
<div
|
||||||
:capacity="storageStats.maximum"
|
class="consumption-gauge"
|
||||||
:used="storageStats.used"
|
:class="{ 'mt-10': showAllocated }"
|
||||||
:units="usedUnits"
|
|
||||||
:number-formatter="formatter"
|
|
||||||
:resource-name="showAllocated ? '' : resourceName"
|
|
||||||
:class="{
|
|
||||||
'mt-10': showAllocated,
|
|
||||||
}"
|
|
||||||
>
|
>
|
||||||
<template #title="{formattedPercentage}">
|
<div class="numbers">
|
||||||
<span>
|
<span>
|
||||||
{{ t('node.detail.glance.consumptionGauge.used') }}
|
{{ t('node.detail.glance.consumptionGauge.used') }}
|
||||||
</span>
|
</span>
|
||||||
<span class="precent-data">
|
<span class="precent-data">
|
||||||
{{ t('node.detail.glance.consumptionGauge.amount', usedAmountTemplateValues) }}
|
{{ t('node.detail.glance.consumptionGauge.amount', usedAmountTemplateValues) }}
|
||||||
<span class="ml-10 percentage">
|
|
||||||
/ {{ formattedPercentage }}
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</div>
|
||||||
</ConsumptionGauge>
|
<div class="mt-10">
|
||||||
|
<PercentageBar
|
||||||
|
:model-value="usedPercentage"
|
||||||
|
show-percentage
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.consumption-gauge {
|
||||||
|
.numbers {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.precent-data {
|
.precent-data {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -331,7 +331,8 @@ harvester:
|
|||||||
message: Message
|
message: Message
|
||||||
phase: Phase
|
phase: Phase
|
||||||
attachedVM: Attached Virtual Machine
|
attachedVM: Attached Virtual Machine
|
||||||
cpuManager: CPU Manager
|
cpuManager: CPU<br>Manager
|
||||||
|
diskState: Disk
|
||||||
routeConnectivityTooltip: Connectivity between the VM network and the management network, which the Harvester nodes are connected to.
|
routeConnectivityTooltip: Connectivity between the VM network and the management network, which the Harvester nodes are connected to.
|
||||||
fingerprint: Fingerprint
|
fingerprint: Fingerprint
|
||||||
value: Value
|
value: Value
|
||||||
@@ -487,7 +488,6 @@ harvester:
|
|||||||
volume:
|
volume:
|
||||||
upperType: Volume name
|
upperType: Volume name
|
||||||
lowerType: volume name
|
lowerType: volume name
|
||||||
needAtLeastOneBootable: 'At least one bootable volume is required!'
|
|
||||||
image:
|
image:
|
||||||
ruleTip: 'The URL you have entered ends in an extension that we do not support. We only accept image files that end in .img, .iso, .qcow, .qcow2, .raw.'
|
ruleTip: 'The URL you have entered ends in an extension that we do not support. We only accept image files that end in .img, .iso, .qcow, .qcow2, .raw.'
|
||||||
ruleFileTip: 'The file you have chosen ends in an extension that we do not support. We only accept image files that end in .img, .iso, .qcow, .qcow2, .raw.'
|
ruleFileTip: 'The file you have chosen ends in an extension that we do not support. We only accept image files that end in .img, .iso, .qcow, .qcow2, .raw.'
|
||||||
@@ -1099,6 +1099,8 @@ harvester:
|
|||||||
downloadLog: Download Log
|
downloadLog: Download Log
|
||||||
logStatus: Log Download Status
|
logStatus: Log Download Status
|
||||||
dismissMessage: Dismiss it
|
dismissMessage: Dismiss it
|
||||||
|
upgradeAvailable: Upgrade Available
|
||||||
|
upgradeUnavailable: Upgrade Unavailable
|
||||||
upgradeInfo:
|
upgradeInfo:
|
||||||
warning: WARNING
|
warning: WARNING
|
||||||
doc: Read the <a href="{url}" target="_blank">documentation</a> before starting the upgrade process. Ensure that you complete procedures that are relevant to your environment and the version you are upgrading to.
|
doc: Read the <a href="{url}" target="_blank">documentation</a> before starting the upgrade process. Ensure that you complete procedures that are relevant to your environment and the version you are upgrading to.
|
||||||
@@ -1836,6 +1838,7 @@ harvester:
|
|||||||
unknown: Unknown
|
unknown: Unknown
|
||||||
identifier: Identifier
|
identifier: Identifier
|
||||||
podNetwork: Pod Network
|
podNetwork: Pod Network
|
||||||
|
ignored: Ignored
|
||||||
usedBy: "Used by:"
|
usedBy: "Used by:"
|
||||||
vlan: "VLAN {id}"
|
vlan: "VLAN {id}"
|
||||||
vCpu: "{count} vCPU"
|
vCpu: "{count} vCPU"
|
||||||
@@ -1954,8 +1957,9 @@ harvester:
|
|||||||
initializingMigration: Initializing migration
|
initializingMigration: Initializing migration
|
||||||
finishedSuccessfully: Finished Successfully
|
finishedSuccessfully: Finished Successfully
|
||||||
step: "Step {index}"
|
step: "Step {index}"
|
||||||
vmCount: "{count} VMs"
|
vmCount: "{count, plural, one {# VM} other {# VMs}}"
|
||||||
vmId: "id: {id}"
|
vmId: "id: {id}"
|
||||||
|
checkMigratedVm: Click to check migrated VM details
|
||||||
configureMappings:
|
configureMappings:
|
||||||
title: Set Mappings
|
title: Set Mappings
|
||||||
description: Map VMware networks and datastores to Harvester and Longhorn target resources
|
description: Map VMware networks and datastores to Harvester and Longhorn target resources
|
||||||
@@ -1969,7 +1973,7 @@ harvester:
|
|||||||
template: Use existing network mapping as template
|
template: Use existing network mapping as template
|
||||||
options:
|
options:
|
||||||
podNetworking: Management Network
|
podNetworking: Management Network
|
||||||
ignored: Ignored
|
ignored: Ignored (No NIC will be created)
|
||||||
storageMapping:
|
storageMapping:
|
||||||
title: Storage Mapping
|
title: Storage Mapping
|
||||||
description: Map VMware datastores to Harvester storage classes
|
description: Map VMware datastores to Harvester storage classes
|
||||||
@@ -1986,7 +1990,8 @@ harvester:
|
|||||||
memory: Memory
|
memory: Memory
|
||||||
storage: Storage
|
storage: Storage
|
||||||
source: Source
|
source: Source
|
||||||
targetNamespace: Target Namespace
|
targetNamespace: Namespace
|
||||||
|
targetNamespacePlaceholder: "Select target namespace"
|
||||||
migrationMode: Migration Mode
|
migrationMode: Migration Mode
|
||||||
coldMigration: Cold Migration
|
coldMigration: Cold Migration
|
||||||
virtualMachines: Virtual Machines
|
virtualMachines: Virtual Machines
|
||||||
@@ -2254,6 +2259,12 @@ harvester:
|
|||||||
prefix: The nvidia-driver-toolkit add-on is not enabled, click
|
prefix: The nvidia-driver-toolkit add-on is not enabled, click
|
||||||
middle: here
|
middle: here
|
||||||
suffix: to enable it to manage your SR-IOV GPU devices.
|
suffix: to enable it to manage your SR-IOV GPU devices.
|
||||||
|
enable:
|
||||||
|
title: Enable SR-IOV GPU Device
|
||||||
|
skipMigConfiguration: Use timesliced mode (skip MIG configuration)
|
||||||
|
note: If unchecked, MIG mode will be attempted by default. If MIG mode is not supported or fails to initialize, it will fall back to timesliced mode. The actual mode depends on whether the GPU supports MIG.
|
||||||
|
timeslicedBadge: timesliced mode
|
||||||
|
timesliced: Timesliced
|
||||||
|
|
||||||
migconfiguration:
|
migconfiguration:
|
||||||
label: vGPU MIG Configurations
|
label: vGPU MIG Configurations
|
||||||
|
|||||||
@@ -86,6 +86,13 @@ export default {
|
|||||||
value: 'spec.address',
|
value: 'spec.address',
|
||||||
sort: ['spec.address']
|
sort: ['spec.address']
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'timesliced',
|
||||||
|
labelKey: 'harvester.sriovgpu.timesliced',
|
||||||
|
value: 'isTimesliced',
|
||||||
|
formatter: 'Checked',
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'vfAddresses',
|
name: 'vfAddresses',
|
||||||
label: 'VF Addresses',
|
label: 'VF Addresses',
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export default {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
...NAME,
|
...NAME,
|
||||||
width: 130,
|
width: 150,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'host-ip',
|
name: 'host-ip',
|
||||||
@@ -136,14 +136,14 @@ export default {
|
|||||||
value: 'id',
|
value: 'id',
|
||||||
formatter: 'HarvesterCPUPinning',
|
formatter: 'HarvesterCPUPinning',
|
||||||
formatterOpts: { rows: this.rows },
|
formatterOpts: { rows: this.rows },
|
||||||
width: 150,
|
width: 75,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (this.hasLonghornSchema) {
|
if (this.hasLonghornSchema) {
|
||||||
out.push({
|
out.push({
|
||||||
name: 'diskState',
|
name: 'diskState',
|
||||||
labelKey: 'tableHeaders.diskState',
|
labelKey: 'harvester.tableHeaders.diskState',
|
||||||
value: 'diskState',
|
value: 'diskState',
|
||||||
formatter: 'HarvesterDiskState',
|
formatter: 'HarvesterDiskState',
|
||||||
width: 130,
|
width: 130,
|
||||||
@@ -156,7 +156,6 @@ export default {
|
|||||||
name: 'console',
|
name: 'console',
|
||||||
label: ' ',
|
label: ' ',
|
||||||
align: 'right',
|
align: 'right',
|
||||||
width: 80,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
@@ -239,6 +238,8 @@ export default {
|
|||||||
v-if="!row.consoleUrl"
|
v-if="!row.consoleUrl"
|
||||||
:href="consoleDocLink"
|
:href="consoleDocLink"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="console-info"
|
||||||
><i class="icon icon-info" /></a>
|
><i class="icon icon-info" /></a>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -249,5 +250,13 @@ export default {
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.console-button {
|
.console-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-info {
|
||||||
|
position: absolute;
|
||||||
|
top: -8px;
|
||||||
|
right: -3px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -162,6 +162,12 @@ export default {
|
|||||||
machineType: '',
|
machineType: '',
|
||||||
machineTypes: [],
|
machineTypes: [],
|
||||||
secretName: '',
|
secretName: '',
|
||||||
|
// Tracks the name prefix that `secretName` was generated from, so it can be
|
||||||
|
// regenerated if the VM name changes (e.g. after a failed create attempt is retried
|
||||||
|
// with a different name). See harvester/harvester#11174.
|
||||||
|
secretNamePrefixUsed: '',
|
||||||
|
// Same purpose as secretNamePrefixUsed, but for the Windows sysprep secret name.
|
||||||
|
sysprepSecretNamePrefixUsed: '',
|
||||||
secretRef: null,
|
secretRef: null,
|
||||||
showAdvanced: false,
|
showAdvanced: false,
|
||||||
deleteAgent: true,
|
deleteAgent: true,
|
||||||
@@ -866,18 +872,30 @@ export default {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.needNewSecret || !this.secretName) {
|
// Regenerate the cloud-init secret name whenever it hasn't been generated yet, or the VM
|
||||||
|
// name has changed since it was last generated (e.g. a previous create attempt failed with
|
||||||
|
// an invalid/duplicate name and the user corrected it). Without this, a stale secret name
|
||||||
|
// derived from a rejected attempt gets reused for the VM that's actually created.
|
||||||
|
// See harvester/harvester#11174.
|
||||||
|
if (this.needNewSecret || !this.secretName || (this.isCreate && this.secretNamePrefixUsed !== this.secretNamePrefix)) {
|
||||||
this.secretName = this.generateSecretName(this.secretNamePrefix);
|
this.secretName = this.generateSecretName(this.secretNamePrefix);
|
||||||
|
this.secretNamePrefixUsed = this.secretNamePrefix;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!disks.find((D) => D.name === 'sysprep') && this.isWindows) {
|
if (!disks.find((D) => D.name === 'sysprep') && this.isWindows) {
|
||||||
const hasSysprepContent = !!this.sysprep.xmlContent?.trim?.();
|
const hasSysprepContent = !!this.sysprep.xmlContent?.trim?.();
|
||||||
|
|
||||||
// If we have content but no secret name, it's a new secret that needs a name.
|
// If we have content but no secret name, it's a new secret that needs a name. Also
|
||||||
if (hasSysprepContent && !this.sysprep.secretName?.trim?.()) {
|
// regenerate when creating a VM if the name prefix has changed since it was last
|
||||||
|
// generated.
|
||||||
|
const sysprepNeedsNewSecretName = !this.sysprep.secretName?.trim?.() ||
|
||||||
|
(this.isCreate && this.sysprepSecretNamePrefixUsed !== '' && this.sysprepSecretNamePrefixUsed !== this.secretNamePrefix);
|
||||||
|
|
||||||
|
if (hasSysprepContent && sysprepNeedsNewSecretName) {
|
||||||
const prefix = this.secretNamePrefix ? `${ this.secretNamePrefix }-windows-sysprep` : 'windows-sysprep';
|
const prefix = this.secretNamePrefix ? `${ this.secretNamePrefix }-windows-sysprep` : 'windows-sysprep';
|
||||||
|
|
||||||
this.sysprep.secretName = `${ this.value.metadata.namespace }/${ this.generateSecretName(prefix) }`;
|
this.sysprep.secretName = `${ this.value.metadata.namespace }/${ this.generateSecretName(prefix) }`;
|
||||||
|
this.sysprepSecretNamePrefixUsed = this.secretNamePrefix;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preserve/attach sysprep whenever a secret is selected/known.
|
// Preserve/attach sysprep whenever a secret is selected/known.
|
||||||
@@ -896,7 +914,7 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!disks.find( (D) => D.name === 'cloudinitdisk') && (this.userData || this.networkData)) {
|
if (!disks.find( (D) => D.name === 'cloudinitdisk') && (this.userData || this.networkScript)) {
|
||||||
if (!this.isWindows) {
|
if (!this.isWindows) {
|
||||||
disks.push({
|
disks.push({
|
||||||
name: 'cloudinitdisk',
|
name: 'cloudinitdisk',
|
||||||
|
|||||||
@@ -69,16 +69,15 @@ export default class SRIOVDevice extends SteveModel {
|
|||||||
return this.spec.enabled && this.status?.vfAddresses?.length > 0 && this.status?.vGPUDevices?.length > 0;
|
return this.spec.enabled && this.status?.vfAddresses?.length > 0 && this.status?.vGPUDevices?.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async enableDevice() {
|
get isTimesliced() {
|
||||||
try {
|
return this.metadata?.annotations?.[HCI_ANNOTATIONS.SKIP_MIG_CONFIGURATION] === 'true';
|
||||||
this.spec.enabled = true;
|
}
|
||||||
await this.save();
|
|
||||||
} catch (err) {
|
enableDevice() {
|
||||||
this.$dispatch('growl/fromError', {
|
this.$dispatch('promptModal', {
|
||||||
title: this.t('generic.notification.title.error', { name: escapeHtml(this.metadata.name) }),
|
resources: [this],
|
||||||
err,
|
component: 'EnableSriovGpuDevice'
|
||||||
}, { root: true });
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async disableDevice() {
|
async disableDevice() {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "harvester",
|
"name": "harvester",
|
||||||
"description": "Rancher UI Extension for Harvester",
|
"description": "Rancher UI Extension for Harvester",
|
||||||
"version": "1.9.0-rc2",
|
"version": "1.9.0-rc3",
|
||||||
"private": false,
|
"private": false,
|
||||||
"rancher": {
|
"rancher": {
|
||||||
"annotations": {
|
"annotations": {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ const inStore = computed(() => store.getters['currentProduct'].inStore);
|
|||||||
const allPlans = computed(() => store.getters[`${ inStore.value }/all`](HCI.FORKLIFT_PLAN));
|
const allPlans = computed(() => store.getters[`${ inStore.value }/all`](HCI.FORKLIFT_PLAN));
|
||||||
const allNetworkMaps = computed(() => store.getters[`${ inStore.value }/all`](HCI.FORKLIFT_NETWORK_MAP));
|
const allNetworkMaps = computed(() => store.getters[`${ inStore.value }/all`](HCI.FORKLIFT_NETWORK_MAP));
|
||||||
const allStorageMaps = computed(() => store.getters[`${ inStore.value }/all`](HCI.FORKLIFT_STORAGE_MAP));
|
const allStorageMaps = computed(() => store.getters[`${ inStore.value }/all`](HCI.FORKLIFT_STORAGE_MAP));
|
||||||
|
const allVMs = computed(() => store.getters[`${ inStore.value }/all`](HCI.VM));
|
||||||
|
|
||||||
const escapeHtml = (value = '') => String(value)
|
const escapeHtml = (value = '') => String(value)
|
||||||
.replaceAll('&', '&')
|
.replaceAll('&', '&')
|
||||||
@@ -98,12 +99,12 @@ const formatPipelineTooltip = (pipeline = []) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const formatDuration = (started, completed) => {
|
const formatDuration = (started, completed) => {
|
||||||
if (!started || !completed) {
|
if (!started) {
|
||||||
return { display: null, seconds: -1 };
|
return { display: null, seconds: -1 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const startMs = new Date(started).getTime();
|
const startMs = new Date(started).getTime();
|
||||||
const endMs = new Date(completed).getTime();
|
const endMs = completed ? new Date(completed).getTime() : Date.now();
|
||||||
|
|
||||||
if (isNaN(startMs) || isNaN(endMs) || endMs < startMs) {
|
if (isNaN(startMs) || isNaN(endMs) || endMs < startMs) {
|
||||||
return { display: null, seconds: -1 };
|
return { display: null, seconds: -1 };
|
||||||
@@ -129,8 +130,8 @@ const rows = computed(() => {
|
|||||||
const netMap = allNetworkMaps.value.find((m) => m.metadata.name === netMapName && m.metadata.namespace === netMapNs);
|
const netMap = allNetworkMaps.value.find((m) => m.metadata.name === netMapName && m.metadata.namespace === netMapNs);
|
||||||
const storMap = allStorageMaps.value.find((m) => m.metadata.name === storMapName && m.metadata.namespace === storMapNs);
|
const storMap = allStorageMaps.value.find((m) => m.metadata.name === storMapName && m.metadata.namespace === storMapNs);
|
||||||
|
|
||||||
plan.networkEntries = (netMap?.spec?.map || []).map((e) => `${ e.source?.id || '-' } → ${ e.destination?.type === 'pod' ? t('harvester.addons.vmMigration.generic.podNetwork') : (e.destination?.name || '-') }`);
|
plan.networkEntries = (netMap?.spec?.map || []).map((e) => `${ e.source?.id || 'Ignored' } → ${ e.destination?.type === 'pod' ? t('harvester.addons.vmMigration.configureMappings.networkMapping.options.podNetworking') : (e.destination?.name || 'Ignored') }`);
|
||||||
plan.storageEntries = (storMap?.spec?.map || []).map((e) => `${ e.source?.id || '-' } → ${ e.destination?.storageClass || '-' }`);
|
plan.storageEntries = (storMap?.spec?.map || []).map((e) => `${ e.source?.id || 'Ignored' } → ${ e.destination?.storageClass || 'Ignored' }`);
|
||||||
|
|
||||||
plan.vmIdsDisplay = (plan.spec?.vms || []).map((vm) => vm.id || vm.name || '').filter(Boolean).join(', ') || '-';
|
plan.vmIdsDisplay = (plan.spec?.vms || []).map((vm) => vm.id || vm.name || '').filter(Boolean).join(', ') || '-';
|
||||||
|
|
||||||
@@ -183,6 +184,24 @@ const rows = computed(() => {
|
|||||||
errorMsg = `${ currentStep || t('harvester.addons.vmMigration.dashboard.progress.migration') }: ${ t('harvester.addons.vmMigration.dashboard.progress.failed') }`;
|
errorMsg = `${ currentStep || t('harvester.addons.vmMigration.dashboard.progress.migration') }: ${ t('harvester.addons.vmMigration.dashboard.progress.failed') }`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const vmNamespace = vm.namespace || plan.spec?.targetNamespace || plan.metadata?.namespace;
|
||||||
|
const routeParams = currentRouter().currentRoute?.value?.params || {};
|
||||||
|
const product = routeParams.product || store.getters['productId'];
|
||||||
|
const cluster = routeParams.cluster || store.getters['clusterId'];
|
||||||
|
const vmNameCandidates = [vm.targetName, vm.name, vm.id].filter(Boolean);
|
||||||
|
const targetVm = allVMs.value.find((item) => vmNameCandidates.includes(item.metadata?.name) && (!vmNamespace || item.metadata?.namespace === vmNamespace));
|
||||||
|
const canNavigateToVm = overallProgress >= 100 && !errorMsg && !plan.planFailed && !plan.planCanceled && !!targetVm && !!product && !!cluster;
|
||||||
|
const vmDetailLocation = canNavigateToVm ? {
|
||||||
|
name: `${ PRODUCT_NAME }-c-cluster-resource-namespace-id`,
|
||||||
|
params: {
|
||||||
|
product,
|
||||||
|
cluster,
|
||||||
|
resource: HCI.VM,
|
||||||
|
namespace: targetVm.metadata.namespace,
|
||||||
|
id: targetVm.metadata.name,
|
||||||
|
}
|
||||||
|
} : null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
vmName: vm.name || vm.id || t('harvester.addons.vmMigration.generic.unknown'),
|
vmName: vm.name || vm.id || t('harvester.addons.vmMigration.generic.unknown'),
|
||||||
vmId: vm.id || '',
|
vmId: vm.id || '',
|
||||||
@@ -191,6 +210,7 @@ const rows = computed(() => {
|
|||||||
pipelineTooltip: formatPipelineTooltip(pipeline),
|
pipelineTooltip: formatPipelineTooltip(pipeline),
|
||||||
errorMsg,
|
errorMsg,
|
||||||
canceled: plan.planCanceled,
|
canceled: plan.planCanceled,
|
||||||
|
vmDetailLocation,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -248,7 +268,7 @@ const headers = [
|
|||||||
labelKey: 'harvester.addons.vmMigration.dashboard.columns.duration',
|
labelKey: 'harvester.addons.vmMigration.dashboard.columns.duration',
|
||||||
value: 'duration',
|
value: 'duration',
|
||||||
sort: 'durationSeconds',
|
sort: 'durationSeconds',
|
||||||
width: 120,
|
width: 90,
|
||||||
},
|
},
|
||||||
{ ...AGE },
|
{ ...AGE },
|
||||||
];
|
];
|
||||||
@@ -283,6 +303,17 @@ const init = async() => {
|
|||||||
store.dispatch(`${ inStore.value }/findAll`, { type: HCI.FORKLIFT_NETWORK_MAP }),
|
store.dispatch(`${ inStore.value }/findAll`, { type: HCI.FORKLIFT_NETWORK_MAP }),
|
||||||
store.dispatch(`${ inStore.value }/findAll`, { type: HCI.FORKLIFT_STORAGE_MAP }),
|
store.dispatch(`${ inStore.value }/findAll`, { type: HCI.FORKLIFT_STORAGE_MAP }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// VMs are only needed to conditionally show the post-migration VM-detail
|
||||||
|
// link, so load them separately and non-fatally: a failure here (or the
|
||||||
|
// cost of findAll on large clusters) should not block plans/maps from
|
||||||
|
// rendering.
|
||||||
|
try {
|
||||||
|
await store.dispatch(`${ inStore.value }/findAll`, { type: HCI.VM });
|
||||||
|
} catch (e) {
|
||||||
|
// Intentionally ignored: the dashboard still works without the VM list,
|
||||||
|
// the migrated VM-detail link simply won't be shown.
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errors.value = [e?.message || t('harvester.addons.vmMigration.errors.failedLoadPlans')];
|
errors.value = [e?.message || t('harvester.addons.vmMigration.errors.failedLoadPlans')];
|
||||||
} finally {
|
} finally {
|
||||||
@@ -366,7 +397,17 @@ init();
|
|||||||
>
|
>
|
||||||
<div class="vm-progress-header">
|
<div class="vm-progress-header">
|
||||||
<div class="vm-name-block">
|
<div class="vm-name-block">
|
||||||
<span class="vm-name">{{ vm.vmName }}</span>
|
<span class="vm-name">
|
||||||
|
{{ vm.vmName }}
|
||||||
|
<router-link
|
||||||
|
v-if="vm.vmDetailLocation"
|
||||||
|
v-clean-tooltip="t('harvester.addons.vmMigration.dashboard.progress.checkMigratedVm')"
|
||||||
|
:to="vm.vmDetailLocation"
|
||||||
|
class="vm-detail-link"
|
||||||
|
>
|
||||||
|
<i class="icon icon-external-link" />
|
||||||
|
</router-link>
|
||||||
|
</span>
|
||||||
<span class="text-muted vm-id">{{ t('harvester.addons.vmMigration.dashboard.progress.vmId', { id: vm.vmId }) }}</span>
|
<span class="text-muted vm-id">{{ t('harvester.addons.vmMigration.dashboard.progress.vmId', { id: vm.vmId }) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -457,6 +498,14 @@ init();
|
|||||||
|
|
||||||
.vm-name {
|
.vm-name {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vm-detail-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vm-name-block {
|
.vm-name-block {
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ const stepData = reactive({
|
|||||||
networkEntries: [],
|
networkEntries: [],
|
||||||
storageEntries: [],
|
storageEntries: [],
|
||||||
},
|
},
|
||||||
review: { planName: '' },
|
review: { planName: '', targetNamespace: '' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const steps = reactive([
|
const steps = reactive([
|
||||||
|
|||||||
@@ -69,17 +69,11 @@ export function vmDisks(spec, getters, errors, validatorArgs, displayKey, value)
|
|||||||
validName(getters, errors, D.name, diskNames, prefix, type, lowerType, upperType);
|
validName(getters, errors, D.name, diskNames, prefix, type, lowerType, upperType);
|
||||||
});
|
});
|
||||||
|
|
||||||
let hasBootableVolume = false;
|
|
||||||
|
|
||||||
_volumes.forEach((V, idx) => {
|
_volumes.forEach((V, idx) => {
|
||||||
const { type, typeValue } = getVolumeType(getters, V, _volumeClaimTemplates, value);
|
const { type, typeValue } = getVolumeType(getters, V, _volumeClaimTemplates, value);
|
||||||
|
|
||||||
const prefix = V.name || idx + 1;
|
const prefix = V.name || idx + 1;
|
||||||
|
|
||||||
if ([SOURCE_TYPE.IMAGE, SOURCE_TYPE.ATTACH_VOLUME, SOURCE_TYPE.CONTAINER].includes(type)) {
|
|
||||||
hasBootableVolume = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === SOURCE_TYPE.NEW || type === SOURCE_TYPE.IMAGE) {
|
if (type === SOURCE_TYPE.NEW || type === SOURCE_TYPE.IMAGE) {
|
||||||
if (!/([1-9]|[1-9][0-9]+)[a-zA-Z]+/.test(typeValue?.spec?.resources?.requests?.storage)) {
|
if (!/([1-9]|[1-9][0-9]+)[a-zA-Z]+/.test(typeValue?.spec?.resources?.requests?.storage)) {
|
||||||
const key = getters['i18n/t']('harvester.fields.size');
|
const key = getters['i18n/t']('harvester.fields.size');
|
||||||
@@ -136,13 +130,6 @@ export function vmDisks(spec, getters, errors, validatorArgs, displayKey, value)
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* At least one bootable volume must be provided. (Verify only when create.)
|
|
||||||
*/
|
|
||||||
if (!hasBootableVolume && !value.links) {
|
|
||||||
errors.push(getters['i18n/t']('harvester.validation.vm.volume.needAtLeastOneBootable'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ if (process.argv.length < 3) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const prTitle = process.argv[2];
|
const prTitle = process.argv[2];
|
||||||
const prLabel = prTitle.split(':')[0].trim();
|
|
||||||
|
// Parse the conventional-commit prefix `type(scope)!:`, keeping only the type
|
||||||
|
// and dropping an optional scope and breaking-change marker.
|
||||||
|
const prefixMatch = prTitle.match(/^\s*([a-zA-Z]+)(?:\([^)]*\))?!?:/);
|
||||||
|
const prLabel = prefixMatch ? prefixMatch[1].toLowerCase() : '';
|
||||||
|
|
||||||
const config = await load({ extends: ["./commitlint.config.js"] });
|
const config = await load({ extends: ["./commitlint.config.js"] });
|
||||||
const commitPrefix = config?.rules?.['type-enum']?.[2] || [];
|
const commitPrefix = config?.rules?.['type-enum']?.[2] || [];
|
||||||
|
|||||||
@@ -2846,10 +2846,10 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/node@*", "@types/node@25.3.3", "@types/node@25.9.4":
|
"@types/node@*", "@types/node@25.3.3", "@types/node@25.9.5":
|
||||||
version "25.9.4"
|
version "25.9.5"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-25.9.4.tgz#18b63c47f88c1fbbed9d55ea2b66ffd494a47001"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-25.9.5.tgz#0fefc09e6e82e94cde291bacf43522e989eb01a4"
|
||||||
integrity sha512-dszCsrKb5U7ZsVZBWiHFklTloVl0mSEnWH/iZXfZUlI4rzCUnsvGmgqfuVRHL54ugE7/wRuxEIXRa2iMZ+BG6g==
|
integrity sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types ">=7.24.0 <7.24.7"
|
undici-types ">=7.24.0 <7.24.7"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user