Merge pull request #294 from mantis-toboggan-md/secret-list-detail

secrets list/detail
This commit is contained in:
Vincent Fiduccia 2020-01-31 11:52:21 -07:00 committed by GitHub
commit 38f7b7a360
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 795 additions and 280 deletions

View File

@ -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' };

View File

@ -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);
}

View File

@ -1,10 +0,0 @@
<script>
export default { props: { title: { type: String, default: '' } } };
</script>
<template>
<div>
<span>{{ title }}</span>
<span><slot /></span>
</div>
</template>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = [];

View File

@ -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'
};

View File

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

View File

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

155
detail/core.v1.secret.vue Normal file
View File

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

View File

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

View File

@ -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: &nbsp; <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>

View File

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

View File

@ -11,7 +11,7 @@ export default {
label: {
type: String,
required: true,
default: ''
},
required: {