mirror of https://github.com/rancher/dashboard.git
Merge pull request #294 from mantis-toboggan-md/secret-list-detail
secrets list/detail
This commit is contained in:
commit
38f7b7a360
|
|
@ -0,0 +1,40 @@
|
|||
import centered from '@storybook/addon-centered/vue';
|
||||
import DetailTop from '@/components/DetailTop';
|
||||
import LabeledInput from '@/components/form/LabeledInput';
|
||||
|
||||
export default {
|
||||
title: 'Components/DetailTop',
|
||||
components: DetailTop,
|
||||
decorators: [centered]
|
||||
};
|
||||
|
||||
export const Story = () => ({
|
||||
components: { DetailTop, LabeledInput },
|
||||
data() {
|
||||
return {
|
||||
columns: [
|
||||
{
|
||||
title: 'Example Title',
|
||||
content: 'Example Content'
|
||||
},
|
||||
{
|
||||
title: 'Second Title',
|
||||
content: 'Second Content'
|
||||
},
|
||||
{
|
||||
title: 'Column using slot',
|
||||
name: 'something'
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
template: `
|
||||
<DetailTop :columns='columns'>
|
||||
<template v-slot:something >
|
||||
<LabeledInput type="text" label="I'm in a slot!" />
|
||||
</template>
|
||||
</DetailTop>
|
||||
`
|
||||
});
|
||||
|
||||
Story.story = { name: 'DetailTop' };
|
||||
|
|
@ -1,6 +1,23 @@
|
|||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
columns: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="detail-top">
|
||||
<slot />
|
||||
<div v-for="col in columns" :key="col.title">
|
||||
<span>{{ col.title }}</span>
|
||||
<slot :name="col.name">
|
||||
<span>{{ col.content || 'n/a' }}</span>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -13,6 +30,10 @@
|
|||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
& >:first-child {
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
border-right: 2px solid var(--border);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
<script>
|
||||
export default { props: { title: { type: String, default: '' } } };
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<span>{{ title }}</span>
|
||||
<span><slot /></span>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,11 +1,16 @@
|
|||
<script>
|
||||
import labeledFormElement from '@/mixins/labeled-form-element';
|
||||
import LabeledInput from '@/components/form/LabeledInput';
|
||||
import LabeledSelect from '@/components/form/LabeledSelect';
|
||||
export default {
|
||||
components: { LabeledInput },
|
||||
components: { LabeledInput, LabeledSelect },
|
||||
mixins: [labeledFormElement],
|
||||
props: {
|
||||
label: {
|
||||
textLabel: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
selectLabel: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
|
@ -17,49 +22,56 @@ export default {
|
|||
type: Array,
|
||||
required: true
|
||||
},
|
||||
inputString: {
|
||||
textRequired: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
textValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
selectValue: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return { selected: this.options[0], string: this.inputString };
|
||||
return { selected: this.selectValue || this.options[0], string: this.textValue };
|
||||
},
|
||||
watch: {
|
||||
inputString(neu) {
|
||||
this.string = neu;
|
||||
selected() {
|
||||
this.change();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
change() {
|
||||
this.$emit('input', { option: this.selected, string: this.string });
|
||||
},
|
||||
blurred() {
|
||||
console.log('blurred');
|
||||
this.$emit('input', { selected: this.selected, text: this.string });
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="input-container" @input="change" @change="change">
|
||||
<v-select
|
||||
v-model="selected"
|
||||
class="in-input fixed"
|
||||
<div class="input-container row" @input="change">
|
||||
<LabeledSelect
|
||||
:value="selected"
|
||||
:label="selectLabel"
|
||||
class="in-input col span-5"
|
||||
:options="options"
|
||||
:clearable="false"
|
||||
:searchable="false"
|
||||
:disabled="isView"
|
||||
@search:focused="blurred"
|
||||
:disbaled="isView"
|
||||
:clearable="false"
|
||||
@input="e=>selected=e.value"
|
||||
/>
|
||||
<LabeledInput
|
||||
v-if="label"
|
||||
v-if="textLabel"
|
||||
v-model="string"
|
||||
class="input-string"
|
||||
:label="label"
|
||||
class="input-string col span-7"
|
||||
:label="textLabel"
|
||||
:placeholder="placeholder"
|
||||
:disabled="isView"
|
||||
:required="textRequired"
|
||||
/>
|
||||
<input
|
||||
v-else
|
||||
|
|
@ -73,17 +85,17 @@ export default {
|
|||
|
||||
<style lang='scss'>
|
||||
.input-container{
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
& .input-string{
|
||||
flex-shrink: 1;
|
||||
padding-right: 0;
|
||||
display: block;
|
||||
height: 50px;
|
||||
width:60%;
|
||||
}
|
||||
}
|
||||
.v-select.in-input{
|
||||
flex-basis:20%;
|
||||
.in-input {
|
||||
margin-right: 0;
|
||||
|
||||
& .v-select{
|
||||
height: 100%;
|
||||
|
||||
.vs__selected {
|
||||
margin: 0;
|
||||
|
|
@ -114,7 +126,14 @@ export default {
|
|||
|
||||
.vs__selected-options {
|
||||
display: -webkit-box;
|
||||
& .labeled-input {
|
||||
top:10px;
|
||||
& LABEL {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vs__actions {
|
||||
padding: 2px;;
|
||||
}
|
||||
|
|
@ -135,5 +154,6 @@ export default {
|
|||
transform: rotate(180deg) scale(0.75);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -13,9 +13,15 @@ export default {
|
|||
grouped: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return { selectedDisplay: 'block' };
|
||||
},
|
||||
computed: {
|
||||
currentLabel() {
|
||||
let entry;
|
||||
|
|
@ -49,50 +55,78 @@ export default {
|
|||
if ( this.$refs.input ) {
|
||||
this.$refs.input.placeholder = '';
|
||||
}
|
||||
},
|
||||
|
||||
searchFocus() {
|
||||
this.selectedDisplay = 'none';
|
||||
},
|
||||
|
||||
searchBlur() {
|
||||
this.selectedDisplay = 'block';
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="{'labeled-input': true, raised, focused, empty, [mode]: true}">
|
||||
<label>
|
||||
{{ label }}
|
||||
<span v-if="required && !value" class="required">*</span>
|
||||
</label>
|
||||
<label class="corner">
|
||||
<slot name="corner" />
|
||||
</label>
|
||||
<div>
|
||||
<div v-if="isView">
|
||||
{{ currentLabel }}
|
||||
</div>
|
||||
<select
|
||||
<v-select
|
||||
v-else
|
||||
ref="input"
|
||||
class="inline"
|
||||
v-bind="$attrs"
|
||||
:disabled="isView || disabled"
|
||||
:value="value"
|
||||
@input="$emit('input', $event.target.value)"
|
||||
:options="options"
|
||||
@input="e=>$emit('input', e)"
|
||||
@search:focus="searchFocus"
|
||||
@search:blur="searchBlur"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
>
|
||||
<option v-if="!focused" disabled value=""></option>
|
||||
<option v-if="focused" disabled value="">
|
||||
{{ placeholder }}
|
||||
</option>
|
||||
<slot name="options" :options="options">
|
||||
<template v-if="grouped">
|
||||
<optgroup v-for="grp in options" :key="grp.group" :label="grp.group">
|
||||
<option v-for="opt in grp.items" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</option>
|
||||
</optgroup>
|
||||
</template>
|
||||
<option v-for="opt in options" v-else :key="opt.value" :value="opt.value">
|
||||
<slot name="label" :opt="opt">
|
||||
{{ opt.label }}
|
||||
</slot>
|
||||
</option>
|
||||
</slot>
|
||||
</select>
|
||||
<template v-slot:selected-option-container>
|
||||
<div :class="{'labeled-input': true, raised, focused, empty, [mode]: true}" :style="{border:'none'}">
|
||||
<label>
|
||||
{{ label }}
|
||||
<span v-if="required && !value" class="required">*</span>
|
||||
</label>
|
||||
<label class="corner">
|
||||
<slot name="corner" />
|
||||
</label>
|
||||
<div v-if="isView">
|
||||
{{ currentLabel }}
|
||||
</div>
|
||||
<div class="selected" :style="{display:selectedDisplay}">
|
||||
{{ currentLabel }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</v-select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang='scss'>
|
||||
.v-select.inline {
|
||||
|
||||
& .labeled-input {
|
||||
background-color: rgba(0,0,0,0);
|
||||
|
||||
& *{
|
||||
background-color: rgba(0,0,0,0);
|
||||
}
|
||||
}
|
||||
|
||||
& .vs__search {
|
||||
background-color: none;
|
||||
padding: 3px 10px 0px 10px;
|
||||
}
|
||||
|
||||
& .selected{
|
||||
position:relative;
|
||||
top: 1.4em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ import { NAMESPACES } from '@/store/prefs';
|
|||
import { NAMESPACE, ANNOTATION } from '@/config/types';
|
||||
import { _CREATE, _VIEW } from '@/config/query-params';
|
||||
import LabeledInput from '@/components/form/LabeledInput';
|
||||
import LabeledSelect from '@/components/form/LabeledSelect';
|
||||
import InputWithSelect from '@/components/form/InputWithSelect';
|
||||
|
||||
export default {
|
||||
components: { LabeledInput, LabeledSelect },
|
||||
components: { LabeledInput, InputWithSelect },
|
||||
|
||||
props: {
|
||||
value: {
|
||||
|
|
@ -48,10 +48,6 @@ export default {
|
|||
generatedSuffix: {
|
||||
type: String,
|
||||
default: '-'
|
||||
},
|
||||
registerBeforeHook: {
|
||||
type: Function,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
|
@ -139,11 +135,6 @@ export default {
|
|||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if (this.registerBeforeHook) {
|
||||
this.registerBeforeHook(this.createNamespace);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const valueRef = get(this.$refs, 'name.$refs.value');
|
||||
|
||||
|
|
@ -152,24 +143,9 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
toggleNSMode() {
|
||||
this.createNS = !this.createNS;
|
||||
},
|
||||
async createNamespace() {
|
||||
if (this.createNS) {
|
||||
if (!this.toCreate) {
|
||||
throw new Error('no namespace name provided');
|
||||
} else {
|
||||
const nsSchema = this.$store.getters['cluster/schemaFor'](NAMESPACE);
|
||||
const data = { metadata: { name: this.toCreate } };
|
||||
|
||||
await nsSchema.followLink('collection', {
|
||||
method: 'POST',
|
||||
data
|
||||
});
|
||||
this.value.metadata.namespace = this.toCreate;
|
||||
}
|
||||
}
|
||||
changeNameNS(e) {
|
||||
this.name = e.text;
|
||||
this.value.metadata.namespace = e.selected;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -178,69 +154,34 @@ export default {
|
|||
<template>
|
||||
<div>
|
||||
<div class="row">
|
||||
<div :class="{col: true, [colSpan]: true}">
|
||||
<slot name="name">
|
||||
<LabeledInput
|
||||
ref="name"
|
||||
key="name"
|
||||
v-model="name"
|
||||
:mode="onlyForCreate"
|
||||
:label="nameLabel"
|
||||
:placeholder="namePlaceholder"
|
||||
:required="true"
|
||||
>
|
||||
<template v-if="notView && !wantDescription" #corner>
|
||||
<a v-if="mode!=='view'" href="#" @click.prevent="addDescription=true">Add a description</a>
|
||||
</template>
|
||||
</LabeledInput>
|
||||
</slot>
|
||||
</div>
|
||||
<div v-if="namespaced" :class="{col: true, [colSpan]: true}">
|
||||
<slot name="namespace">
|
||||
<LabeledInput v-if="createNS" v-model="toCreate" required label="Namespace" placeholder="e.g. myapp">
|
||||
<template #corner>
|
||||
<a v-if="!mode!=='view'" href="#" @click.prevent="toggleNSMode">
|
||||
Use an existing namespace
|
||||
</a>
|
||||
</template>
|
||||
</LabeledInput>
|
||||
<LabeledSelect
|
||||
v-else
|
||||
key="namespace"
|
||||
v-model="value.metadata.namespace"
|
||||
:mode="onlyForCreate"
|
||||
<InputWithSelect
|
||||
:options="namespaces"
|
||||
:required="true"
|
||||
label="Namespace"
|
||||
placeholder="Select a namespace"
|
||||
>
|
||||
<template #corner>
|
||||
<a v-if="registerBeforeHook && mode!=='view'" href="#" @click.prevent="toggleNSMode">
|
||||
Create new namespace
|
||||
</a>
|
||||
</template>
|
||||
</LabeledSelect>
|
||||
text-label="Name"
|
||||
select-label="Namespace"
|
||||
:text-value="name"
|
||||
:text-required="true"
|
||||
select-value="default"
|
||||
@input="changeNameNS"
|
||||
/>
|
||||
</slot>
|
||||
</div>
|
||||
<div :class="{col: true, [colSpan]: true}">
|
||||
<LabeledInput
|
||||
key="description"
|
||||
v-model="value.metadata.annotations[ANNOTATION_DESCRIPTION]"
|
||||
type="multiline"
|
||||
label="Description"
|
||||
:mode="mode"
|
||||
:placeholder="descriptionPlaceholder"
|
||||
:min-height="30"
|
||||
/>
|
||||
</div>
|
||||
<div v-for="slot in extraColumns" :key="slot" :class="{col: true, [colSpan]: true}">
|
||||
<slot :name="slot">
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="wantDescription" class="row">
|
||||
<div class="col span-12">
|
||||
<div>
|
||||
<LabeledInput
|
||||
key="description"
|
||||
v-model="value.metadata.annotations[ANNOTATION_DESCRIPTION]"
|
||||
type="multiline"
|
||||
label="Description"
|
||||
:mode="mode"
|
||||
:placeholder="descriptionPlaceholder"
|
||||
:min-height="30"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -12,28 +12,40 @@ export default {
|
|||
label: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
grouped: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
clicked() {
|
||||
this.$emit('input', this.value);
|
||||
}
|
||||
console.log('clicked', this.label);
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<label class="radio-container">
|
||||
<label class="radio-button">
|
||||
<label
|
||||
ref="radio"
|
||||
class="radio-button"
|
||||
:tabindex="grouped ? -1 : 0"
|
||||
>
|
||||
<input
|
||||
:checked="value"
|
||||
type="radio"
|
||||
:name="name"
|
||||
@click="clicked"
|
||||
:tabindex="-1"
|
||||
@keyup.16="clicked"
|
||||
@click.stop="clicked"
|
||||
/>
|
||||
<span class="radio-custom"><span /></span>
|
||||
</label>
|
||||
<span class="radio-label">{{ label }}</span>
|
||||
<span class="radio-label" @click.stop="clicked">{{ label }}</span>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -17,10 +17,11 @@ export default {
|
|||
},
|
||||
labels: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return this.options
|
||||
;
|
||||
}
|
||||
default: null
|
||||
},
|
||||
row: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
|
@ -30,7 +31,16 @@ export default {
|
|||
statuses[option] = index === this.selected;
|
||||
});
|
||||
|
||||
return { statuses };
|
||||
return { statuses, focused: false };
|
||||
},
|
||||
computed: {
|
||||
labelsToUse() {
|
||||
if (this.labels) {
|
||||
return this.labels;
|
||||
} else {
|
||||
return this.options;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
select(option) {
|
||||
|
|
@ -44,19 +54,51 @@ export default {
|
|||
for (const option of this.options) {
|
||||
this.statuses[option] = false;
|
||||
}
|
||||
},
|
||||
|
||||
focusGroup() {
|
||||
this.focused = true;
|
||||
},
|
||||
blurred() {
|
||||
this.focused = false;
|
||||
},
|
||||
|
||||
clickNext(direction) {
|
||||
const newSelection = this.options[this.selected + direction];
|
||||
|
||||
this.select(newSelection);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
ref="radio-group"
|
||||
class="radio-group"
|
||||
:style="{display:row?'flex':'block'}"
|
||||
tabindex="0"
|
||||
@focus="focusGroup"
|
||||
@blur="blurred"
|
||||
@keyup.39.stop="clickNext(1)"
|
||||
@keyup.37.stop="clickNext(-1)"
|
||||
>
|
||||
<RadioButton
|
||||
v-for="(option, i) in options"
|
||||
:key="option"
|
||||
:ref="`radio-${i}`"
|
||||
:value="statuses[option]"
|
||||
:label="labels[i]"
|
||||
:label="labelsToUse[i]"
|
||||
grouped
|
||||
:class="{focused:focused&&selected===i}"
|
||||
@input="select(option)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.radio-group:focus{
|
||||
border:none;
|
||||
outline:none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
maxLength: {
|
||||
type: Number,
|
||||
default: 20
|
||||
}
|
||||
},
|
||||
data() {
|
||||
const expanded = this.value.length <= this.maxLength;
|
||||
|
||||
return { expanded };
|
||||
},
|
||||
computed: {
|
||||
preview() {
|
||||
if (this.expanded) {
|
||||
return this.value;
|
||||
} else {
|
||||
return this.value.slice(0, this.maxLength);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
expand() {
|
||||
this.expanded = !this.expanded;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span @click.stop="expand">
|
||||
{{ preview }}
|
||||
<span v-if="!expanded">
|
||||
...
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<script>
|
||||
export default { props: { value: { type: String, required: true } } };
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a rel="nofollow noopener noreferrer" target="_blank" :href="value">{{ value }}</a>
|
||||
</template>
|
||||
|
|
@ -9,7 +9,8 @@ import {
|
|||
TARGET, TARGET_KIND, USERNAME, USER_DISPLAY_NAME, USER_ID, USER_STATUS,
|
||||
NODE_NAME, ROLES,
|
||||
VERSION, CPU,
|
||||
RAM, PODS
|
||||
RAM, PODS,
|
||||
CREATED
|
||||
} from '@/config/table-headers';
|
||||
import { _CREATE, _CLONE, _STAGE } from '@/config/query-params';
|
||||
|
||||
|
|
@ -183,15 +184,16 @@ export const FRIENDLY = {
|
|||
headers: [
|
||||
STATE,
|
||||
NAMESPACE_NAME,
|
||||
KEYS,
|
||||
{
|
||||
name: 'type',
|
||||
label: 'Type',
|
||||
value: 'typeDisplay',
|
||||
sort: ['typeDisplay', 'nameSort'],
|
||||
},
|
||||
KEYS,
|
||||
AGE
|
||||
CREATED
|
||||
],
|
||||
hasDetail: true
|
||||
},
|
||||
|
||||
users: {
|
||||
|
|
|
|||
|
|
@ -220,7 +220,7 @@ export const P95 = {
|
|||
|
||||
export const KEYS = {
|
||||
name: 'keys',
|
||||
label: 'Keys',
|
||||
label: 'Detail',
|
||||
sort: false,
|
||||
value: 'keysDisplay',
|
||||
};
|
||||
|
|
@ -315,10 +315,11 @@ export const KEY = {
|
|||
sort: ['key']
|
||||
};
|
||||
export const VALUE = {
|
||||
name: 'value',
|
||||
label: 'Value',
|
||||
value: 'value',
|
||||
sort: ['value']
|
||||
name: 'value',
|
||||
label: 'Value',
|
||||
value: 'value',
|
||||
sort: ['value'],
|
||||
formatter: 'ClickExpand'
|
||||
};
|
||||
export function headersFor(schema) {
|
||||
const out = [];
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ export const SECRET = 'core.v1.secret';
|
|||
export const EVENT = 'core.v1.event';
|
||||
export const RESOURCE_QUOTA = 'core.v1.resourcequota';
|
||||
export const NODE = 'core.v1.node';
|
||||
export const SERVICE_ACCOUNT = 'core.v1.serviceaccount';
|
||||
|
||||
export const CLOUD = {
|
||||
CLUSTER: 'cloud.rio.rancher.io.v1.cluster',
|
||||
|
|
@ -42,6 +43,8 @@ export const RANCHER = {
|
|||
};
|
||||
|
||||
export const ANNOTATION = {
|
||||
DESCRIPTION: 'cattle.io/description',
|
||||
TIMESTAMP: 'cattle.io/timestamp'
|
||||
DESCRIPTION: 'cattle.io/description',
|
||||
TIMESTAMP: 'cattle.io/timestamp',
|
||||
CERT_ISSUER: 'cert-manager.io/issuer-name',
|
||||
SERVICE_ACCT: 'kubernetes.io/service-account.uid'
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { ANNOTATION } from '@/config/types';
|
||||
import DetailTop from '@/components/DetailTop';
|
||||
import DetailTopColumn from '@/components/DetailTopColumn';
|
||||
import SortableTable from '@/components/SortableTable';
|
||||
import VStack from '@/components/Layout/Stack/VStack';
|
||||
import {
|
||||
|
|
@ -17,7 +17,6 @@ export default {
|
|||
name: 'DetailConfigMap',
|
||||
components: {
|
||||
DetailTop,
|
||||
DetailTopColumn,
|
||||
SortableTable,
|
||||
VStack
|
||||
},
|
||||
|
|
@ -73,22 +72,30 @@ export default {
|
|||
created: '2020-01-20T11:00:00+00:00'
|
||||
}
|
||||
];
|
||||
}
|
||||
},
|
||||
|
||||
detailTopColumns() {
|
||||
const { metadata = {} } = this.value;
|
||||
const { annotations = {} } = metadata;
|
||||
|
||||
return [
|
||||
{
|
||||
title: 'Description',
|
||||
content: annotations[ANNOTATION.DESCRIPTION]
|
||||
},
|
||||
{
|
||||
title: 'Namespace',
|
||||
content: metadata.namespace
|
||||
}
|
||||
];
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VStack class="config-map">
|
||||
<DetailTop>
|
||||
<DetailTopColumn title="Description">
|
||||
{{ value.metadata.annotations['cattle.io/description'] }}
|
||||
</DetailTopColumn>
|
||||
<DetailTopColumn title="Namespace">
|
||||
{{ value.metadata.namespace }}
|
||||
</DetailTopColumn>
|
||||
</DetailTop>
|
||||
<DetailTop :columns="detailTopColumns" />
|
||||
<div>
|
||||
<div class="title">
|
||||
Values
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
import CopyToClipboardText from '@/components/CopyToClipboardText';
|
||||
import ConsumptionGauge from '@/components/ConsumptionGauge';
|
||||
import DetailTop from '@/components/DetailTop';
|
||||
import DetailTopColumn from '@/components/DetailTopColumn';
|
||||
import HStack from '@/components/Layout/Stack/HStack';
|
||||
import VStack from '@/components/Layout/Stack/VStack';
|
||||
import Alert from '@/components/Alert';
|
||||
|
|
@ -23,7 +22,7 @@ export default {
|
|||
name: 'DetailNode',
|
||||
|
||||
components: {
|
||||
Alert, ConsumptionGauge, CopyToClipboardText, DetailTop, DetailTopColumn, HStack, VStack, Tab, Tabbed, SortableTable
|
||||
Alert, ConsumptionGauge, CopyToClipboardText, DetailTop, HStack, VStack, Tab, Tabbed, SortableTable
|
||||
},
|
||||
|
||||
props: {
|
||||
|
|
@ -93,6 +92,31 @@ export default {
|
|||
key,
|
||||
value: this.value.metadata.annotations[key]
|
||||
}));
|
||||
},
|
||||
|
||||
detailTopColumns() {
|
||||
return [
|
||||
{
|
||||
title: 'Description',
|
||||
content: this.value.id
|
||||
},
|
||||
{
|
||||
title: 'IP Address',
|
||||
name: 'ipAddress'
|
||||
},
|
||||
{
|
||||
title: 'Version',
|
||||
name: 'version'
|
||||
},
|
||||
{
|
||||
title: 'OS',
|
||||
name: 'os'
|
||||
},
|
||||
{
|
||||
title: 'Created',
|
||||
name: 'created'
|
||||
},
|
||||
];
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -111,22 +135,19 @@ export default {
|
|||
|
||||
<template>
|
||||
<VStack class="node">
|
||||
<DetailTop>
|
||||
<DetailTopColumn title="Description">
|
||||
{{ value.id }}
|
||||
</DetailTopColumn>
|
||||
<DetailTopColumn title="IP Address">
|
||||
<DetailTop :columns="detailTopColumns">
|
||||
<template v-slot:ipAddress>
|
||||
<CopyToClipboardText :text="value.status.addresses[0].address" />
|
||||
</DetailTopColumn>
|
||||
<DetailTopColumn title="Version">
|
||||
</template>
|
||||
<template v-slot:version>
|
||||
<CopyToClipboardText :text="value.status.nodeInfo.kubeletVersion" />
|
||||
</DetailTopColumn>
|
||||
<DetailTopColumn title="OS">
|
||||
</template>
|
||||
<template v-slot:os>
|
||||
<CopyToClipboardText :text="value.status.nodeInfo.operatingSystem" />
|
||||
</DetailTopColumn>
|
||||
<DetailTopColumn title="Created">
|
||||
</template>
|
||||
<template v-slot:created>
|
||||
<CopyToClipboardText :text="value.metadata.creationTimestamp" />
|
||||
</DetailTopColumn>
|
||||
</template>
|
||||
</DetailTop>
|
||||
<HStack class="glance" :show-dividers="true">
|
||||
<VStack class="alerts" :show-dividers="true" vertical-align="space-evenly">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,155 @@
|
|||
<script>
|
||||
import { get } from '@/utils/object';
|
||||
import { ANNOTATION } from '@/config/types';
|
||||
import { DOCKER_JSON } from '@/models/core.v1.secret';
|
||||
import DetailTop from '@/components/DetailTop';
|
||||
import SortableTable from '@/components/SortableTable';
|
||||
import { KEY, VALUE } from '@/config/table-headers';
|
||||
import { base64Decode } from '@/utils/crypto';
|
||||
export default {
|
||||
components: {
|
||||
DetailTop,
|
||||
SortableTable,
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return { relatedServices: [] };
|
||||
},
|
||||
computed: {
|
||||
dockerJSON() {
|
||||
return DOCKER_JSON;
|
||||
},
|
||||
|
||||
dockerRows() {
|
||||
const auths = JSON.parse(this.dataRows[0].value).auths;
|
||||
|
||||
const address = Object.keys(auths)[0];
|
||||
const { username, url } = auths[address];
|
||||
|
||||
return [{
|
||||
address, username, url
|
||||
}];
|
||||
},
|
||||
|
||||
dockerHeaders() {
|
||||
const headers = [
|
||||
{
|
||||
name: 'address',
|
||||
label: 'Address',
|
||||
value: 'address',
|
||||
formatter: 'ExternalLink'
|
||||
},
|
||||
{
|
||||
name: 'username',
|
||||
label: 'Username',
|
||||
value: 'username',
|
||||
}
|
||||
];
|
||||
|
||||
if (this.dockerRows[0].url) {
|
||||
headers.push({
|
||||
name: 'url',
|
||||
label: 'url(for artifactory/custom)',
|
||||
value: 'url'
|
||||
});
|
||||
}
|
||||
|
||||
return headers;
|
||||
},
|
||||
|
||||
issuer() {
|
||||
const { metadata:{ annotations = {} } } = this.value;
|
||||
|
||||
return annotations[ANNOTATION.CERT_ISSUER];
|
||||
},
|
||||
description() {
|
||||
const { metadata:{ annotations = {} } } = this.value;
|
||||
|
||||
return annotations[ANNOTATION.DESCRIPTION];
|
||||
},
|
||||
detailTopColumns() {
|
||||
const columns = [
|
||||
{
|
||||
title: 'Description',
|
||||
content: this.description
|
||||
},
|
||||
{
|
||||
title: 'Namespace',
|
||||
content: get(this.value, 'metadata.namespace')
|
||||
},
|
||||
{
|
||||
title: 'Type',
|
||||
content: this.value.typeDisplay
|
||||
}
|
||||
];
|
||||
|
||||
if (this.issuer) {
|
||||
columns.push({
|
||||
title: 'Issuer',
|
||||
content: this.issuer
|
||||
});
|
||||
}
|
||||
|
||||
return columns;
|
||||
},
|
||||
|
||||
dataRows() {
|
||||
const rows = [];
|
||||
const { data = {} } = this.value;
|
||||
|
||||
Object.keys(data).forEach((key) => {
|
||||
const value = base64Decode(data[key]);
|
||||
|
||||
rows.push({
|
||||
key,
|
||||
value
|
||||
});
|
||||
});
|
||||
|
||||
return rows;
|
||||
},
|
||||
|
||||
dataHeaders() {
|
||||
return [KEY, VALUE];
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<DetailTop :columns="detailTopColumns" />
|
||||
<template v-if="value.secretType===dockerJSON">
|
||||
<SortableTable
|
||||
class="mt-20"
|
||||
key-field="address"
|
||||
:rows="dockerRows"
|
||||
:headers="dockerHeaders"
|
||||
:search="false"
|
||||
:table-actions="false"
|
||||
:row-actions="false"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="mt-20 mb-20">
|
||||
<h4>Values</h4>
|
||||
<SortableTable
|
||||
class="mt-20"
|
||||
:rows="dataRows"
|
||||
:headers="dataHeaders"
|
||||
key-field="value"
|
||||
:search="false"
|
||||
:row-actions="false"
|
||||
:table-actions="false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -7,8 +7,11 @@ import {
|
|||
} from '@/config/table-headers';
|
||||
import { sortBy } from '@/utils/sort';
|
||||
import LiveDate from '@/components/formatter/LiveDate';
|
||||
import DetailTop from '@/components/DetailTop';
|
||||
export default {
|
||||
components: { ResourceTable, LiveDate },
|
||||
components: {
|
||||
ResourceTable, LiveDate, DetailTop
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
|
|
@ -18,24 +21,6 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
// mockEvents: [
|
||||
// {
|
||||
// _type: 'Warning',
|
||||
// name: 'rhdrtrtehtrrhe',
|
||||
// reason: 'sdsfweqrwerer',
|
||||
// message: 'dskhfsdkhfahfauewhfew',
|
||||
// eventTime: Date.now(),
|
||||
// links: { self: 'http://google.com' }
|
||||
// },
|
||||
// {
|
||||
// _type: 'Something Else',
|
||||
// name: 'sdfsafasdfasfsdfhft',
|
||||
// reason: 'sdsfwwertewtrewer',
|
||||
// message: 'ouehfpifkrsdfheorf',
|
||||
// eventTime: Date.now() + Date.now(),
|
||||
// links: { self: 'http://google.com' }
|
||||
// }
|
||||
// ],
|
||||
pods: [],
|
||||
events: [],
|
||||
eventColumns: [
|
||||
|
|
@ -96,6 +81,26 @@ export default {
|
|||
},
|
||||
ports() {
|
||||
return get(this.value, 'spec.ports') || [];
|
||||
},
|
||||
detailTopColumns() {
|
||||
return [
|
||||
{
|
||||
title: 'Namespace',
|
||||
content: this.namespace
|
||||
},
|
||||
{
|
||||
title: 'Image',
|
||||
content: get(this.value, 'spec.image')
|
||||
},
|
||||
{
|
||||
title: 'Scale',
|
||||
content: this.scaleAvailable
|
||||
},
|
||||
{
|
||||
title: 'Created',
|
||||
content: this.creationTimestamp
|
||||
}
|
||||
];
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
|
@ -164,26 +169,7 @@ export default {
|
|||
|
||||
<template>
|
||||
<div class="service-detail">
|
||||
<div class="detail-top">
|
||||
<div>
|
||||
<span>Namespace</span>
|
||||
<span>{{ namespace }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>Image</span>
|
||||
<span>{{ value.spec.image }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>Scale</span>
|
||||
<span>
|
||||
{{ scaleAvailable }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>Created</span>
|
||||
<span> {{ creationTimestamp }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<DetailTop :columns="detailTopColumns" />
|
||||
<div>
|
||||
<h4>Pods</h4>
|
||||
<ResourceTable
|
||||
|
|
@ -329,25 +315,4 @@ export default {
|
|||
opacity: 0.5
|
||||
}
|
||||
}
|
||||
.detail-top{
|
||||
display: flex;
|
||||
min-height: 75px;
|
||||
& > * {
|
||||
margin-right: 20px;
|
||||
padding: 10px 0 10px 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
&:not(:last-child){
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
|
||||
& >:not(:first-child){
|
||||
color: var(--input-label);
|
||||
padding: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,45 +1,256 @@
|
|||
<script>
|
||||
import { DOCKER_JSON, OPAQUE, TLS } from '@/models/core.v1.secret';
|
||||
import { base64Encode } from '@/utils/crypto';
|
||||
import { get } from '@/utils/object';
|
||||
import { ANNOTATION, NAMESPACE } from '@/config/types';
|
||||
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 LabeledInput from '@/components/form/LabeledInput';
|
||||
import RadioGroup from '@/components/form/RadioGroup';
|
||||
import NameNsDescription from '@/components/form/NameNsDescription';
|
||||
import LabeledSelect from '@/components/form/LabeledSelect';
|
||||
|
||||
export default {
|
||||
name: 'CruSecret',
|
||||
|
||||
components: {
|
||||
NameNsDescription,
|
||||
KeyValue,
|
||||
Footer,
|
||||
LabeledInput,
|
||||
LabeledSelect,
|
||||
RadioGroup,
|
||||
NameNsDescription
|
||||
},
|
||||
mixins: [CreateEditView],
|
||||
data() {
|
||||
const types = [
|
||||
{ label: 'Registry', value: DOCKER_JSON },
|
||||
{ label: 'Secret', value: OPAQUE },
|
||||
{ label: 'Certificate', value: TLS }
|
||||
];
|
||||
const registryAddresses = [
|
||||
'DockerHub', 'Quay.io', 'Artifactory', 'Custom'
|
||||
];
|
||||
const isNamespaced = !!this.value.metadata.namespace;
|
||||
|
||||
return {
|
||||
types,
|
||||
isNamespaced,
|
||||
registryAddresses,
|
||||
newNS: false,
|
||||
registryProvider: registryAddresses[0],
|
||||
username: '',
|
||||
password: '',
|
||||
registryFQDN: null,
|
||||
toUpload: null,
|
||||
key: null,
|
||||
cert: null
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
certificate() {
|
||||
return TLS;
|
||||
},
|
||||
|
||||
name: {
|
||||
get() {
|
||||
return get(this.value, 'metadata.name');
|
||||
},
|
||||
set(neu) {
|
||||
this.$set(this.value.metadata, 'name', neu);
|
||||
}
|
||||
},
|
||||
|
||||
description: {
|
||||
get() {
|
||||
const { metadata:{ annotations = {} } } = this.value;
|
||||
|
||||
return annotations[ANNOTATION.DESCRIPTION] || '';
|
||||
},
|
||||
set(neu) {
|
||||
this.$set(this.value.metadata.annotations, ANNOTATION.DESCRIPTION, neu );
|
||||
}
|
||||
},
|
||||
|
||||
type: {
|
||||
get() {
|
||||
return this.value._type || OPAQUE;
|
||||
},
|
||||
set(neu) {
|
||||
this.$set(this.value, '_type', neu.value);
|
||||
}
|
||||
},
|
||||
|
||||
namespace: {
|
||||
get() {
|
||||
if (this.isNamespaced) {
|
||||
return get(this.value, 'metadata.namespace') || 'default';
|
||||
} else {
|
||||
return 'n/a';
|
||||
}
|
||||
},
|
||||
set(neu) {
|
||||
this.$set(this.value.metadata, 'namespace', neu);
|
||||
}
|
||||
},
|
||||
|
||||
dockerconfigjson() {
|
||||
let dockerServer = this.registryProvider === 'DockerHub' ? 'index.dockerhub.io/v1/' : 'quay.io';
|
||||
|
||||
if (this.needsDockerServer) {
|
||||
dockerServer = this.registryFQDN;
|
||||
}
|
||||
|
||||
if (this.isRegistry && dockerServer) {
|
||||
const config = {
|
||||
auths: {
|
||||
[dockerServer]: {
|
||||
username: this.username,
|
||||
password: this.password,
|
||||
}
|
||||
}
|
||||
};
|
||||
const json = JSON.stringify(config);
|
||||
|
||||
return json;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
namespaces() {
|
||||
return this.$store.getters['cluster/all'](NAMESPACE).map((obj) => {
|
||||
return {
|
||||
label: obj.nameDisplay,
|
||||
value: obj.id,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
isRegistry() {
|
||||
return this.type === DOCKER_JSON;
|
||||
},
|
||||
|
||||
needsDockerServer() {
|
||||
return this.registryProvider === 'Artifactory' || this.registryProvider === 'Custom';
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
saveSecret(buttonCB) {
|
||||
if (this.type === DOCKER_JSON) {
|
||||
const data = { '.dockerconfigjson': base64Encode(this.dockerconfigjson) };
|
||||
|
||||
this.$set(this.value, 'data', data);
|
||||
} else if (this.type === TLS) {
|
||||
const data = { 'tls.cert': base64Encode(this.cert), 'tls.key': base64Encode(this.key) };
|
||||
|
||||
this.$set(this.value, 'data', data);
|
||||
}
|
||||
this.save(buttonCB);
|
||||
},
|
||||
|
||||
fileUpload(field) {
|
||||
this.toUpload = field;
|
||||
this.$refs.uploader.click();
|
||||
},
|
||||
|
||||
fileChange(event) {
|
||||
const input = event.target;
|
||||
const handles = input.files;
|
||||
const names = [];
|
||||
|
||||
if ( handles ) {
|
||||
for ( let i = 0 ; i < handles.length ; i++ ) {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (loaded) => {
|
||||
const value = loaded.target.result;
|
||||
|
||||
this[this.toUpload] = value;
|
||||
};
|
||||
|
||||
reader.onerror = (err) => {
|
||||
this.$dispatch('growl/fromError', { title: 'Error reading file', err }, { root: true });
|
||||
};
|
||||
|
||||
names[i] = handles[i].name;
|
||||
reader.readAsText(handles[i]);
|
||||
}
|
||||
|
||||
input.value = '';
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form>
|
||||
<NameNsDescription
|
||||
:value="value"
|
||||
:mode="mode"
|
||||
name-label="Name"
|
||||
:register-before-hook="registerBeforeHook"
|
||||
/>
|
||||
<NameNsDescription v-model="value" :mode="mode" :extra-columns="['type']">
|
||||
<template v-slot:type>
|
||||
<LabeledSelect v-model="type" label="Type" :options="types" />
|
||||
</template>
|
||||
</NameNsDescription>
|
||||
<template v-if="isRegistry">
|
||||
<div id="registry-type" class="row">
|
||||
Provider: <RadioGroup row :options="registryAddresses" :selected="registryAddresses.indexOf(registryProvider)" @input="e=>registryProvider = e" />
|
||||
</div>
|
||||
<div v-if="needsDockerServer" class="row">
|
||||
<LabeledInput v-model="registryFQDN" label="Registry Domain Name" placeholder="Docker registry FQDN" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col span-6">
|
||||
<LabeledInput v-model="username" label="Username" />
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabeledInput v-model="password" label="Password" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="spacer"></div>
|
||||
|
||||
<div class="row">
|
||||
<div v-else-if="type===certificate" class="row">
|
||||
<div class="col span-6">
|
||||
<KeyValue
|
||||
key="data"
|
||||
v-model="value.data"
|
||||
:mode="mode"
|
||||
title="Data"
|
||||
:initial-empty-row="true"
|
||||
:value-base64="true"
|
||||
/>
|
||||
<LabeledInput v-model="key" label="Private Key" />
|
||||
<button type="button" class="btn btn-sm bg-primary mt-10" @click="fileUpload('key')">
|
||||
READ FROM FILE
|
||||
</button>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabeledInput v-model="cert" label="CA Certificate" />
|
||||
<button type="button" class="btn btn-sm bg-primary mt-10" @click="fileUpload('cert')">
|
||||
READ FROM FILE
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer :mode="mode" :errors="errors" @save="save" @done="done" />
|
||||
<div v-else class="row">
|
||||
<KeyValue
|
||||
key="data"
|
||||
v-model="value.data"
|
||||
:mode="mode"
|
||||
title="Data"
|
||||
:initial-empty-row="true"
|
||||
:value-base64="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<input
|
||||
ref="uploader"
|
||||
type="file"
|
||||
class="hide"
|
||||
@change="fileChange"
|
||||
/>
|
||||
|
||||
<Footer :mode="mode" :errors="errors" @save="saveSecret" @done="done" />
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
#registry-type {
|
||||
display: flex;
|
||||
align-items:center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -42,8 +42,8 @@ export default {
|
|||
this.$emit('input', out );
|
||||
},
|
||||
update(input) {
|
||||
this.value = input.string;
|
||||
this.type = input.option;
|
||||
this.value = input.text;
|
||||
this.type = input.selected;
|
||||
this.change();
|
||||
}
|
||||
}
|
||||
|
|
@ -52,6 +52,6 @@ export default {
|
|||
|
||||
<template>
|
||||
<div class="match-input">
|
||||
<InputWithSelect :options="types" :input-string="spec[type]" :label="label" :placeholder="placeholder" @input="update" />
|
||||
<InputWithSelect :options="types" :text-value="spec[type]" :label="label" :placeholder="placeholder" @input="update" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export default {
|
|||
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: ''
|
||||
},
|
||||
|
||||
required: {
|
||||
|
|
|
|||
Loading…
Reference in New Issue