mirror of https://github.com/rancher/dashboard.git
456 lines
11 KiB
Vue
456 lines
11 KiB
Vue
<script lang="ts">
|
|
import { PropType } from 'vue';
|
|
import { isEmpty } from 'lodash';
|
|
import { checkSchemasForFindAllHash } from '@shell/utils/auth';
|
|
import { isHarvesterCluster } from '@shell/utils/cluster';
|
|
import { FLEET } from '@shell/config/types';
|
|
import FleetUtils from '@shell/utils/fleet';
|
|
import { Expression, Selector, Target, TargetMode } from '@shell/types/fleet';
|
|
import { _CREATE, _EDIT, _VIEW } from '@shell/config/query-params';
|
|
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
|
|
import MatchExpressions from '@shell/components/form/MatchExpressions.vue';
|
|
import { Banner } from '@components/Banner';
|
|
import { RcButton } from '@components/RcButton';
|
|
import RadioGroup from '@components/Form/Radio/RadioGroup.vue';
|
|
import TargetsList from '@shell/components/fleet/FleetClusterTargets/TargetsList.vue';
|
|
|
|
export interface Cluster {
|
|
name: string,
|
|
nameDisplay: string
|
|
}
|
|
|
|
interface DataType {
|
|
targetMode: TargetMode,
|
|
allClusters: any[],
|
|
selectedClusters: string[],
|
|
clusterSelectors: Selector[],
|
|
key: number,
|
|
}
|
|
|
|
const excludeHarvesterRule = FleetUtils.Application.excludeHarvesterRule;
|
|
|
|
export default {
|
|
|
|
name: 'FleetClusterTargets',
|
|
|
|
emits: ['update:value', 'created'],
|
|
|
|
components: {
|
|
Banner,
|
|
LabeledSelect,
|
|
MatchExpressions,
|
|
RadioGroup,
|
|
RcButton,
|
|
TargetsList,
|
|
},
|
|
|
|
props: {
|
|
targets: {
|
|
type: Array as PropType<Target[]>,
|
|
default: () => [],
|
|
},
|
|
|
|
matching: {
|
|
type: Array as PropType<Cluster[]>,
|
|
default: () => [],
|
|
},
|
|
|
|
namespace: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
|
|
mode: {
|
|
type: String,
|
|
default: _EDIT
|
|
},
|
|
|
|
created: {
|
|
type: String as PropType<TargetMode>,
|
|
default: '',
|
|
},
|
|
},
|
|
|
|
async fetch() {
|
|
const hash = await checkSchemasForFindAllHash({
|
|
allClusters: {
|
|
inStoreType: 'management',
|
|
type: FLEET.CLUSTER
|
|
}
|
|
}, this.$store) as { allClusters: any[] };
|
|
|
|
this.allClusters = hash.allClusters || [];
|
|
},
|
|
|
|
data(): DataType {
|
|
return {
|
|
targetMode: 'all',
|
|
allClusters: [],
|
|
selectedClusters: [],
|
|
clusterSelectors: [],
|
|
key: 0 // Generates a unique key to handle Targets
|
|
};
|
|
},
|
|
|
|
mounted() {
|
|
this.fromTargets();
|
|
|
|
if (this.mode === _CREATE) {
|
|
this.update();
|
|
|
|
// Restore the targetMode from parent component; this is the case of edit targets in CREATE mode, go to YAML editor and come back to the form
|
|
this.targetMode = this.created || 'all';
|
|
} else {
|
|
this.targetMode = FleetUtils.Application.getTargetMode(this.targets || [], this.namespace);
|
|
}
|
|
},
|
|
|
|
watch: {
|
|
namespace() {
|
|
if (this.mode === _CREATE) {
|
|
this.reset();
|
|
}
|
|
|
|
if (this.mode !== _VIEW) {
|
|
this.update();
|
|
}
|
|
},
|
|
},
|
|
|
|
computed: {
|
|
targetModeOptions(): { label: string, value: TargetMode }[] {
|
|
if (this.namespace === 'fleet-local') {
|
|
return [{
|
|
label: 'local cluster',
|
|
value: 'local'
|
|
}];
|
|
}
|
|
|
|
const out: { label: string, value: TargetMode }[] = [
|
|
{
|
|
label: 'All Clusters in the workspace',
|
|
value: 'all',
|
|
},
|
|
{
|
|
label: 'No clusters',
|
|
value: 'none'
|
|
},
|
|
];
|
|
|
|
if (this.clustersOptions.length) {
|
|
out.push({
|
|
label: 'Manually selected clusters',
|
|
value: 'clusters'
|
|
});
|
|
}
|
|
|
|
return out;
|
|
},
|
|
|
|
clustersOptions() {
|
|
return this.allClusters
|
|
.filter((x) => x.metadata.namespace === this.namespace && !isHarvesterCluster(x))
|
|
.map((x) => ({ label: x.nameDisplay, value: x.metadata.name }));
|
|
},
|
|
|
|
isLocal() {
|
|
return this.namespace === 'fleet-local';
|
|
},
|
|
|
|
isView() {
|
|
return this.mode === _VIEW;
|
|
}
|
|
},
|
|
|
|
methods: {
|
|
selectTargetMode(value: TargetMode) {
|
|
this.targetMode = value;
|
|
|
|
// Save the current targetMode in parent component
|
|
this.$emit('created', this.targetMode);
|
|
|
|
this.update();
|
|
},
|
|
|
|
selectClusters(list: string[]) {
|
|
this.selectedClusters = list;
|
|
|
|
this.update();
|
|
},
|
|
|
|
addMatchExpressions() {
|
|
const neu = { key: this.key++ };
|
|
|
|
this.clusterSelectors.push(neu);
|
|
|
|
// Focus first element in MatchExpression
|
|
this.$nextTick(() => {
|
|
const matchExpression = (this.$refs[`match-expression-${ neu.key }`] as HTMLElement[])?.[0];
|
|
|
|
matchExpression?.focus();
|
|
});
|
|
|
|
this.update();
|
|
},
|
|
|
|
updateMatchExpressions(index: number, value: Selector, key?: number) {
|
|
this.clusterSelectors[index] = { ...value, key };
|
|
|
|
this.update();
|
|
},
|
|
|
|
removeMatchExpressions(key?: number) {
|
|
this.clusterSelectors = this.clusterSelectors.filter((f) => f.key !== key);
|
|
|
|
this.update();
|
|
},
|
|
|
|
update() {
|
|
const targets = this.toTargets();
|
|
|
|
this.$emit('update:value', targets);
|
|
},
|
|
|
|
fromTargets() {
|
|
if (!this.targets?.length) {
|
|
return;
|
|
}
|
|
|
|
for (const target of this.targets) {
|
|
const {
|
|
clusterName,
|
|
clusterSelector,
|
|
clusterGroup,
|
|
clusterGroupSelector,
|
|
} = target;
|
|
|
|
// If clusterGroup or clusterGroupSelector are defined, targets are marked as complex and won't handle by the UI
|
|
if (clusterGroup || clusterGroupSelector) {
|
|
return;
|
|
}
|
|
|
|
if (clusterName) {
|
|
this.selectedClusters.push(clusterName);
|
|
}
|
|
|
|
if (!isEmpty(clusterSelector)) {
|
|
const neu = {
|
|
key: this.key++,
|
|
matchLabels: clusterSelector.matchLabels,
|
|
matchExpressions: clusterSelector.matchExpressions?.filter((f) => f.key !== excludeHarvesterRule.clusterSelector.matchExpressions[0].key)
|
|
};
|
|
|
|
if (neu.matchLabels || neu.matchExpressions?.length) {
|
|
this.clusterSelectors.push(neu);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
toTargets(): Target[] | undefined {
|
|
switch (this.targetMode) {
|
|
case 'none':
|
|
return undefined;
|
|
case 'all':
|
|
return [excludeHarvesterRule];
|
|
case 'clusters':
|
|
return this.normalizeTargets(this.selectedClusters, this.clusterSelectors);
|
|
case 'advanced':
|
|
case 'local':
|
|
return this.targets;
|
|
}
|
|
},
|
|
|
|
normalizeTargets(selected: string[], clusterMatchExpressions: Selector[]) {
|
|
const targets: Target[] = [];
|
|
|
|
selected.forEach((clusterName) => {
|
|
targets.push({ clusterName });
|
|
});
|
|
|
|
clusterMatchExpressions.forEach((elem) => {
|
|
const { matchLabels: labels, matchExpressions: expressions } = elem || {};
|
|
|
|
if (labels || expressions) {
|
|
const matchLabels = Object.keys(labels || {}).reduce((acc, key) => {
|
|
if (key && labels) {
|
|
return {
|
|
...acc,
|
|
[key]: labels[key],
|
|
};
|
|
}
|
|
|
|
return acc;
|
|
}, {});
|
|
|
|
const matchExpressions = (expressions || []).reduce((acc, expression) => {
|
|
// Do not display Harvester-exclude rule
|
|
if (expression.key && expression.key !== excludeHarvesterRule.clusterSelector.matchExpressions[0].key) {
|
|
return [
|
|
...acc,
|
|
expression
|
|
];
|
|
}
|
|
|
|
return acc;
|
|
}, [] as Expression[]);
|
|
|
|
const clusterSelector: Selector = {};
|
|
|
|
if (!isEmpty(matchLabels)) {
|
|
clusterSelector.matchLabels = matchLabels;
|
|
}
|
|
|
|
if (matchExpressions.length) {
|
|
clusterSelector.matchExpressions = matchExpressions;
|
|
}
|
|
|
|
if (!isEmpty(clusterSelector)) {
|
|
targets.push({ clusterSelector });
|
|
}
|
|
}
|
|
});
|
|
|
|
if (targets.length) {
|
|
return targets;
|
|
}
|
|
|
|
return undefined;
|
|
},
|
|
|
|
reset() {
|
|
this.targetMode = 'all';
|
|
this.selectedClusters = [];
|
|
this.clusterSelectors = [];
|
|
}
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
v-if="targetMode !== 'advanced'"
|
|
class="row"
|
|
>
|
|
<RadioGroup
|
|
name="targetMode"
|
|
data-testid="fleet-target-cluster-radio-button"
|
|
:value="isLocal ? 'local' : targetMode"
|
|
:mode="mode"
|
|
:options="targetModeOptions"
|
|
:disabled="isView"
|
|
@update:value="selectTargetMode"
|
|
/>
|
|
</div>
|
|
|
|
<Banner
|
|
v-if="targetMode === 'advanced'"
|
|
class="row"
|
|
color="warning"
|
|
:label="t('fleet.clusterTargets.advancedConfigs')"
|
|
/>
|
|
|
|
<div
|
|
v-if="targetMode === 'clusters'"
|
|
class="row mt-20"
|
|
>
|
|
<div class="col span-9">
|
|
<h3 class="m-0">
|
|
{{ t('fleet.clusterTargets.title') }}
|
|
</h3>
|
|
<LabeledSelect
|
|
data-testid="fleet-target-cluster-name-selector"
|
|
class="mmt-4"
|
|
:value="selectedClusters"
|
|
:label="t('fleet.clusterTargets.label')"
|
|
:options="clustersOptions"
|
|
:taggable="true"
|
|
:close-on-select="false"
|
|
:mode="mode"
|
|
:multiple="true"
|
|
:placeholder="t('fleet.clusterTargets.placeholders.selectMultiple')"
|
|
@update:value="selectClusters"
|
|
/>
|
|
<div class="mmt-8">
|
|
<h3 class="m-0">
|
|
{{ t('fleet.clusterTargets.rules.title') }}
|
|
</h3>
|
|
<div
|
|
v-for="(selector, i) in clusterSelectors"
|
|
:key="selector.key"
|
|
class="match-expressions-container mmt-4"
|
|
>
|
|
<MatchExpressions
|
|
:ref="`match-expression-${ selector.key }`"
|
|
class="body"
|
|
:value="selector"
|
|
:mode="mode"
|
|
:initial-empty-row="true"
|
|
:label-key="t('fleet.clusterTargets.rules.labelKey')"
|
|
:add-icon="'icon-plus'"
|
|
:add-class="'btn-sm'"
|
|
@update:value="updateMatchExpressions(i, $event, selector.key)"
|
|
/>
|
|
<RcButton
|
|
small
|
|
link
|
|
@click="removeMatchExpressions(selector.key)"
|
|
>
|
|
<i class="icon icon-x" />
|
|
</RcButton>
|
|
</div>
|
|
<RcButton
|
|
small
|
|
secondary
|
|
class="mmt-6"
|
|
@click="addMatchExpressions"
|
|
>
|
|
<i class="icon icon-plus" />
|
|
<span>{{ t('fleet.clusterTargets.rules.addSelector') }}</span>
|
|
</RcButton>
|
|
</div>
|
|
</div>
|
|
<div class="col span-3">
|
|
<TargetsList
|
|
class="target-list"
|
|
:clusters="matching"
|
|
:empty-label="t('fleet.clusterTargets.rules.matching.placeholder')"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-if="targetMode === 'all' && !isLocal"
|
|
class="row"
|
|
>
|
|
<div class="col span-6">
|
|
<TargetsList
|
|
class="target-list mt-20"
|
|
:clusters="matching"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang="scss" scoped>
|
|
.match-expressions-container {
|
|
display: flex;
|
|
align-items: start;
|
|
border: 1px solid var(--border);
|
|
border-radius: 5px;
|
|
|
|
.body {
|
|
padding: 15px;
|
|
width: 100%;
|
|
}
|
|
|
|
.btn {
|
|
margin: 5px;
|
|
}
|
|
}
|
|
|
|
.target-list {
|
|
max-height: 250px;
|
|
}
|
|
</style>
|