mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-04 21:26:08 +08:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
535a6067c9 | ||
|
|
ace28d1c0e | ||
|
|
1df5af32b8 | ||
|
|
fbd4a1108f | ||
|
|
823a88c85b | ||
|
|
3a2c6e3a3d | ||
|
|
be6e9b5f06 | ||
|
|
a88bed9eb4 | ||
|
|
f97f14c811 | ||
|
|
f8a29a1c54 |
@@ -8,12 +8,16 @@
|
|||||||
- [ ] Yes, the backend owner is @
|
- [ ] Yes, the backend owner is @
|
||||||
- [ ] No, frontend-only.
|
- [ ] No, frontend-only.
|
||||||
|
|
||||||
### Related Issue #
|
### Related Issue
|
||||||
<!-- Link the issue as harvester/harvester#<issue_number> -->
|
<!-- Link the issue as harvester/harvester#<issue_number> -->
|
||||||
harvester/harvester#<issue_number>
|
harvester/harvester#<issue_number>
|
||||||
|
|
||||||
|
|
||||||
### Test screenshot or video (Required)
|
### Test Steps
|
||||||
<!-- Attach screenshot or video of the changes and eventual comparison if you find it necessary -->
|
<!-- Provide the test steps for reviewer to verify the issue -->
|
||||||
|
1. Go to ... page
|
||||||
|
|
||||||
|
### Test screenshot or video
|
||||||
|
<!-- Attach screenshot or video of the changes and eventual comparison -->
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
name: Build Standalone on PR Merge
|
name: Build Standalone on PR Merge
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch: # manually trigger on Github Actions only support PR branch pushed to harvester/harvester-ui-extension
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- 'release-harvester-v*'
|
- 'release-harvester-v*'
|
||||||
- '*-dev'
|
- '*-dev'
|
||||||
- bump_shell_rc7_demo
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
@@ -30,3 +30,35 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
with:
|
with:
|
||||||
CI_BRANCH: ${{github.ref_name}}
|
CI_BRANCH: ${{github.ref_name}}
|
||||||
|
comment-on-pr:
|
||||||
|
name: Comment build result on PR
|
||||||
|
# Only for manual runs so the existing push / pull_request flows are untouched.
|
||||||
|
if: github.event_name == 'workflow_dispatch'
|
||||||
|
needs:
|
||||||
|
- build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- name: Add build artifact comment to the PR opened from this branch
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
BRANCH: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
# Match scripts/version: sanitize the branch and map main -> latest.
|
||||||
|
if [ "$BRANCH" = "main" ]; then
|
||||||
|
DIR="latest"
|
||||||
|
else
|
||||||
|
DIR=$(printf '%s' "$BRANCH" | sed -E 's/[^a-zA-Z0-9.-]+/-/g')
|
||||||
|
fi
|
||||||
|
URL="https://releases.rancher.com/harvester-ui/dashboard/${DIR}/index.html"
|
||||||
|
|
||||||
|
PR=$(gh pr list --repo "$GITHUB_REPOSITORY" --state open --base main --head "$BRANCH" --json number --jq '.[0].number // empty')
|
||||||
|
if [ -z "$PR" ]; then
|
||||||
|
echo "No open PR found for branch $BRANCH, skipping comment."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
BODY=$(printf '🚀 **UI Artifact Build Status:** `SUCCESS` ✅\n> ui-index: %s\n> ui-source: external' "$URL")
|
||||||
|
gh pr comment "$PR" --repo "$GITHUB_REPOSITORY" --body "$BODY"
|
||||||
|
|||||||
@@ -213,9 +213,15 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
clearSearchHighlights() {
|
clearSearchHighlights() {
|
||||||
this.terminal?.clearSelection();
|
|
||||||
this.searchAddon?.clearActiveDecoration();
|
this.searchAddon?.clearActiveDecoration();
|
||||||
this.searchAddon?.clearDecorations();
|
this.searchAddon?.clearDecorations();
|
||||||
|
this.terminal?.clearSelection();
|
||||||
|
|
||||||
|
// Disposing the search decorations doesn't repaint the webgl/canvas
|
||||||
|
// layer, so force a refresh to remove the lingering highlight.
|
||||||
|
if (this.terminal) {
|
||||||
|
this.terminal.refresh(0, this.terminal.rows - 1);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
findNext() {
|
findNext() {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import jsyaml from 'js-yaml';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import Tabbed from '@shell/components/Tabbed';
|
import Tabbed from '@shell/components/Tabbed';
|
||||||
import Tab from '@shell/components/Tabbed/Tab';
|
import Tab from '@shell/components/Tabbed/Tab';
|
||||||
@@ -177,8 +178,17 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async saveVMT(buttonCb) {
|
async saveVMT(buttonCb) {
|
||||||
|
this.errors = [];
|
||||||
this.parseVM();
|
this.parseVM();
|
||||||
|
|
||||||
|
this.validateCloudInit();
|
||||||
|
|
||||||
|
if (this.errors.length) {
|
||||||
|
buttonCb(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const templates = await this.$store.dispatch('harvester/findAll', { type: HCI.VM_TEMPLATE });
|
const templates = await this.$store.dispatch('harvester/findAll', { type: HCI.VM_TEMPLATE });
|
||||||
const template = templates.find( (O) => O.metadata.name === this.templateValue.metadata.name);
|
const template = templates.find( (O) => O.metadata.name === this.templateValue.metadata.name);
|
||||||
|
|
||||||
@@ -237,6 +247,24 @@ export default {
|
|||||||
this.$refs.yamlEditor?.refresh();
|
this.$refs.yamlEditor?.refresh();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
validateCloudInit() {
|
||||||
|
if (this.userScript) {
|
||||||
|
try {
|
||||||
|
jsyaml.load(this.userScript);
|
||||||
|
} catch (e) {
|
||||||
|
this.errors.push(this.t('harvester.virtualMachine.cloudConfig.user.invalidYaml'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.networkScript) {
|
||||||
|
try {
|
||||||
|
jsyaml.load(this.networkScript);
|
||||||
|
} catch (e) {
|
||||||
|
this.errors.push(this.t('harvester.virtualMachine.cloudConfig.network.invalidYaml'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import Loading from '@shell/components/Loading';
|
|||||||
import CreateEditView from '@shell/mixins/create-edit-view';
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
||||||
import { RadioGroup } from '@components/Form/Radio';
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
import { NETWORK_PROTOCOL, NETWORK_TYPE } from '@pkg/harvester/config/types';
|
import { NETWORK_PROTOCOL, NETWORK_TYPE } from '@pkg/harvester/config/types';
|
||||||
import { set } from '@shell/utils/object';
|
import { set, remove } from '@shell/utils/object';
|
||||||
import ArrayList from '@shell/components/form/ArrayList';
|
import ArrayList from '@shell/components/form/ArrayList';
|
||||||
import { allHash } from '@shell/utils/promise';
|
import { allHash } from '@shell/utils/promise';
|
||||||
import { HCI } from '../../types';
|
import { HCI } from '../../types';
|
||||||
@@ -142,10 +142,12 @@ export default {
|
|||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
const vlans = this.$store.getters[`${ inStore }/all`](HCI.VLAN) || [];
|
const vlans = this.$store.getters[`${ inStore }/all`](HCI.VLAN) || [];
|
||||||
|
|
||||||
return vlans.map((vlan) => ({
|
const options = vlans.map((vlan) => ({
|
||||||
label: vlan.id,
|
label: vlan.id,
|
||||||
value: vlan.id,
|
value: vlan.id,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
return [{ label: this.t('generic.none'), value: '' }, ...options];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -160,6 +162,14 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
onVlanChange(value) {
|
||||||
|
if (value === '') {
|
||||||
|
remove(this.value.spec, 'vlan');
|
||||||
|
} else {
|
||||||
|
set(this.value, 'spec.vlan', value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async saveSubnet(buttonCb) {
|
async saveSubnet(buttonCb) {
|
||||||
const errors = [];
|
const errors = [];
|
||||||
const name = this.value?.metadata?.name;
|
const name = this.value?.metadata?.name;
|
||||||
@@ -283,12 +293,13 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
<div class="col span-6">
|
<div class="col span-6">
|
||||||
<LabeledSelect
|
<LabeledSelect
|
||||||
v-model:value="value.spec.vlan"
|
:value="value.spec.vlan ?? ''"
|
||||||
class="mb-20"
|
class="mb-20"
|
||||||
:options="vlanOptions"
|
:options="vlanOptions"
|
||||||
:placeholder="t('harvester.subnet.vlan.placeholder')"
|
:placeholder="t('harvester.subnet.vlan.placeholder')"
|
||||||
:label="t('harvester.subnet.vlan.label')"
|
:label="t('harvester.subnet.vlan.label')"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
|
@update:value="onVlanChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import jsyaml from 'js-yaml';
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import Tabbed from '@shell/components/Tabbed';
|
import Tabbed from '@shell/components/Tabbed';
|
||||||
@@ -379,6 +380,7 @@ export default {
|
|||||||
|
|
||||||
this.validateCPUMemory();
|
this.validateCPUMemory();
|
||||||
this.validateWindowsSysprep();
|
this.validateWindowsSysprep();
|
||||||
|
this.validateCloudInit();
|
||||||
|
|
||||||
// block create VM flow if has validation errors
|
// block create VM flow if has validation errors
|
||||||
if (this.errors.length) {
|
if (this.errors.length) {
|
||||||
@@ -424,6 +426,24 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
validateCloudInit() {
|
||||||
|
if (this.userScript) {
|
||||||
|
try {
|
||||||
|
jsyaml.load(this.userScript);
|
||||||
|
} catch (e) {
|
||||||
|
this.errors.push(this.t('harvester.virtualMachine.cloudConfig.user.invalidYaml'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.networkScript) {
|
||||||
|
try {
|
||||||
|
jsyaml.load(this.networkScript);
|
||||||
|
} catch (e) {
|
||||||
|
this.errors.push(this.t('harvester.virtualMachine.cloudConfig.network.invalidYaml'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async saveSingle(buttonCb) {
|
async saveSingle(buttonCb) {
|
||||||
this.parseVM();
|
this.parseVM();
|
||||||
this.value.spec.template.spec.hostname = this.hostname ? this.hostname : this.value.metadata.name;
|
this.value.spec.template.spec.hostname = this.hostname ? this.hostname : this.value.metadata.name;
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ export default {
|
|||||||
RANCHER_TYPES,
|
RANCHER_TYPES,
|
||||||
fvFormRuleSets: [{ path: 'spec.displayName', rules: ['required'] }],
|
fvFormRuleSets: [{ path: 'spec.displayName', rules: ['required'] }],
|
||||||
canEditPSPBindings: true,
|
canEditPSPBindings: true,
|
||||||
|
isQuotasValid: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -190,15 +191,13 @@ export default {
|
|||||||
this['membershipUpdate'] = update;
|
this['membershipUpdate'] = update;
|
||||||
},
|
},
|
||||||
|
|
||||||
removeQuota(key) {
|
validateQuotas(isValid) {
|
||||||
['resourceQuota', 'namespaceDefaultResourceQuota'].forEach((specProp) => {
|
this.isQuotasValid = isValid;
|
||||||
if (this.value?.spec[specProp]?.limit && this.value?.spec[specProp]?.limit[key]) {
|
},
|
||||||
delete this.value?.spec[specProp]?.limit[key];
|
|
||||||
}
|
onQuotasInput({ projectLimit, nsLimit } = {}) {
|
||||||
if (this.value?.spec[specProp]?.usedLimit && this.value?.spec[specProp]?.usedLimit[key]) {
|
this.value.spec.resourceQuota = { ...this.value.spec.resourceQuota, limit: projectLimit };
|
||||||
delete this.value?.spec[specProp]?.usedLimit[key];
|
this.value.spec.namespaceDefaultResourceQuota = { ...this.value.spec.namespaceDefaultResourceQuota, limit: nsLimit };
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -212,7 +211,7 @@ export default {
|
|||||||
:resource="value"
|
:resource="value"
|
||||||
:subtypes="[]"
|
:subtypes="[]"
|
||||||
:can-yaml="false"
|
:can-yaml="false"
|
||||||
:validation-passed="fvFormIsValid"
|
:validation-passed="fvFormIsValid && isQuotasValid"
|
||||||
@error="e=>errors=e"
|
@error="e=>errors=e"
|
||||||
@finish="save"
|
@finish="save"
|
||||||
@cancel="done"
|
@cancel="done"
|
||||||
@@ -267,10 +266,11 @@ export default {
|
|||||||
:weight="9"
|
:weight="9"
|
||||||
>
|
>
|
||||||
<ResourceQuota
|
<ResourceQuota
|
||||||
v-model:value="value"
|
:value="value"
|
||||||
:mode="canEditTabElements"
|
:mode="canEditTabElements"
|
||||||
:types="isStandaloneHarvester ? HARVESTER_TYPES : RANCHER_TYPES"
|
:types="isStandaloneHarvester ? HARVESTER_TYPES : RANCHER_TYPES"
|
||||||
@remove="removeQuota"
|
@input="onQuotasInput"
|
||||||
|
@validationChanged="validateQuotas"
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
|
|||||||
@@ -830,10 +830,12 @@ harvester:
|
|||||||
label: User Data Template
|
label: User Data Template
|
||||||
title: "User Data:"
|
title: "User Data:"
|
||||||
tip: "You can specify user data to configure an instance or run a configuration script during launch. If you launch more than one instance at a time, the user data is available to all the instances in that reservation. <a href='https://cloudinit.readthedocs.io/en/latest/topics/examples.html' target='_blank'>Learn more</a>"
|
tip: "You can specify user data to configure an instance or run a configuration script during launch. If you launch more than one instance at a time, the user data is available to all the instances in that reservation. <a href='https://cloudinit.readthedocs.io/en/latest/topics/examples.html' target='_blank'>Learn more</a>"
|
||||||
|
invalidYaml: 'Invalid User Data YAML syntax'
|
||||||
network:
|
network:
|
||||||
label: Network Data Template
|
label: Network Data Template
|
||||||
title: "Network Data:"
|
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>"
|
||||||
|
invalidYaml: 'Invalid Network Data YAML syntax'
|
||||||
sysprep:
|
sysprep:
|
||||||
title: Windows Sysprep Configuration
|
title: Windows Sysprep Configuration
|
||||||
description: "Configure Windows automated installation using autounattend.xml. The configuration will be stored in a Kubernetes Secret and mounted as a CD-ROM during installation. <a href='https://docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/automate-windows-setup' target='_blank'>Learn more</a>"
|
description: "Configure Windows automated installation using autounattend.xml. The configuration will be stored in a Kubernetes Secret and mounted as a CD-ROM during installation. <a href='https://docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/automate-windows-setup' target='_blank'>Learn more</a>"
|
||||||
|
|||||||
Reference in New Issue
Block a user