Merge branch 'master' into download.yaml.overview

This commit is contained in:
Vincent Fiduccia 2020-04-20 12:03:01 -07:00 committed by GitHub
commit aadb803bc5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 491 additions and 361 deletions

View File

@ -4,6 +4,7 @@ locale:
generic: generic:
customize: Customize customize: Customize
comingSoon: Coming Soon
header: header:
backToRancher: "← Back to Rancher" backToRancher: "← Back to Rancher"

View File

@ -10,8 +10,6 @@ import {
_UNFLAG, _UNFLAG,
} from '@/config/query-params'; } from '@/config/query-params';
// import { mapPref, DIFF } from '@/store/prefs';
export default { export default {
components: { components: {
Footer, Footer,

View File

@ -27,9 +27,17 @@ export default {
type: Boolean, type: Boolean,
default: false default: false
}, },
optionKey: {
type: String,
default: null
},
optionLabel: { optionLabel: {
type: String, type: String,
default: 'label' default: 'label'
},
optionKey: {
type: String,
default: null
} }
}, },
data() { data() {

View File

@ -20,6 +20,11 @@ export default {
}, },
computed: { computed: {
containerClass() {
return this.displaySideBySide
? 'row'
: '';
},
sectionClass() { sectionClass() {
return this.displaySideBySide return this.displaySideBySide
? 'col span-6' ? 'col span-6'
@ -39,7 +44,7 @@ export default {
}; };
</script> </script>
<template> <template>
<div class="row"> <div :class="containerClass">
<div :class="sectionClass"> <div :class="sectionClass">
<KeyValue <KeyValue
key="labels" key="labels"

View File

@ -182,7 +182,7 @@ export default {
v-model="name" v-model="name"
label="Name" label="Name"
:disabled="nameDisabled" :disabled="nameDisabled"
:mode="nameMode" :mode="mode"
:min-height="30" :min-height="30"
/> />
</slot> </slot>

View File

@ -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>

View File

@ -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>

View File

@ -10,6 +10,14 @@ export const STATE = {
formatter: 'BadgeState', formatter: 'BadgeState',
}; };
export const DOWNLOAD = {
name: 'download',
label: 'Download',
value: 'download',
canBeVariable: true,
align: 'right',
};
export const NAME = { export const NAME = {
name: 'name', name: 'name',
label: 'Name', label: 'Name',

View File

@ -1,9 +1,16 @@
<script> <script>
import { DESCRIPTION } from '@/config/labels-annotations'; import { DESCRIPTION } from '@/config/labels-annotations';
import CreateEditView from '@/mixins/create-edit-view';
import DetailTop from '@/components/DetailTop'; import DetailTop from '@/components/DetailTop';
import Labels from '@/components/form/Labels';
import SortableTable from '@/components/SortableTable'; import SortableTable from '@/components/SortableTable';
import VStack from '@/components/Layout/Stack/VStack'; import VStack from '@/components/Layout/Stack/VStack';
import { downloadFile } from '@/utils/download';
import Tab from '@/components/Tabbed/Tab';
import Tabbed from '@/components/Tabbed';
import { import {
DOWNLOAD,
KEY, KEY,
VALUE, VALUE,
STATE, STATE,
@ -17,10 +24,15 @@ export default {
name: 'DetailConfigMap', name: 'DetailConfigMap',
components: { components: {
DetailTop, DetailTop,
Labels,
SortableTable, SortableTable,
Tab,
Tabbed,
VStack VStack
}, },
mixins: [CreateEditView],
props: { props: {
value: { value: {
type: Object, type: Object,
@ -29,12 +41,18 @@ export default {
}, },
data() { data() {
const valuesTableHeaders = [
{
...KEY, sort: false, width: 400
},
{ ...VALUE, sort: false },
];
return { return {
valuesTableHeaders: [ valuesTableHeaders,
{ binaryValuesTableHeaders: [
...KEY, sort: false, width: 400 ...valuesTableHeaders,
}, DOWNLOAD
{ ...VALUE, sort: false },
], ],
relatedWorkloadsHeaders: [ relatedWorkloadsHeaders: [
STATE, 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() { relatedWorkloadsRows() {
return [ 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'
}
];
}, },
detailTopColumns() { detailTopColumns() {
@ -93,47 +99,74 @@ export default {
]; ];
} }
}, },
methods: {
onDownloadClick(file, ev) {
ev.preventDefault();
downloadFile(file.key, file.value, 'application/octet-stream');
}
},
}; };
</script> </script>
<template> <template>
<VStack class="config-map"> <VStack class="config-map">
<DetailTop :columns="detailTopColumns" /> <DetailTop class="detail-top" :columns="detailTopColumns" />
<div> <div>
<div class="title"> <h2>
Values Related Workloads
</div> </h2>
<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>
<SortableTable <SortableTable
key-field="_key" key-field="_key"
:headers="relatedWorkloadsHeaders" :headers="relatedWorkloadsHeaders"
:rows="relatedWorkloadsRows" :rows="relatedWorkloadsRows"
:row-actions="false" :row-actions="false"
:search="false" :search="false"
no-rows-key="generic.comingSoon"
/> />
</div> </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> </VStack>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.config-map > * { .detail-top {
margin-bottom: 50px; margin-bottom: 50px;
}
.title {
margin-bottom: 20px;
} }
</style> </style>

View File

@ -9,6 +9,7 @@ import Date from '@/components/formatter/Date';
import LoadDeps from '@/mixins/load-deps'; import LoadDeps from '@/mixins/load-deps';
import { allHash } from '@/utils/promise'; import { allHash } from '@/utils/promise';
import WorkloadPorts from '@/edit/workload/WorkloadPorts'; import WorkloadPorts from '@/edit/workload/WorkloadPorts';
import { DESCRIPTION } from '@/config/labels-annotations';
export default { export default {
components: { components: {
@ -126,10 +127,15 @@ export default {
detailTopColumns() { detailTopColumns() {
return [ return [
{ {
title: 'Namespace', title: 'Namespace',
content: get(this.value, 'metadata.namespace') content: get(this.value, 'metadata.namespace')
}, },
{
title: 'Description',
content: this.value?.metadata?.annotations[DESCRIPTION]
},
{ {
title: 'Image', title: 'Image',
content: this.container.image content: this.container.image

View File

@ -3,11 +3,13 @@ import CreateEditView from '@/mixins/create-edit-view';
import NameNsDescription from '@/components/form/NameNsDescription'; import NameNsDescription from '@/components/form/NameNsDescription';
import Footer from '@/components/form/Footer'; import Footer from '@/components/form/Footer';
import KeyValue from '@/components/form/KeyValue'; import KeyValue from '@/components/form/KeyValue';
import Labels from '@/components/form/Labels';
export default { export default {
name: 'CruConfigMap', name: 'CruConfigMap',
components: { components: {
Labels,
NameNsDescription, NameNsDescription,
KeyValue, KeyValue,
Footer, Footer,
@ -33,7 +35,7 @@ export default {
key="data" key="data"
v-model="value.data" v-model="value.data"
:mode="mode" :mode="mode"
title="Data" title="Values"
protip="Use this area for anything that's UTF-8 text data" protip="Use this area for anything that's UTF-8 text data"
:initial-empty-row="true" :initial-empty-row="true"
/> />
@ -47,7 +49,7 @@ export default {
<KeyValue <KeyValue
key="binaryData" key="binaryData"
v-model="value.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" protip="Use this area for binary or other data that is not UTF-8 text"
:mode="mode" :mode="mode"
:add-allowed="false" :add-allowed="false"
@ -61,6 +63,8 @@ export default {
</div> </div>
</div> </div>
<Labels :spec="value" :mode="mode" />
<Footer :mode="mode" :errors="errors" @save="save" @done="done" /> <Footer :mode="mode" :errors="errors" @save="save" @done="done" />
</form> </form>
</template> </template>

View File

@ -1,14 +1,13 @@
<script> <script>
import { get } from '../../utils/object';
import RadioGroup from '@/components/form/RadioGroup'; import RadioGroup from '@/components/form/RadioGroup';
import LabeledSelect from '@/components/form/LabeledSelect'; import LabeledSelect from '@/components/form/LabeledSelect';
import Selectors from '@/edit/workload/Selectors'; import NodeAffinity from '@/components/form/NodeAffinity';
export default { export default {
components: { components: {
RadioGroup, RadioGroup,
Selectors, LabeledSelect,
LabeledSelect NodeAffinity
}, },
props: { props: {
value: { value: {
@ -26,9 +25,12 @@ export default {
const { affinity = {}, nodeName = '' } = this.value; const { affinity = {}, nodeName = '' } = this.value;
const { nodeAffinity = {} } = affinity; const { nodeAffinity = {} } = affinity;
const required = get(nodeAffinity, 'requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms') || []; if (!nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution) {
const preferred = get(nodeAffinity, 'preferredDuringSchedulingIgnoredDuringExecution') || []; this.$set(nodeAffinity, 'requiredDuringSchedulingIgnoredDuringExecution', { nodeSelectorTerms: [] } );
}
if (!nodeAffinity.preferredDuringSchedulingIgnoredDuringExecution) {
this.$set(nodeAffinity, 'preferredDuringSchedulingIgnoredDuringExecution', []);
}
let selectNode = false; let selectNode = false;
if (nodeName.length) { if (nodeName.length) {
@ -36,44 +38,13 @@ export default {
} }
return { return {
required, preferred, selectNode, nodeName selectNode, nodeName, nodeAffinity
}; };
}, },
watch: {
preferred() {
this.update();
},
required() {
this.update();
}
},
methods: { methods: {
update() { update() {
const out = { const out = { ...this.value, affinity: this.nodeAffinity };
...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] };
})
}
}
}
};
if (this.selectNode) { if (this.selectNode) {
this.$set(out, 'nodeName', this.nodeName); this.$set(out, 'nodeName', this.nodeName);
@ -98,22 +69,18 @@ export default {
<LabeledSelect v-model="nodeName" :options="nodes" :mode="mode" /> <LabeledSelect v-model="nodeName" :options="nodes" :mode="mode" />
</div> </div>
<template v-else> <template v-else>
<div class="row"> <NodeAffinity :value="nodeAffinity" :mode="mode">
<div class="col span-6"> <template #title>
<h5 class="mb-10"> <h5 class="mb-10">
Require all of: Require all of:
</h5> </h5>
<span v-if="mode==='view' && !required.length">n/a </span> </template>
<Selectors v-model="required" :mode="mode" /> <template #title-weighted>
</div>
<div class="col span-6">
<h5 class="mb-10"> <h5 class="mb-10">
Prefer any of: Prefer any of:
</h5> </h5>
<span v-if="mode==='view' && !preferred.length">n/a </span> </template>
<Selectors v-else v-model="preferred" is-weighted :mode="mode" /> </NodeAffinity>
</div>
</div>
</template> </template>
</div> </div>
</template> </template>

View File

@ -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>

View File

@ -22,6 +22,23 @@ import WorkloadPorts from '@/edit/workload/WorkloadPorts';
import { defaultAsyncData } from '@/components/ResourceDetail.vue'; import { defaultAsyncData } from '@/components/ResourceDetail.vue';
import { _EDIT } from '@/config/query-params'; 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 { export default {
name: 'CruWorkload', name: 'CruWorkload',
components: { components: {
@ -39,15 +56,17 @@ export default {
Networking, Networking,
Footer, Footer,
Job, Job,
WorkloadPorts WorkloadPorts,
}, },
mixins: [CreateEditView, LoadDeps], mixins: [CreateEditView, LoadDeps],
props: { props: {
value: { value: {
type: Object, type: Object,
required: true, required: true,
}, },
mode: { mode: {
type: String, type: String,
default: 'create' default: 'create'
@ -55,23 +74,6 @@ export default {
}, },
data() { 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; let type = this.value._type || this.value.type || WORKLOAD_TYPES.DEPLOYMENT;
if (type === 'workload') { if (type === 'workload') {
@ -90,45 +92,62 @@ export default {
} }
return { return {
selectNode,
spec, spec,
type, type,
typeOpts, workloadTypeOptions,
allConfigMaps: null, allConfigMaps: null,
allSecrets: null, allSecrets: null,
allNodes: null, allNodes: null,
showTabs: false showTabs: false,
}; };
}, },
computed: { 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: { container: {
get() { get() {
let template = this.spec.template; const { containers } = this.podTemplateSpec;
if (this.isCronJob) {
template = this.spec.jobTemplate.spec.template;
}
const { containers } = template.spec;
if (!containers) { 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) { set(neu) {
let template = this.spec.template; this.$set(this.podTemplateSpec.containers, 0, { ...neu, name: this.value.metadata.name });
if (this.isCronJob) {
template = this.spec.jobTemplate.spec.template;
}
this.$set(template.spec.containers, 0, { ...neu, name: this.value.metadata.name });
} }
}, },
@ -150,18 +169,11 @@ export default {
} }
}, },
canReplicate() { schema() {
return (this.type === WORKLOAD_TYPES.DEPLOYMENT || this.type === WORKLOAD_TYPES.REPLICA_SET || this.type === WORKLOAD_TYPES.REPLICATION_CONTROLLER || this.type === WORKLOAD_TYPES.STATEFUL_SET); return this.$store.getters['cluster/schemaFor']( this.type );
},
isJob() {
return this.type === WORKLOAD_TYPES.JOB || this.isCronJob;
},
isCronJob() {
return this.type === WORKLOAD_TYPES.CRON_JOB;
}, },
// show cron schedule in human-readable format
cronLabel() { cronLabel() {
const { schedule } = this.spec; 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 }` }; return { 'workload.user.cattle.io/workloadselector': `${ 'deployment' }-${ this.value.metadata.namespace }-${ this.value.metadata.name }` };
}, },
isEdit() {
return this.mode === _EDIT;
},
}, },
watch: { watch: {
@ -198,7 +207,7 @@ export default {
this.$set(template.spec, 'restartPolicy', restartPolicy); this.$set(template.spec, 'restartPolicy', restartPolicy);
if (!this.canReplicate) { if (!this.isReplicable) {
delete this.spec.replicas; delete this.spec.replicas;
} }
@ -273,7 +282,7 @@ export default {
<slot :value="value" name="top"> <slot :value="value" name="top">
<NameNsDescription :value="value" :mode="mode" :extra-columns="['type']"> <NameNsDescription :value="value" :mode="mode" :extra-columns="['type']">
<template v-slot: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> </template>
</NameNsDescription> </NameNsDescription>
@ -288,7 +297,7 @@ export default {
<span class="cron-hint text-small">{{ cronLabel }}</span> <span class="cron-hint text-small">{{ cronLabel }}</span>
</div> </div>
</template> </template>
<template v-if="canReplicate"> <template v-if="isReplicable">
<div class="col span-4"> <div class="col span-4">
<LabeledInput v-model.number="spec.replicas" label="Replicas" /> <LabeledInput v-model.number="spec.replicas" label="Replicas" />
</div> </div>
@ -315,20 +324,17 @@ export default {
/> />
</Tab> </Tab>
<Tab label="Networking" name="networking"> <Tab label="Networking" name="networking">
<Networking v-if="isCronJob" v-model="spec.jobTemplate.spec.template.spec" :mode="mode" /> <Networking v-model="podTemplateSpec" :mode="mode" />
<Networking v-else v-model="spec.template.spec" :mode="mode" />
</Tab> </Tab>
<Tab label="Health" name="health"> <Tab label="Health" name="health">
<HealthCheck :spec="container" :mode="mode" /> <HealthCheck :spec="container" :mode="mode" />
</Tab> </Tab>
<Tab label="Security" name="security"> <Tab label="Security" name="security">
<Security v-if="isCronJob" v-model="spec.jobTemplate.spec.template.spec" :mode="mode" /> <Security v-model="podTemplateSpec" :mode="mode" />
<Security v-else v-model="spec.template.spec" :mode="mode" />
</Tab> </Tab>
<Tab label="Node Scheduling" name="scheduling"> <Tab label="Node Scheduling" name="scheduling">
<Scheduling v-if="isCronJob" v-model="spec.jobTemplate.spec.template.spec" :nodes="allNodes" :mode="mode" /> <Scheduling v-model="podTemplateSpec" :mode="mode" />
<Scheduling v-else v-model="spec.template.spec" :mode="mode" />
</Tab> </Tab>
<Tab label="Scaling/Upgrade Policy" name="upgrading"> <Tab label="Scaling/Upgrade Policy" name="upgrading">
<Upgrading v-model="spec" :mode="mode" /> <Upgrading v-model="spec" :mode="mode" />
@ -338,7 +344,7 @@ export default {
<Labels :spec="value" :mode="mode" /> <Labels :spec="value" :mode="mode" />
</Tab> </Tab>
</Tabbed> </Tabbed>
<Footer :errors="errors" :mode="mode" @save="saveWorkload" @done="done" /> <Footer v-if="mode!= 'view'" :errors="errors" :mode="mode" @save="saveWorkload" @done="done" />
</form> </form>
</template> </template>

View File

@ -1,6 +1,5 @@
export default { export default {
// remove clone as yaml/edit as yaml until API supported // remove clone as yaml/edit as yaml until API supported
_availableActions() { _availableActions() {
let out = this._standardActions; let out = this._standardActions;
@ -19,5 +18,5 @@ export default {
}); });
return out; return out;
} },
}; };

View File

@ -10,7 +10,7 @@
"node": ">=10" "node": ">=10"
}, },
"scripts": { "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", "test": "./node_modules/.bin/nyc ava --serial --verbose",
"nuxt": "./node_modules/.bin/nuxt", "nuxt": "./node_modules/.bin/nuxt",
"dev": "./node_modules/.bin/nuxt dev", "dev": "./node_modules/.bin/nuxt dev",