harvester-ui-extension/pkg/harvester/components/HarvesterNodeSelector.vue
Andy Lee 5985913f5e
feat: add Host Networks tab list and edit pages (#920)
* feat: add Host Network tab

Signed-off-by: Andy Lee <andy.lee@suse.com>

* feat: add Host Network edit page

Signed-off-by: Andy Lee <andy.lee@suse.com>

* feat: add node selector tab

Signed-off-by: Andy Lee <andy.lee@suse.com>

* refactor: copilot review

Signed-off-by: Andy Lee <andy.lee@suse.com>

* fix: lint

Signed-off-by: Andy Lee <andy.lee@suse.com>

* fix: copilot review

Signed-off-by: Andy Lee <andy.lee@suse.com>

* refactor: add warning message if addon is not enabled or already hasone

Signed-off-by: Andy Lee <andy.lee@suse.com>

* refactor: some wordings in en-us.yaml

Signed-off-by: Andy Lee <andy.lee@suse.com>

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
2026-06-16 10:57:15 +08:00

187 lines
4.5 KiB
Vue

<script>
import { Banner } from '@components/Banner';
import MatchExpressions from '@shell/components/form/MatchExpressions';
import ResourceTable from '@shell/components/ResourceTable';
import { _EDIT } from '@shell/config/query-params';
import { convert, simplify, matching as selectorMatching } from '@shell/utils/selector';
import throttle from 'lodash/throttle';
import { NODE } from '@shell/config/types';
import { NAME, AGE } from '@shell/config/table-headers';
export default {
name: 'HarvesterNodeSelector',
components: {
Banner,
MatchExpressions,
ResourceTable,
},
props: {
mode: {
type: String,
default: _EDIT,
},
value: {
type: Object,
required: true,
},
},
async fetch() {
this.updateMatchingResources();
},
data() {
const inStore = this.$store.getters['currentProduct'].inStore;
return {
matchingResources: {
matched: 0,
matches: [],
none: true,
sample: null,
total: 0,
},
tableHeaders: [
NAME,
{
name: 'host-ip',
labelKey: 'tableHeaders.hostIp',
search: ['internalIp'],
value: 'internalIp',
sort: ['internalIp'],
align: 'center',
},
{
name: 'cpuManager',
labelKey: 'harvester.tableHeaders.cpuManager',
value: 'id',
formatter: 'HarvesterCPUPinning',
width: 150,
align: 'center',
},
{
name: 'diskState',
labelKey: 'tableHeaders.diskState',
value: 'diskState',
formatter: 'HarvesterDiskState',
width: 130,
},
AGE,
],
inStore,
};
},
watch: {
value: {
handler: 'updateMatchingResources',
deep: true,
},
allResourcesInScope() {
this.updateMatchingResources();
},
},
computed: {
schema() {
return this.$store.getters[`${ this.inStore }/schemaFor`](NODE);
},
selectorExpressions: {
get() {
return convert(
this.value.matchLabels || {},
this.value.matchExpressions || []
);
},
set(selectorExpressions) {
const { matchLabels, matchExpressions } = simplify(selectorExpressions);
this.value['matchLabels'] = matchLabels;
this.value['matchExpressions'] = matchExpressions;
this.updateMatchingResources();
},
},
allNodes() {
return this.$store.getters[`${ this.inStore }/all`](NODE) || [];
},
allResourcesInScope() {
return this.allNodes.length;
},
},
methods: {
updateMatchingResources: throttle(function() {
const expressions = this.selectorExpressions;
const allNodes = this.allNodes;
// Empty expressions with no key = no match
const hasValidExpression = expressions.length > 0 && expressions.every((e) => !!e.key);
if (!hasValidExpression) {
this.matchingResources = {
matched: 0,
matches: [],
none: true,
sample: null,
total: allNodes.length,
};
return;
}
const matches = selectorMatching(allNodes, expressions, 'metadata.labels');
this.matchingResources = {
matched: matches.length,
matches,
none: matches.length === 0,
sample: matches[0]?.nameDisplay || null,
total: allNodes.length,
};
}, 100, { trailing: true })
},
};
</script>
<template>
<div>
<div class="row">
<div class="col span-12">
<MatchExpressions
v-model:value="selectorExpressions"
:mode="mode"
:show-remove="false"
:type="'node'"
:target-resources="allResourcesInScope"
/>
</div>
</div>
<div class="row">
<div class="col span-12">
<Banner :color="(matchingResources.none ? 'warning' : 'success')">
<span v-clean-html="t('generic.selectors.matchingResources.matchesSome', matchingResources)" />
</Banner>
</div>
</div>
<div class="row">
<div class="col span-12">
<ResourceTable
:rows="matchingResources.matches"
:headers="tableHeaders"
key-field="id"
:table-actions="false"
:row-actions="false"
:schema="schema"
:groupable="false"
:search="false"
/>
</div>
</div>
</div>
</template>