mirror of https://github.com/rancher/dashboard.git
Merge branch 'master' into download.yaml.overview
This commit is contained in:
commit
aadb803bc5
|
|
@ -4,6 +4,7 @@ locale:
|
|||
|
||||
generic:
|
||||
customize: Customize
|
||||
comingSoon: Coming Soon
|
||||
|
||||
header:
|
||||
backToRancher: "← Back to Rancher"
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@ import {
|
|||
_UNFLAG,
|
||||
} from '@/config/query-params';
|
||||
|
||||
// import { mapPref, DIFF } from '@/store/prefs';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Footer,
|
||||
|
|
|
|||
|
|
@ -27,9 +27,17 @@ export default {
|
|||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
optionKey: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
optionLabel: {
|
||||
type: String,
|
||||
default: 'label'
|
||||
},
|
||||
optionKey: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,11 @@ export default {
|
|||
},
|
||||
|
||||
computed: {
|
||||
containerClass() {
|
||||
return this.displaySideBySide
|
||||
? 'row'
|
||||
: '';
|
||||
},
|
||||
sectionClass() {
|
||||
return this.displaySideBySide
|
||||
? 'col span-6'
|
||||
|
|
@ -39,7 +44,7 @@ export default {
|
|||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="row">
|
||||
<div :class="containerClass">
|
||||
<div :class="sectionClass">
|
||||
<KeyValue
|
||||
key="labels"
|
||||
|
|
|
|||
|
|
@ -182,7 +182,7 @@ export default {
|
|||
v-model="name"
|
||||
label="Name"
|
||||
:disabled="nameDisabled"
|
||||
:mode="nameMode"
|
||||
:mode="mode"
|
||||
:min-height="30"
|
||||
/>
|
||||
</slot>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,184 @@
|
|||
<script>
|
||||
import LabeledInput from '@/components/form/LabeledInput';
|
||||
import LabeledSelect from '@/components/form/LabeledSelect';
|
||||
|
||||
export default {
|
||||
components: { LabeledInput, LabeledSelect },
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'edit'
|
||||
},
|
||||
|
||||
// weighted selector terms are nested differently
|
||||
isWeighted: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
// whether or not to show removal 'x' button in upper right (probably shouldn't show if node selectors are required)
|
||||
showRemove: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
const ops = [{ label: '<', value: 'Lt' }, { label: '>', value: 'Gt' }, { label: 'is set', value: 'Exists' }, { label: 'is not set', value: 'DoesNotExist' }, { label: 'in list', value: 'In' }, { label: 'not in list', value: 'NotIn' }];
|
||||
|
||||
let rules;
|
||||
|
||||
rules = this.value.matchExpressions || [];
|
||||
|
||||
if (this.isWeighted) {
|
||||
rules = this.value?.preference?.matchExpressions || [];
|
||||
}
|
||||
|
||||
rules = rules.map((rule) => {
|
||||
if (rule.values) {
|
||||
rule.values.join(',');
|
||||
}
|
||||
|
||||
return rule;
|
||||
});
|
||||
|
||||
if (!rules.length) {
|
||||
rules.push({ values: '' });
|
||||
}
|
||||
|
||||
return {
|
||||
ops, rules, custom: []
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
isView() {
|
||||
return this.mode === 'view';
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
removeRule(idx) {
|
||||
this.rules.splice(idx, 1);
|
||||
this.update();
|
||||
},
|
||||
|
||||
addRule() {
|
||||
this.rules.push({ values: '' });
|
||||
},
|
||||
|
||||
update() {
|
||||
this.$nextTick(() => {
|
||||
const matchExpressions = [
|
||||
...this.rules.map((rule) => {
|
||||
const matchExpression = { key: rule.key };
|
||||
|
||||
if (rule.operator) {
|
||||
matchExpression.operator = rule.operator;
|
||||
}
|
||||
if (rule.values && rule.operator !== 'Exists' && rule.operator !== 'DoesNotExist') {
|
||||
matchExpression.values = (rule.values || []).split(',');
|
||||
}
|
||||
|
||||
return matchExpression;
|
||||
})];
|
||||
|
||||
let out = { matchExpressions };
|
||||
|
||||
if (this.isWeighted) {
|
||||
out = { preference: { matchExpressions }, weight: this.value.weight || 1 };
|
||||
}
|
||||
this.$emit('input', out);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :style="{'position':'relative'}" @input="update">
|
||||
<button v-if="showRemove" id="remove-btn" class="btn btn-lg role-link">
|
||||
<i class="icon icon-lg icon-x" @click="$emit('remove')" />
|
||||
</button>
|
||||
<div class="rule-row headers">
|
||||
<span>Key</span>
|
||||
<span>Operator</span>
|
||||
<span>Value</span>
|
||||
</div>
|
||||
<div v-for="(rule, i) in rules" :key="i">
|
||||
<div class="rule-row">
|
||||
<div class="">
|
||||
<LabeledInput v-model="rule.key" :mode="mode" />
|
||||
</div>
|
||||
<LabeledSelect
|
||||
v-model="rule.operator"
|
||||
class=""
|
||||
:options="ops"
|
||||
:mode="mode"
|
||||
@input="update"
|
||||
/>
|
||||
<div>
|
||||
<LabeledInput v-model="rule.values" :mode="mode" :disabled="rule.operator==='Exists' && rule.operator==='DoesNotExist'" />
|
||||
</div>
|
||||
<button
|
||||
v-if="!isView"
|
||||
type="button"
|
||||
class="btn btn-sm role-link col remove-rule-button"
|
||||
:style="{padding:'0px'}"
|
||||
:disabled="mode==='view'"
|
||||
@click="removeRule(i)"
|
||||
>
|
||||
<i class="icon icon-minus icon-lg" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button v-if="!isView" type="button" class="btn role-tertiary add" @click="addRule">
|
||||
Add Rule
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang='scss'>
|
||||
#operator {
|
||||
& .vs__dropdown-option{
|
||||
padding: 3px 6px 3px 6px !important
|
||||
}
|
||||
}
|
||||
|
||||
#remove-btn{
|
||||
padding: 8px;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 0px;
|
||||
& .icon-x {
|
||||
transform: scale(1.2)
|
||||
}
|
||||
}
|
||||
|
||||
.rule-row {
|
||||
display:grid;
|
||||
grid-template-columns: auto 20% auto 3%;
|
||||
grid-column-gap:10px;
|
||||
margin-bottom:10px;
|
||||
|
||||
&.headers>* {
|
||||
padding:0px 10px 0px 10px;
|
||||
color: var(--input-label)
|
||||
}
|
||||
|
||||
& .labeled-input INPUT[type='text']:not(.view){
|
||||
padding: 9px 0px 9px 0px
|
||||
}
|
||||
}
|
||||
|
||||
.remove-rule-button{
|
||||
justify-content:center;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
<script>
|
||||
import NodeSelectorTerm from './NodeSelectorTerm';
|
||||
|
||||
export default {
|
||||
components: { NodeSelectorTerm },
|
||||
|
||||
props: {
|
||||
// value should be NodeAffinity or VolumeNodeAffinity
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'create'
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
// VolumeNodeAffinity only has 'required' field
|
||||
if (this.value.required) {
|
||||
return { nodeSelectorTerms: this.value.required.nodeSelectorTerms, multipleSelectors: true };
|
||||
} else {
|
||||
const { preferredDuringSchedulingIgnoredDuringExecution = [], requiredDuringSchedulingIgnoredDuringExecution = {} } = this.value;
|
||||
const { nodeSelectorTerms = [] } = requiredDuringSchedulingIgnoredDuringExecution;
|
||||
|
||||
if (!nodeSelectorTerms.length) {
|
||||
nodeSelectorTerms.push({ });
|
||||
}
|
||||
|
||||
return {
|
||||
nodeSelectorTerms,
|
||||
weightedNodeSelectorTerms: preferredDuringSchedulingIgnoredDuringExecution,
|
||||
multipleSelectors: false
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
hasWeighted() {
|
||||
return !!this.weightedNodeSelectorTerms;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row">
|
||||
<div :class="{'col span-6':hasWeighted, 'col span-12':!hasWeighted}">
|
||||
<slot name="title" />
|
||||
<template v-for="(nodeSelector, i) in nodeSelectorTerms">
|
||||
<div :key="i" class="row">
|
||||
<div :key="i" class="col span-12">
|
||||
<NodeSelectorTerm
|
||||
:mode="mode"
|
||||
:show-remove="false"
|
||||
class="node-selector container"
|
||||
:value="nodeSelector"
|
||||
@remove="e=>nodeSelectorTerms.splice(i, 1)"
|
||||
@input="e=>$set(nodeSelectorTerms, i, e)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<button v-if="multipleSelectors" type="button" class="btn btn-sm role-primary" @click="e=>nodeSelectorTerms.push({matchExpressions:[]})">
|
||||
Add Node Selector
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="hasWeighted" class="col span-6">
|
||||
<slot name="title-weighted" />
|
||||
<template v-for="(nodeSelector, i) in weightedNodeSelectorTerms">
|
||||
<div :key="i" class="row">
|
||||
<div :key="i" class="col span-12">
|
||||
<NodeSelectorTerm
|
||||
:mode="mode"
|
||||
class="node-selector container"
|
||||
is-weighted
|
||||
:value="nodeSelector"
|
||||
@remove="e=>weightedNodeSelectorTerms.splice(i, 1)"
|
||||
@input="e=>$set(weightedNodeSelectorTerms, i, e)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<button type="button" class="btn btn-sm role-primary" @click="e=>weightedNodeSelectorTerms.push({matchExpressions:[], weight: 1})">
|
||||
Add Node Selector
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.node-selector {
|
||||
padding: 10px;
|
||||
background-color: var(--body-bg);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -10,6 +10,14 @@ export const STATE = {
|
|||
formatter: 'BadgeState',
|
||||
};
|
||||
|
||||
export const DOWNLOAD = {
|
||||
name: 'download',
|
||||
label: 'Download',
|
||||
value: 'download',
|
||||
canBeVariable: true,
|
||||
align: 'right',
|
||||
};
|
||||
|
||||
export const NAME = {
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
|
|
|
|||
|
|
@ -1,9 +1,16 @@
|
|||
<script>
|
||||
import { DESCRIPTION } from '@/config/labels-annotations';
|
||||
import CreateEditView from '@/mixins/create-edit-view';
|
||||
import DetailTop from '@/components/DetailTop';
|
||||
import Labels from '@/components/form/Labels';
|
||||
import SortableTable from '@/components/SortableTable';
|
||||
import VStack from '@/components/Layout/Stack/VStack';
|
||||
import { downloadFile } from '@/utils/download';
|
||||
import Tab from '@/components/Tabbed/Tab';
|
||||
import Tabbed from '@/components/Tabbed';
|
||||
|
||||
import {
|
||||
DOWNLOAD,
|
||||
KEY,
|
||||
VALUE,
|
||||
STATE,
|
||||
|
|
@ -17,10 +24,15 @@ export default {
|
|||
name: 'DetailConfigMap',
|
||||
components: {
|
||||
DetailTop,
|
||||
Labels,
|
||||
SortableTable,
|
||||
Tab,
|
||||
Tabbed,
|
||||
VStack
|
||||
},
|
||||
|
||||
mixins: [CreateEditView],
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
|
|
@ -29,12 +41,18 @@ export default {
|
|||
},
|
||||
|
||||
data() {
|
||||
const valuesTableHeaders = [
|
||||
{
|
||||
...KEY, sort: false, width: 400
|
||||
},
|
||||
{ ...VALUE, sort: false },
|
||||
];
|
||||
|
||||
return {
|
||||
valuesTableHeaders: [
|
||||
{
|
||||
...KEY, sort: false, width: 400
|
||||
},
|
||||
{ ...VALUE, sort: false },
|
||||
valuesTableHeaders,
|
||||
binaryValuesTableHeaders: [
|
||||
...valuesTableHeaders,
|
||||
DOWNLOAD
|
||||
],
|
||||
relatedWorkloadsHeaders: [
|
||||
STATE,
|
||||
|
|
@ -54,27 +72,15 @@ export default {
|
|||
}));
|
||||
},
|
||||
|
||||
binaryValuesTableRows() {
|
||||
return Object.entries(this.value.binaryData || {}).map(kvp => ({
|
||||
key: kvp[0],
|
||||
value: `${ kvp[1].length } byte${ kvp[1].length !== 1 ? 's' : '' }`
|
||||
}));
|
||||
},
|
||||
|
||||
relatedWorkloadsRows() {
|
||||
return [
|
||||
{
|
||||
stateDisplay: 'Success',
|
||||
stateBackground: 'bg-success',
|
||||
nameDisplay: 'Workload0',
|
||||
detailUrl: '#',
|
||||
scale: 4,
|
||||
image: 'nginx',
|
||||
created: '2020-01-20T09:00:00+00:00'
|
||||
},
|
||||
{
|
||||
stateDisplay: 'Success',
|
||||
stateBackground: 'bg-success',
|
||||
nameDisplay: 'Workload1',
|
||||
detailUrl: '#',
|
||||
scale: 44,
|
||||
image: 'ubuntu',
|
||||
created: '2020-01-20T11:00:00+00:00'
|
||||
}
|
||||
];
|
||||
return [];
|
||||
},
|
||||
|
||||
detailTopColumns() {
|
||||
|
|
@ -93,47 +99,74 @@ export default {
|
|||
];
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onDownloadClick(file, ev) {
|
||||
ev.preventDefault();
|
||||
downloadFile(file.key, file.value, 'application/octet-stream');
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VStack class="config-map">
|
||||
<DetailTop :columns="detailTopColumns" />
|
||||
<DetailTop class="detail-top" :columns="detailTopColumns" />
|
||||
<div>
|
||||
<div class="title">
|
||||
Values
|
||||
</div>
|
||||
<SortableTable
|
||||
key-field="_key"
|
||||
:headers="valuesTableHeaders"
|
||||
:rows="valuesTableRows"
|
||||
:row-actions="false"
|
||||
:search="false"
|
||||
:table-actions="false"
|
||||
:top-divider="false"
|
||||
:emphasized-body="false"
|
||||
:body-dividers="true"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div>Related Workloads</div>
|
||||
<h2>
|
||||
Related Workloads
|
||||
</h2>
|
||||
<SortableTable
|
||||
key-field="_key"
|
||||
:headers="relatedWorkloadsHeaders"
|
||||
:rows="relatedWorkloadsRows"
|
||||
:row-actions="false"
|
||||
:search="false"
|
||||
no-rows-key="generic.comingSoon"
|
||||
/>
|
||||
</div>
|
||||
<Tabbed default-tab="values">
|
||||
<Tab name="values" label="Values">
|
||||
<SortableTable
|
||||
key-field="_key"
|
||||
:headers="valuesTableHeaders"
|
||||
:rows="valuesTableRows"
|
||||
:row-actions="false"
|
||||
:search="false"
|
||||
:table-actions="false"
|
||||
:top-divider="false"
|
||||
:emphasized-body="false"
|
||||
:body-dividers="true"
|
||||
/>
|
||||
</Tab>
|
||||
<Tab name="binary-values" label="Binary Values">
|
||||
<SortableTable
|
||||
key-field="_key"
|
||||
:headers="binaryValuesTableHeaders"
|
||||
:rows="binaryValuesTableRows"
|
||||
:row-actions="false"
|
||||
:search="false"
|
||||
:table-actions="false"
|
||||
:top-divider="false"
|
||||
:emphasized-body="false"
|
||||
:body-dividers="true"
|
||||
>
|
||||
<template #col:download="{row}">
|
||||
<td data-title="Download:" align="right" class="col-click-expand">
|
||||
<a href="#" @click="onDownloadClick(row, $event)">Download</a>
|
||||
</td>
|
||||
</template>
|
||||
</SortableTable>
|
||||
</Tab>
|
||||
<Tab label="Labels and Annotations" name="labelsAndAnnotations">
|
||||
<Labels :spec="value" :mode="mode" />
|
||||
</Tab>
|
||||
</Tabbed>
|
||||
</VStack>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.config-map > * {
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: 20px;
|
||||
.detail-top {
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import Date from '@/components/formatter/Date';
|
|||
import LoadDeps from '@/mixins/load-deps';
|
||||
import { allHash } from '@/utils/promise';
|
||||
import WorkloadPorts from '@/edit/workload/WorkloadPorts';
|
||||
import { DESCRIPTION } from '@/config/labels-annotations';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
@ -126,10 +127,15 @@ export default {
|
|||
|
||||
detailTopColumns() {
|
||||
return [
|
||||
|
||||
{
|
||||
title: 'Namespace',
|
||||
content: get(this.value, 'metadata.namespace')
|
||||
},
|
||||
{
|
||||
title: 'Description',
|
||||
content: this.value?.metadata?.annotations[DESCRIPTION]
|
||||
},
|
||||
{
|
||||
title: 'Image',
|
||||
content: this.container.image
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@ import CreateEditView from '@/mixins/create-edit-view';
|
|||
import NameNsDescription from '@/components/form/NameNsDescription';
|
||||
import Footer from '@/components/form/Footer';
|
||||
import KeyValue from '@/components/form/KeyValue';
|
||||
import Labels from '@/components/form/Labels';
|
||||
|
||||
export default {
|
||||
name: 'CruConfigMap',
|
||||
|
||||
components: {
|
||||
Labels,
|
||||
NameNsDescription,
|
||||
KeyValue,
|
||||
Footer,
|
||||
|
|
@ -33,7 +35,7 @@ export default {
|
|||
key="data"
|
||||
v-model="value.data"
|
||||
:mode="mode"
|
||||
title="Data"
|
||||
title="Values"
|
||||
protip="Use this area for anything that's UTF-8 text data"
|
||||
:initial-empty-row="true"
|
||||
/>
|
||||
|
|
@ -47,7 +49,7 @@ export default {
|
|||
<KeyValue
|
||||
key="binaryData"
|
||||
v-model="value.binaryData"
|
||||
title="Binary Data"
|
||||
title="Binary Values"
|
||||
protip="Use this area for binary or other data that is not UTF-8 text"
|
||||
:mode="mode"
|
||||
:add-allowed="false"
|
||||
|
|
@ -61,6 +63,8 @@ export default {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<Labels :spec="value" :mode="mode" />
|
||||
|
||||
<Footer :mode="mode" :errors="errors" @save="save" @done="done" />
|
||||
</form>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
<script>
|
||||
import { get } from '../../utils/object';
|
||||
import RadioGroup from '@/components/form/RadioGroup';
|
||||
import LabeledSelect from '@/components/form/LabeledSelect';
|
||||
import Selectors from '@/edit/workload/Selectors';
|
||||
import NodeAffinity from '@/components/form/NodeAffinity';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
RadioGroup,
|
||||
Selectors,
|
||||
LabeledSelect
|
||||
LabeledSelect,
|
||||
NodeAffinity
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
|
|
@ -26,9 +25,12 @@ export default {
|
|||
const { affinity = {}, nodeName = '' } = this.value;
|
||||
const { nodeAffinity = {} } = affinity;
|
||||
|
||||
const required = get(nodeAffinity, 'requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms') || [];
|
||||
const preferred = get(nodeAffinity, 'preferredDuringSchedulingIgnoredDuringExecution') || [];
|
||||
|
||||
if (!nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution) {
|
||||
this.$set(nodeAffinity, 'requiredDuringSchedulingIgnoredDuringExecution', { nodeSelectorTerms: [] } );
|
||||
}
|
||||
if (!nodeAffinity.preferredDuringSchedulingIgnoredDuringExecution) {
|
||||
this.$set(nodeAffinity, 'preferredDuringSchedulingIgnoredDuringExecution', []);
|
||||
}
|
||||
let selectNode = false;
|
||||
|
||||
if (nodeName.length) {
|
||||
|
|
@ -36,44 +38,13 @@ export default {
|
|||
}
|
||||
|
||||
return {
|
||||
required, preferred, selectNode, nodeName
|
||||
selectNode, nodeName, nodeAffinity
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
preferred() {
|
||||
this.update();
|
||||
},
|
||||
required() {
|
||||
this.update();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
update() {
|
||||
const out = {
|
||||
...this.value,
|
||||
affinity: {
|
||||
nodeAffinity: {
|
||||
preferredDuringSchedulingIgnoredDuringExecution: this.preferred.map((rule) => {
|
||||
let weight = 1;
|
||||
|
||||
if (!!rule.weight) {
|
||||
weight = rule.weight;
|
||||
}
|
||||
delete rule.weight;
|
||||
|
||||
return { preference: { matchExpressions: [rule] }, weight };
|
||||
}),
|
||||
requiredDuringSchedulingIgnoredDuringExecution: {
|
||||
nodeSelectorTerms: this.required.map((rule) => {
|
||||
return { matchExpressions: [rule] };
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
const out = { ...this.value, affinity: this.nodeAffinity };
|
||||
|
||||
if (this.selectNode) {
|
||||
this.$set(out, 'nodeName', this.nodeName);
|
||||
|
|
@ -98,22 +69,18 @@ export default {
|
|||
<LabeledSelect v-model="nodeName" :options="nodes" :mode="mode" />
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="row">
|
||||
<div class="col span-6">
|
||||
<NodeAffinity :value="nodeAffinity" :mode="mode">
|
||||
<template #title>
|
||||
<h5 class="mb-10">
|
||||
Require all of:
|
||||
</h5>
|
||||
<span v-if="mode==='view' && !required.length">n/a </span>
|
||||
<Selectors v-model="required" :mode="mode" />
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
</template>
|
||||
<template #title-weighted>
|
||||
<h5 class="mb-10">
|
||||
Prefer any of:
|
||||
</h5>
|
||||
<span v-if="mode==='view' && !preferred.length">n/a </span>
|
||||
<Selectors v-else v-model="preferred" is-weighted :mode="mode" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</NodeAffinity>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,190 +0,0 @@
|
|||
<script>
|
||||
import LabeledInput from '@/components/form/LabeledInput';
|
||||
import LabeledSelect from '@/components/form/LabeledSelect';
|
||||
|
||||
export default {
|
||||
components: { LabeledInput, LabeledSelect },
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'edit'
|
||||
},
|
||||
|
||||
isWeighted: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
const ops = [{ label: '<', value: 'Lt' }, { label: '>', value: 'Gt' }, { label: 'is set', value: 'Exists' }, { label: 'is not set', value: 'DoesNotExist' }, { label: 'in list', value: 'In' }, { label: 'not in list', value: 'NotIn' }];
|
||||
|
||||
// if node affinity has weighted rules, the matchExpressions are nested further in 'preference' field
|
||||
const rules = this.isWeighted ? this.value.map((rule) => {
|
||||
return { ...rule.preference.matchExpressions[0], weight: rule.weight };
|
||||
})
|
||||
: this.value.map(rule => rule.matchExpressions[0]);
|
||||
|
||||
return {
|
||||
ops, rules, custom: []
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
isView() {
|
||||
return this.mode === 'view';
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
parseRuleString(rule) {
|
||||
const all = rule.split(' ');
|
||||
let key; let op; let value;
|
||||
|
||||
if (all[0].length) {
|
||||
if (all.length > 1) {
|
||||
key = all[0];
|
||||
op = all[1];
|
||||
value = all[2].startsWith('(') ? all[2].slice(1, all[2].length - 1) : all[2];
|
||||
} else if (all[0].startsWith('!')) {
|
||||
op = '!';
|
||||
key = all[0].slice(1);
|
||||
|
||||
return { key, op };
|
||||
} else {
|
||||
key = all[0];
|
||||
op = ' ';
|
||||
|
||||
return { key, op };
|
||||
}
|
||||
|
||||
return {
|
||||
key, op, value
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
removeRule(idx) {
|
||||
this.rules.splice(idx, 1);
|
||||
this.update();
|
||||
},
|
||||
|
||||
removeCustom(idx) {
|
||||
this.custom.splice(idx, 1);
|
||||
this.update();
|
||||
},
|
||||
|
||||
addRule() {
|
||||
this.rules.push({ values: [] });
|
||||
},
|
||||
|
||||
addCustomRule() {
|
||||
this.custom.push('');
|
||||
},
|
||||
|
||||
updateCustom(idx, rule) {
|
||||
this.$set(this.custom, idx, rule);
|
||||
},
|
||||
|
||||
update() {
|
||||
this.$nextTick(() => {
|
||||
const out = [
|
||||
...this.rules.map((rule) => {
|
||||
const matchExpression = { key: rule.key };
|
||||
|
||||
if (rule.operator) {
|
||||
matchExpression.operator = rule.operator;
|
||||
}
|
||||
if (rule.values.length && rule.operator !== 'Exists' && rule.operator !== 'DoesNotExist') {
|
||||
matchExpression.values = rule.values;
|
||||
}
|
||||
|
||||
return matchExpression;
|
||||
}),
|
||||
...this.custom.map((rule) => {
|
||||
return {
|
||||
key: rule,
|
||||
operator: 'Exists'
|
||||
};
|
||||
})];
|
||||
|
||||
this.$emit('input', out);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div @input="update">
|
||||
<div v-for="(rule, i) in rules" :key="i">
|
||||
<div class="row">
|
||||
<div class="col span-4">
|
||||
<LabeledInput v-model="rule.key" label="Key" :mode="mode" />
|
||||
</div>
|
||||
<LabeledSelect
|
||||
id="operator"
|
||||
v-model="rule.operator"
|
||||
class="col span-2"
|
||||
:options="ops"
|
||||
:mode="mode"
|
||||
label="Op"
|
||||
@input="update"
|
||||
/>
|
||||
<!-- use conditional rendering here to avoid this v-model breaking the page if rule.values doesn't exist -->
|
||||
<div v-if="rule.operator!=='Exists'&&rule.operator!=='DoesNotExist'" class="col span-4">
|
||||
<LabeledInput v-model="rule.values[0]" label="Value" :mode="mode" />
|
||||
</div>
|
||||
<div v-if="isWeighted" class="col span-2">
|
||||
<LabeledInput v-model="rule.weight" label="Weight" :mode="mode" />
|
||||
</div>
|
||||
<button
|
||||
v-if="!isView"
|
||||
type="button"
|
||||
class="btn btn-sm role-link"
|
||||
:style="{padding:'0px'}"
|
||||
:disabled="mode==='view'"
|
||||
@click="removeRule(i)"
|
||||
>
|
||||
<!-- REMOVE -->
|
||||
<i class="icon icon-minus icon-lg" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="(rule, i) in custom" :key="i" class="row">
|
||||
<div class="col span-10">
|
||||
<LabeledInput :multiline="false" :value="rule" placeholder="e.g. foo > 42 && bar != baz" :mode="mode" @input="e=>updateCustom(i, e)" />
|
||||
</div>
|
||||
<button
|
||||
v-if="!isView"
|
||||
type="button"
|
||||
class="btn btn-sm role-link"
|
||||
:style="{padding:'0px'}"
|
||||
:disabled="mode==='view'"
|
||||
@click="removeCustom(i)"
|
||||
>
|
||||
<!-- REMOVE -->
|
||||
<i class="icon icon-minus icon-lg" />
|
||||
</button>
|
||||
</div>
|
||||
<button v-if="!isView" type="button" class="btn role-tertiary add" @click="addRule">
|
||||
Add Rule
|
||||
</button>
|
||||
<!-- <button v-if="!isView" type="button" class="btn role-tertiary add" @click="addCustomRule">
|
||||
Add custom rule
|
||||
</button> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang='scss'>
|
||||
#operator {
|
||||
& .vs__dropdown-option{
|
||||
padding: 3px 6px 3px 6px !important
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -22,6 +22,23 @@ import WorkloadPorts from '@/edit/workload/WorkloadPorts';
|
|||
import { defaultAsyncData } from '@/components/ResourceDetail.vue';
|
||||
import { _EDIT } from '@/config/query-params';
|
||||
|
||||
const workloadTypeOptions = [
|
||||
{ value: WORKLOAD_TYPES.DEPLOYMENT, label: 'Deployment' },
|
||||
|
||||
{ value: WORKLOAD_TYPES.DAEMON_SET, label: 'Daemon Set' },
|
||||
|
||||
{ value: WORKLOAD_TYPES.STATEFUL_SET, label: 'Stateful Set' },
|
||||
|
||||
{ value: WORKLOAD_TYPES.REPLICA_SET, label: 'Replica Set' },
|
||||
|
||||
{ value: WORKLOAD_TYPES.JOB, label: 'Job' },
|
||||
|
||||
{ value: WORKLOAD_TYPES.CRON_JOB, label: 'Cron Job' },
|
||||
|
||||
{ value: WORKLOAD_TYPES.REPLICATION_CONTROLLER, label: 'Replication Controller' }
|
||||
|
||||
];
|
||||
|
||||
export default {
|
||||
name: 'CruWorkload',
|
||||
components: {
|
||||
|
|
@ -39,15 +56,17 @@ export default {
|
|||
Networking,
|
||||
Footer,
|
||||
Job,
|
||||
WorkloadPorts
|
||||
WorkloadPorts,
|
||||
},
|
||||
|
||||
mixins: [CreateEditView, LoadDeps],
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'create'
|
||||
|
|
@ -55,23 +74,6 @@ export default {
|
|||
},
|
||||
|
||||
data() {
|
||||
const typeOpts = [];
|
||||
const workloadMap = {
|
||||
[WORKLOAD_TYPES.DEPLOYMENT]: 'Deployment',
|
||||
[WORKLOAD_TYPES.DAEMON_SET]: 'Daemon Set',
|
||||
[WORKLOAD_TYPES.STATEFUL_SET]: 'Stateful Set',
|
||||
[WORKLOAD_TYPES.REPLICA_SET]: 'Replica Set',
|
||||
[WORKLOAD_TYPES.JOB]: 'Job',
|
||||
[WORKLOAD_TYPES.CRON_JOB]: 'Cron Job',
|
||||
[WORKLOAD_TYPES.REPLICATION_CONTROLLER]: 'Replication Controller'
|
||||
};
|
||||
|
||||
for (const key in workloadMap) {
|
||||
typeOpts.push({ value: key, label: workloadMap[key] });
|
||||
}
|
||||
|
||||
const selectNode = false;
|
||||
|
||||
let type = this.value._type || this.value.type || WORKLOAD_TYPES.DEPLOYMENT;
|
||||
|
||||
if (type === 'workload') {
|
||||
|
|
@ -90,45 +92,62 @@ export default {
|
|||
}
|
||||
|
||||
return {
|
||||
selectNode,
|
||||
spec,
|
||||
type,
|
||||
typeOpts,
|
||||
allConfigMaps: null,
|
||||
allSecrets: null,
|
||||
allNodes: null,
|
||||
showTabs: false
|
||||
workloadTypeOptions,
|
||||
allConfigMaps: null,
|
||||
allSecrets: null,
|
||||
allNodes: null,
|
||||
showTabs: false,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
schema() {
|
||||
return this.$store.getters['cluster/schemaFor']( this.type );
|
||||
|
||||
isEdit() {
|
||||
return this.mode === _EDIT;
|
||||
},
|
||||
|
||||
isJob() {
|
||||
return this.type === WORKLOAD_TYPES.JOB || this.isCronJob;
|
||||
},
|
||||
|
||||
isCronJob() {
|
||||
return this.type === WORKLOAD_TYPES.CRON_JOB;
|
||||
},
|
||||
|
||||
isReplicable() {
|
||||
return (this.type === WORKLOAD_TYPES.DEPLOYMENT || this.type === WORKLOAD_TYPES.REPLICA_SET || this.type === WORKLOAD_TYPES.REPLICATION_CONTROLLER || this.type === WORKLOAD_TYPES.STATEFUL_SET);
|
||||
},
|
||||
|
||||
// if this is a cronjob, grab pod spec from within job template spec
|
||||
podTemplateSpec: {
|
||||
get() {
|
||||
return this.isCronJob ? this.spec.jobTemplate.spec.template.spec : this.spec.template.spec;
|
||||
},
|
||||
set(neu) {
|
||||
if (this.isJob) {
|
||||
this.$set(this.spec.jobTemplate.spec.template, 'spec', neu);
|
||||
} else {
|
||||
this.$set(this.spec.template, 'spec', neu);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
container: {
|
||||
get() {
|
||||
let template = this.spec.template;
|
||||
|
||||
if (this.isCronJob) {
|
||||
template = this.spec.jobTemplate.spec.template;
|
||||
}
|
||||
const { containers } = template.spec;
|
||||
const { containers } = this.podTemplateSpec;
|
||||
|
||||
if (!containers) {
|
||||
this.$set(template.spec, 'containers', [{ name: this.value.metadata.name }]);
|
||||
this.$set(this.podTemplateSpec, 'containers', [{ name: this.value.metadata.name }]);
|
||||
}
|
||||
|
||||
return template.spec.containers[0];
|
||||
// TODO account for multiple containers (sidecar)
|
||||
return this.podTemplateSpec.containers[0];
|
||||
},
|
||||
|
||||
set(neu) {
|
||||
let template = this.spec.template;
|
||||
|
||||
if (this.isCronJob) {
|
||||
template = this.spec.jobTemplate.spec.template;
|
||||
}
|
||||
this.$set(template.spec.containers, 0, { ...neu, name: this.value.metadata.name });
|
||||
this.$set(this.podTemplateSpec.containers, 0, { ...neu, name: this.value.metadata.name });
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -150,18 +169,11 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
canReplicate() {
|
||||
return (this.type === WORKLOAD_TYPES.DEPLOYMENT || this.type === WORKLOAD_TYPES.REPLICA_SET || this.type === WORKLOAD_TYPES.REPLICATION_CONTROLLER || this.type === WORKLOAD_TYPES.STATEFUL_SET);
|
||||
},
|
||||
|
||||
isJob() {
|
||||
return this.type === WORKLOAD_TYPES.JOB || this.isCronJob;
|
||||
},
|
||||
|
||||
isCronJob() {
|
||||
return this.type === WORKLOAD_TYPES.CRON_JOB;
|
||||
schema() {
|
||||
return this.$store.getters['cluster/schemaFor']( this.type );
|
||||
},
|
||||
|
||||
// show cron schedule in human-readable format
|
||||
cronLabel() {
|
||||
const { schedule } = this.spec;
|
||||
|
||||
|
|
@ -182,9 +194,6 @@ export default {
|
|||
return { 'workload.user.cattle.io/workloadselector': `${ 'deployment' }-${ this.value.metadata.namespace }-${ this.value.metadata.name }` };
|
||||
},
|
||||
|
||||
isEdit() {
|
||||
return this.mode === _EDIT;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
|
@ -198,7 +207,7 @@ export default {
|
|||
|
||||
this.$set(template.spec, 'restartPolicy', restartPolicy);
|
||||
|
||||
if (!this.canReplicate) {
|
||||
if (!this.isReplicable) {
|
||||
delete this.spec.replicas;
|
||||
}
|
||||
|
||||
|
|
@ -273,7 +282,7 @@ export default {
|
|||
<slot :value="value" name="top">
|
||||
<NameNsDescription :value="value" :mode="mode" :extra-columns="['type']">
|
||||
<template v-slot:type>
|
||||
<LabeledSelect v-model="type" label="Type" :disabled="isEdit" :options="typeOpts" />
|
||||
<LabeledSelect v-model="type" label="Type" :disabled="isEdit" :options="workloadTypeOptions" />
|
||||
</template>
|
||||
</NameNsDescription>
|
||||
|
||||
|
|
@ -288,7 +297,7 @@ export default {
|
|||
<span class="cron-hint text-small">{{ cronLabel }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="canReplicate">
|
||||
<template v-if="isReplicable">
|
||||
<div class="col span-4">
|
||||
<LabeledInput v-model.number="spec.replicas" label="Replicas" />
|
||||
</div>
|
||||
|
|
@ -315,20 +324,17 @@ export default {
|
|||
/>
|
||||
</Tab>
|
||||
<Tab label="Networking" name="networking">
|
||||
<Networking v-if="isCronJob" v-model="spec.jobTemplate.spec.template.spec" :mode="mode" />
|
||||
<Networking v-else v-model="spec.template.spec" :mode="mode" />
|
||||
<Networking v-model="podTemplateSpec" :mode="mode" />
|
||||
</Tab>
|
||||
<Tab label="Health" name="health">
|
||||
<HealthCheck :spec="container" :mode="mode" />
|
||||
</Tab>
|
||||
<Tab label="Security" name="security">
|
||||
<Security v-if="isCronJob" v-model="spec.jobTemplate.spec.template.spec" :mode="mode" />
|
||||
<Security v-else v-model="spec.template.spec" :mode="mode" />
|
||||
<Security v-model="podTemplateSpec" :mode="mode" />
|
||||
</Tab>
|
||||
|
||||
<Tab label="Node Scheduling" name="scheduling">
|
||||
<Scheduling v-if="isCronJob" v-model="spec.jobTemplate.spec.template.spec" :nodes="allNodes" :mode="mode" />
|
||||
<Scheduling v-else v-model="spec.template.spec" :mode="mode" />
|
||||
<Scheduling v-model="podTemplateSpec" :mode="mode" />
|
||||
</Tab>
|
||||
<Tab label="Scaling/Upgrade Policy" name="upgrading">
|
||||
<Upgrading v-model="spec" :mode="mode" />
|
||||
|
|
@ -338,7 +344,7 @@ export default {
|
|||
<Labels :spec="value" :mode="mode" />
|
||||
</Tab>
|
||||
</Tabbed>
|
||||
<Footer :errors="errors" :mode="mode" @save="saveWorkload" @done="done" />
|
||||
<Footer v-if="mode!= 'view'" :errors="errors" :mode="mode" @save="saveWorkload" @done="done" />
|
||||
</form>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
export default {
|
||||
// remove clone as yaml/edit as yaml until API supported
|
||||
|
||||
_availableActions() {
|
||||
let out = this._standardActions;
|
||||
|
||||
|
|
@ -19,5 +18,5 @@ export default {
|
|||
});
|
||||
|
||||
return out;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
"node": ">=10"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "./node_modules/.bin/eslint --ext .js,.vue .",
|
||||
"lint": "./node_modules/.bin/eslint --max-warnings 0 --ext .js,.vue .",
|
||||
"test": "./node_modules/.bin/nyc ava --serial --verbose",
|
||||
"nuxt": "./node_modules/.bin/nuxt",
|
||||
"dev": "./node_modules/.bin/nuxt dev",
|
||||
|
|
|
|||
Loading…
Reference in New Issue