Merge pull request #37 from vincent99/master

Public domains and stuffs
This commit is contained in:
Vincent Fiduccia 2019-11-04 15:29:11 -07:00 committed by GitHub
commit 97de65d3cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 980 additions and 355 deletions

View File

@ -0,0 +1,45 @@
<script>
import ResourceYaml from '@/components/ResourceYaml';
export async function asyncData(ctx) {
const { resource, namespace, id } = ctx.params;
const fqid = (namespace ? `${ namespace }/` : '') + id;
const obj = await ctx.store.dispatch('cluster/find', { type: resource, id: fqid });
const value = await obj.followLink('view', { headers: { accept: 'application/yaml' } });
return {
obj,
value: value.data
};
}
export default {
components: { ResourceYaml },
props: {
asyncData: {
type: Object,
required: true,
},
},
data() {
return { ...this.asyncData };
},
computed: {
doneRoute() {
const name = this.$route.name.replace(/(-namespace)?-id$/, '');
return name;
}
},
};
</script>
<template>
<div>
<ResourceYaml :obj="obj" :value="value" :done-route="doneRoute" />
</div>
</template>

147
components/RioDetail.vue Normal file
View File

@ -0,0 +1,147 @@
<script>
import CreateEditView from '@/mixins/create-edit-view';
import ResourceYaml from '@/components/ResourceYaml';
import { FRIENDLY } from '@/config/friendly';
import {
MODE, _VIEW, _EDIT, EDIT_YAML, _FLAGGED
} from '@/config/query-params';
export async function asyncData({ store, params, route }) {
const { resource, namespace, id } = params;
const type = FRIENDLY[resource].type;
const asYaml = route.query[EDIT_YAML] === _FLAGGED;
const schema = store.getters['cluster/schemaFor'](type);
let fqid = id;
if ( schema.attributes.namespaced ) {
fqid = `${ namespace }/${ fqid }`;
}
const obj = await store.dispatch('cluster/find', { type, id: fqid });
const model = await store.dispatch('cluster/clone', obj);
const view = await obj.followLink('view', { headers: { accept: 'application/yaml' } });
const out = {
asYaml,
resource,
model,
yaml: view.data,
originalModel: obj
};
return out;
}
export const watchQuery = [MODE, EDIT_YAML];
export default {
components: { ResourceYaml },
mixins: { CreateEditView },
props: {
asyncData: {
type: Object,
required: true,
},
},
data() {
const mode = this.$route.query.mode || _VIEW;
return {
mode,
...this.asyncData
};
},
computed: {
isView() {
return this.mode === _VIEW;
},
isEdit() {
return this.mode === _EDIT;
},
type() {
return FRIENDLY[this.resource].type;
},
doneRoute() {
const name = this.$route.name.replace(/(-namespace)?-id$/, '');
return name;
},
doneParams() {
return this.$route.params;
},
parentLink() {
const name = this.doneRoute;
const params = this.donneParams;
const out = this.$router.resolve({ name, params }).href;
return out;
},
cruComponent() {
return () => import(`@/components/cru/${ this.type }`);
},
typeDisplay() {
return FRIENDLY[this.resource].singular;
},
},
methods: {
showActions() {
this.$store.commit('selection/show', {
resources: this.originalModel,
elem: this.$refs.actions,
});
},
}
};
</script>
<template>
<div>
<ResourceYaml
v-if="asYaml"
:obj="model"
:value="yaml"
:done-route="doneRoute"
:parent-route="doneRoute"
:parent-params="doneParams"
/>
<template v-else>
<header>
<h1 v-trim-whitespace>
<span v-if="isEdit">Edit</span>
<nuxt-link v-trim-whitespace :to="parentLink">
{{ typeDisplay }}
</nuxt-link>: {{ originalModel.nameDisplay }}
</h1>
<div v-if="isView" class="actions">
<button ref="actions" class="btn btn-sm bg-primary actions" @click="showActions">
<i class="icon icon-actions" />
</button>
</div>
</header>
<component
:is="cruComponent"
v-model="model"
:original-value="originalModel"
:done-route="doneRoute"
:done-params="doneParams"
:parent-route="doneRoute"
:parent-params="doneParams"
:namespace-suffix-on-create="true"
:type-label="typeDisplay"
:mode="mode"
/>
</template>
</div>
</template>

View File

@ -0,0 +1,315 @@
<script>
import { findBy } from '../../utils/array';
import { TLS } from '../../models/core.v1.secret';
import LoadDeps from '@/mixins/load-deps';
import Loading from '@/components/Loading';
import CreateEditView from '@/mixins/create-edit-view';
import NameNsDescription from '@/components/form/NameNsDescription';
import LabeledSelect from '@/components/form/LabeledSelect';
import Footer from '@/components/form/Footer';
import { RIO, SECRET } from '@/config/types';
import { groupAndFilterOptions } from '@/utils/group';
import { allHash } from '@/utils/promise';
const KIND_LABELS = {
'router': 'A router',
'app': 'All versions of a service',
'version': 'A single version of a service',
};
const SECRET_LABELS = {
'auto': 'Automatically generate a certificate',
'secret': 'Choose a secret in the rio-system namespace',
};
export default {
name: 'CruPublicDomain',
components: {
Loading,
NameNsDescription,
LabeledSelect,
Footer,
},
mixins: [CreateEditView, LoadDeps],
data() {
let spec = this.value.spec;
if ( !this.value.spec ) {
spec = {};
this.value.spec = spec;
}
let kind, targetApp, targetVersion, targetRouter, secretKind, secret;
if ( spec.targetVersion ) {
targetApp = spec.targetApp;
targetVersion = spec.targetVersion;
kind = 'version';
} else if ( spec.targetApp ) {
const matchingRouter = findBy(this.allRouters, 'app', spec.targetApp );
if ( matchingRouter ) {
targetRouter = spec.targetApp;
kind = 'router';
} else {
targetApp = spec.targetApp;
kind = 'app';
}
} else {
kind = 'router';
}
if ( spec.secret ) {
secret = spec.secret;
secretKind = 'secret';
} else {
secretKind = 'auto';
}
return {
allServices: null,
allRouters: null,
allSecrets: null,
targetNamespace: spec.targetNamespace || null,
kind,
targetApp,
targetVersion,
targetRouter,
secretKind,
secret,
};
},
computed: {
appOptions() {
return groupAndFilterOptions(this.allServices, null, { itemValueKey: 'namespaceApp', itemLabelKey: 'app' });
},
routerOptions() {
return groupAndFilterOptions(this.allRouters, null, { itemValueKey: 'namespaceApp', itemLabelKey: 'app' });
},
secretOptions() {
return groupAndFilterOptions(this.allSecrets, {
'metadata.namespace': RIO.SYSTEM_NAMESPACE,
'secretType': TLS,
}, {
groupBy: null,
itemValueKey: 'metadata.name',
});
},
versionOptions() {
const namespaceApp = this.targetApp;
if ( !namespaceApp ) {
return [];
}
return groupAndFilterOptions(this.allServices, { namespaceApp }, {
groupBy: null,
itemValueKey: 'version',
itemLabelKey: 'versionWithDateDisplay',
itemSortKey: 'metadata.creationTimestamp:desc'
});
},
kindLabels() {
return KIND_LABELS;
},
kindOptions() {
return Object.keys(KIND_LABELS).map((k) => {
return { label: KIND_LABELS[k], value: k };
});
},
secretKindLabels() {
return SECRET_LABELS;
},
secretKindOptions() {
return Object.keys(SECRET_LABELS).map((k) => {
return { label: SECRET_LABELS[k], value: k };
});
}
},
watch: {
kind() {
this.update();
},
secretKind() {
this.update();
},
},
methods: {
async loadDeps() {
const hash = await allHash({
services: this.$store.dispatch('cluster/findAll', { type: RIO.SERVICE }),
routers: this.$store.dispatch('cluster/findAll', { type: RIO.ROUTER }),
secrets: this.$store.dispatch('cluster/findAll', { type: SECRET }),
});
this.allServices = hash.services;
this.allRouters = hash.routers;
this.allSecrets = hash.secrets;
},
update() {
const spec = this.value.spec;
spec.targetNamespace = null;
spec.targetRouter = null;
spec.targetApp = null;
spec.targetVersion = null;
switch ( this.kind ) {
case 'router':
if ( this.targetRouter ) {
const [ns, router] = this.targetRouter.split(':', 2);
this.targetNamespace = ns;
spec.targetNamespace = ns;
spec.targetRouter = router;
}
break;
case 'app':
case 'version':
if ( this.targetApp ) {
const [ns, app] = this.targetApp.split(':', 2);
this.targetNamespace = ns;
spec.targetNamespace = ns;
spec.targetApp = app;
}
if ( this.kind === 'version' ) {
if ( this.targetVersion ) {
spec.targetVersion = this.targetVersion;
} else if ( this.versionOptions.length ) {
this.targetVersion = this.versionOptions[0].value;
spec.targetVersion = this.targetVersion;
}
}
break;
}
if ( this.secretKind === 'secret' ) {
if ( this.secret ) {
spec.secretName = this.secret;
}
}
}
},
};
</script>
<template>
<form>
<Loading ref="loader" />
<div v-if="loading">
</div>
<template v-else>
<NameNsDescription
:namespaced="false"
:value="value"
:mode="mode"
name-label="Public Domain Name"
/>
<hr />
<div class="row">
<div class="col span-11-of-23">
<h4>Target</h4>
<div v-if="mode === 'view'">
{{ kindLabels[kind] }}
</div>
<div v-else>
<div v-for="opt in kindOptions" :key="opt.value">
<label class="radio">
<input v-model="kind" type="radio" :value="opt.value" />
{{ opt.label }}
</label>
</div>
</div>
<div v-if="kind === 'router'" class="mt-20">
<LabeledSelect
v-model="targetRouter"
:options="routerOptions"
:grouped="true"
:mode="mode"
label="Target Router"
placeholder="Select a Router..."
@input="update"
/>
</div>
<div v-if="kind === 'app' || kind === 'version'" class="mt-20">
<LabeledSelect
v-model="targetApp"
:mode="mode"
label="Target App"
:options="appOptions"
:grouped="true"
placeholder="Select a service"
@input="update"
/>
</div>
<div v-if="kind === 'version'" class="mt-20">
<LabeledSelect
v-model="targetVersion"
label="Target Version"
:mode="mode"
:options="versionOptions"
placeholder="Select a version"
@input="update"
/>
</div>
</div>
<div class="col span-1-of-23" style="position: relative; overflow: hidden">
<hr class="vertical" />
</div>
<div class="col span-11-of-23">
<h4>Certificate</h4>
<div v-if="mode === 'view'">
{{ secretKindLabels[kind] }}
</div>
<div v-else>
<div v-for="opt in secretKindOptions" :key="opt.value">
<label class="radio">
<input v-model="secretKind" type="radio" :value="opt.value" />
{{ opt.label }}
</label>
</div>
</div>
<div v-if="secretKind === 'secret'" class="mt-20">
<LabeledSelect
v-model="secret"
:mode="mode"
label="Secret Name"
:options="secretOptions"
placeholder="Select a Certificate Secret..."
@input="update"
/>
</div>
</div>
</div>
<Footer :mode="mode" :errors="errors" @save="save" @done="done" />
</template>
</form>
</template>

View File

@ -10,6 +10,12 @@ import Footer from '@/components/form/Footer';
import { RIO } from '@/config/types';
import { groupAndFilterOptions } from '@/utils/group';
const KIND_LABELS = {
'service': 'Another service',
'ip': 'A list of IP Addresses',
'fqdn': 'A DNS name',
};
export default {
name: 'CruExternalService',
@ -48,10 +54,6 @@ export default {
targetService = `${ spec.targetServiceNamespace }/${ spec.targetServiceName }`;
}
if ( typeof window !== 'undefined' ) {
window.v = this.value;
}
return {
kind,
allServices: null,
@ -65,6 +67,16 @@ export default {
serviceOptions() {
return groupAndFilterOptions(this.allServices);
},
kindLabels() {
return KIND_LABELS;
},
kindOptions() {
return Object.keys(KIND_LABELS).map((k) => {
return { label: KIND_LABELS[k], value: k };
});
}
},
watch: {
@ -128,26 +140,25 @@ export default {
<div class="row">
<div class="col span-12">
<h4>Target</h4>
<div>
<label class="radio">
<input v-model="kind" type="radio" value="service" /> Another service
</label>
<div v-if="mode === 'view'">
{{ kindLabels[kind] }}
</div>
<div>
<label class="radio">
<input v-model="kind" type="radio" value="fqdn" /> A DNS name
</label>
</div>
<div>
<label class="radio">
<input v-model="kind" type="radio" value="ip" /> One or more IP addresses
</label>
<div v-else>
<div v-for="opt in kindOptions" :key="opt.value">
<label class="radio">
<input v-model="kind" type="radio" :value="opt.value" />
{{ opt.label }}
</label>
</div>
</div>
</div>
</div>
<div class="row">
<div v-if="kind === 'service'" class="col span-6">
<select v-model="targetService">
<template v-if="isView">
{{ targetService }}
</template>
<select v-else v-model="targetService">
<option disabled value="">
Select a Service...
</option>
@ -159,7 +170,7 @@ export default {
</select>
</div>
<div v-if="kind === 'fqdn'" class="col span-6">
<LabeledInput v-model="fqdn" label="DNS FQDN" @input="update" />
<LabeledInput v-model="fqdn" :mode="mode" label="DNS FQDN" @input="update" />
</div>
<div v-if="kind === 'ip'" class="col span-6">
<ArrayList

View File

@ -1,7 +1,6 @@
<script>
import LabeledInput from '@/components/form/LabeledInput';
import InputWithSelect from '@/components/form/InputWithSelect';
import RadioGroup from '@/components/form/RadioGroup';
export default {
components: { InputWithSelect },
props: {
@ -21,11 +20,13 @@ export default {
default: () => {}
}
},
data() {
return {
types: this.options || ['exact', 'prefix', 'regexp'], value: Object.values(this.spec)[0] || '', type: Object.keys(this.spec)[0] || 'exact'
};
},
methods: {
selectType(type) {
this.type = type;

View File

@ -86,7 +86,7 @@ export default {
<div class="row">
<div class="col span-6">
<UnitInput
v-model="spec.memory"
v-model.number="spec.memoryBytes"
:mode="mode"
:increment="1024"
:input-exponent="2"
@ -96,7 +96,7 @@ export default {
</div>
<div class="col span-6">
<UnitInput
v-model="spec.cpu"
v-model="spec.cpuMillis"
:mode="mode"
label="CPU Reservation"
:increment="1000"

View File

@ -101,7 +101,7 @@ export default {
return KIND_LABELS;
},
kindChoices() {
kindOptions() {
return Object.keys(KIND_LABELS).map((k) => {
return { label: KIND_LABELS[k], value: k };
});
@ -163,7 +163,7 @@ export default {
{{ kindLabels[kind] }}
</div>
<div v-else>
<div v-for="opt in kindChoices" :key="opt.value">
<div v-for="opt in kindOptions" :key="opt.value">
<label class="radio">
<input v-model="kind" type="radio" :value="opt.value" />
{{ opt.label }}

View File

@ -31,7 +31,7 @@ export default {
<div class="col span-6">
<LabeledInput
key="increment"
v-model="spec.weight"
v-model.number="spec.weight"
:mode="mode"
type="number"
min="0"
@ -43,7 +43,7 @@ export default {
<div class="col span-6">
<LabeledInput
key="increment"
v-model="spec.rollout.increment"
v-model.number="spec.rollout.increment"
:mode="mode"
type="number"
min="1"
@ -52,7 +52,7 @@ export default {
</div>
<div class="col span-6">
<UnitInput
v-model="spec.rollout.interval"
v-model.number="spec.rollout.intervalSeconds"
:mode="mode"
label="Rollout Interval"
suffix="sec"
@ -64,7 +64,7 @@ export default {
<div class="col span-6">
<LabeledInput
key="maxUnavailable"
v-model="spec.maxUnavailable"
v-model.number="spec.maxUnavailable"
:mode="mode"
type="number"
min="0"
@ -74,7 +74,7 @@ export default {
<div class="col span-6">
<LabeledInput
key="maxSurge"
v-model="spec.maxSurge"
v-model.number="spec.maxSurge"
:mode="mode"
type="number"
min="0"

View File

@ -65,10 +65,6 @@ export default {
spec.imagePullPolicy = 'Always';
}
if ( typeof window !== 'undefined' ) {
window.v = this.value;
}
return {
multipleContainers,
nameResource,

View File

@ -10,11 +10,23 @@ export default {
type: Array,
default: null,
},
grouped: {
type: Boolean,
default: false,
}
},
computed: {
currentLabel() {
const entry = findBy(this.options || [], 'value', this.value);
let entry;
if ( this.grouped ) {
for ( let i = 0 ; i < this.options.length && !entry ; i++ ) {
entry = findBy(this.options[i].items || [], 'value', this.value);
}
} else {
entry = findBy(this.options || [], 'value', this.value);
}
if ( entry ) {
return entry.label;
@ -65,7 +77,14 @@ export default {
{{ placeholder }}
</option>
<slot name="options" :options="options">
<option v-for="opt in options" :key="opt.value" :value="opt.value">
<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>

View File

@ -16,7 +16,11 @@ export default {
type: String,
required: true,
},
threeColumn: {
namespaced: {
type: Boolean,
default: true,
},
extraColumn: {
type: Boolean,
default: false,
},
@ -81,6 +85,13 @@ export default {
notView() {
return this.mode !== _VIEW;
},
colSpan() {
const cols = 1 + (this.namespaced ? 1 : 0) + (this.extraColumn ? 1 : 0);
const span = 12 / cols;
return `span-${ span }`;
}
},
};
@ -89,7 +100,7 @@ export default {
<template>
<div>
<div class="row">
<div :class="{col: true, 'span-6': !threeColumn, 'span-4': threeColumn}">
<div :class="{col: true, [colSpan]: true}">
<slot name="name">
<LabeledInput
key="name"
@ -105,7 +116,7 @@ export default {
</LabeledInput>
</slot>
</div>
<div :class="{col: true, 'span-6': !threeColumn, 'span-4': threeColumn}">
<div v-if="namespaced" :class="{col: true, [colSpan]: true}">
<slot name="namespace">
<LabeledSelect
key="namespace"
@ -118,7 +129,7 @@ export default {
/>
</slot>
</div>
<div v-if="threeColumn" class="col span-4">
<div v-if="extraColumn" :class="{col: true, [colSpan]: true}">
<slot name="right">
</slot>
</div>

View File

@ -7,7 +7,7 @@ export default {
props: {
value: {
type: Number,
type: Number,
default: null
},

View File

@ -0,0 +1,25 @@
<script>
export default {
props: {
value: {
type: String,
default: ''
},
row: {
type: Object,
required: true
},
col: {
type: Object,
default: () => {}
},
},
};
</script>
<template>
<span>
{{ value }}%
</span>
</template>

View File

@ -0,0 +1,78 @@
<script>
import { RIO } from '@/config/types';
import { filterBy } from '@/utils/array';
export default {
props: {
value: {
type: Number,
default: 0,
},
row: {
type: Object,
required: true
},
col: {
type: Object,
default: () => {}
},
},
computed: {
total() {
const services = this.$store.getters['cluster/all'](RIO.SERVICE);
const forThisApp = filterBy(services, 'app', this.row.app);
let desired = 0;
let current = 0;
for ( const service of forThisApp ) {
const weights = service.weights;
desired += weights.desired || 0;
current += weights.current || 0;
}
desired = Math.max(1, desired);
current = Math.max(1, current);
return { desired, current };
},
desired() {
const desired = this.row.weights.desired;
const total = this.total.desired;
return Math.round(desired / total * 1000) / 10;
},
current() {
const current = this.row.weights.current;
const total = this.total.current;
if ( total === 0 ) {
return 100;
}
return Math.round(current / total * 1000) / 10;
},
showDesired() {
return this.current !== this.desired;
},
}
};
</script>
<template>
<div>
<p v-trim-whitespace :class="{'text-muted': current === 100 && desired === 100}">
{{ current }}%
</p>
<div v-if="showDesired">
<i class="icon icon-chevron-right" />
{{ desired }}%
</div>
</div>
</template>

View File

@ -1,6 +1,9 @@
import { CONFIG_MAP, SECRET, RIO } from '@/config/types';
import {
STATE, NAMESPACE_NAME, RIO_IMAGE, SCALE, AGE, KEYS, TARGET, TARGET_KIND,
STATE, NAME, NAMESPACE_NAME, AGE,
RIO_IMAGE, WEIGHT, SCALE,
KEYS,
TARGET, TARGET_KIND, TARGET_SECRET,
} from '@/config/table-headers';
export const FRIENDLY = {
@ -31,8 +34,17 @@ export const FRIENDLY = {
'public-domains': {
singular: 'Public Domain',
plural: 'Public Domains',
type: RIO.PUBLIC_DOMAIN
type: RIO.PUBLIC_DOMAIN,
headers: [
STATE,
NAME,
TARGET_KIND,
TARGET,
TARGET_SECRET,
AGE,
],
},
services: {
singular: 'Service',
plural: 'Services',
@ -41,20 +53,24 @@ export const FRIENDLY = {
STATE,
NAMESPACE_NAME,
RIO_IMAGE,
WEIGHT,
SCALE,
AGE,
]
},
stack: {
singular: 'Stack',
plural: 'Stacks',
type: RIO.STACK
},
routers: {
singular: 'Router',
plural: 'Routers',
type: RIO.ROUTER
},
secrets: {
singular: 'Secret',
plural: 'Secrets',

View File

@ -74,12 +74,14 @@ export const SCALE = {
formatter: 'Scale',
align: 'center',
};
export const WEIGHT = {
name: 'weight',
label: 'Weight',
sort: false,
width: 60,
value: 'status.computedWeight',
width: 100,
align: 'center',
formatter: 'Weight',
};
export const SUCCESS = {
@ -114,7 +116,7 @@ export const KEYS = {
};
export const TARGET_KIND = {
name: 'targetKind',
name: 'target-kind',
label: 'Target Type',
value: 'kindDisplay',
width: 100,
@ -126,6 +128,13 @@ export const TARGET = {
value: 'targetDisplay',
};
export const TARGET_SECRET = {
name: 'secret-name',
label: 'Secret',
value: 'secretName',
sort: ['secretName', 'targetApp', 'targetVersion', 'id'],
};
export function headersFor(schema) {
const out = [];
const columns = schema.attributes.columns;

View File

@ -15,13 +15,15 @@ export const RIO = {
CLUSTER_DOMAIN: 'admin.rio.cattle.io.v1.clusterdomain',
FEATURE: 'admin.rio.cattle.io.v1.feature',
INFO: 'admin.rio.cattle.io.v1.rioinfo',
PUBLIC_DOMAIN: 'admin.rio.cattle.io.v1.publicdomain',
APP: 'rio.cattle.io.v1.app',
EXTERNAL_SERVICE: 'rio.cattle.io.v1.externalservice',
PUBLIC_DOMAIN: 'admin.rio.cattle.io.v1.publicdomain',
STACK: 'rio.cattle.io.v1.stack',
ROUTER: 'rio.cattle.io.v1.router',
SERVICE: 'rio.cattle.io.v1.service',
APP: 'rio.cattle.io.v1.app',
EXTERNAL_SERVICE: 'rio.cattle.io.v1.externalservice',
STACK: 'rio.cattle.io.v1.stack',
ROUTER: 'rio.cattle.io.v1.router',
SERVICE: 'rio.cattle.io.v1.service',
SYSTEM_NAMESPACE: 'rio-system',
};
export const RANCHER = {

View File

@ -36,6 +36,10 @@ export default {
},
data() {
if ( typeof window !== 'undefined' ) {
window.v = this.value;
}
return { errors: null };
},

View File

@ -0,0 +1,56 @@
export default {
targetKind() {
const spec = this.spec;
if ( !spec ) {
return 'unknown';
}
if ( spec.targetRouter && spec.targetNamespace ) {
return 'router';
}
if ( spec.targetApp && spec.targetVersion && spec.targetNamespace ) {
return 'version';
}
if ( spec.targetApp && spec.targetNamespace ) {
return 'app';
}
return 'unknown';
},
kindDisplay() {
// Satisfy eslint that it's a string...
const kind = `${ this.targetKind }`;
switch ( kind ) {
case 'router':
return 'Router';
case 'version':
return 'Service version';
case 'app':
return 'Service';
case 'unknown':
return '?';
}
},
targetDisplay() {
// Satisfy eslint that it's a string...
const kind = `${ this.targetKind }`;
switch ( kind ) {
case 'router':
return `${ this.spec.targetNamespace }/${ this.spec.targetRouter }`;
case 'version':
return `${ this.spec.targetNamespace }/${ this.spec.targetApp }@${ this.spec.targetVersion }`;
case 'app':
return `${ this.spec.targetNamespace }/${ this.spec.targetApp }`;
}
return '';
}
};

View File

@ -1,13 +1,23 @@
export const OPAQUE = 'Opaque';
export const SERVICE_ACCT = 'kubernetes.io/service-account-token';
export const DOCKER = 'kubernetes.io/dockercfg';
export const DOCKER_JSON = 'kubernetes.io/dockerconfigjson';
export const BASIC = 'kubernetes.io/basic-auth';
export const SSH = 'kubernetes.io/ssh-auth';
export const TLS = 'kubernetes.io/tls';
export const BOOTSTRAP = 'bootstrap.kubernetes.io/token';
export const ISTIO_TLS = 'istio.io/key-and-cert';
const DISPLAY_TYPES = {
Opaque: 'Opaque',
'kubernetes.io/service-account-token': 'Service Acct',
'kubernetes.io/dockercfg': 'Dockercfg',
'kubernetes.io/dockerconfigjson': 'Docker JSON',
'kubernetes.io/basic-auth': 'Basic Auth',
'kubernetes.io/ssh-auth': 'SSH',
'kubernetes.io/tls': 'TLS',
'bootstrap.kubernetes.io/token': 'Bootstrap Token',
'istio.io/key-and-cert': 'TLS (Istio)',
[OPAQUE]: 'Opaque',
[SERVICE_ACCT]: 'Service Acct',
[DOCKER]: 'Dockercfg',
[DOCKER_JSON]: 'Docker JSON',
[BASIC]: 'Basic Auth',
[SSH]: 'SSH',
[TLS]: 'TLS',
[BOOTSTRAP]: 'Bootstrap Token',
[ISTIO_TLS]: 'TLS (Istio)',
};
export default {
@ -28,6 +38,10 @@ export default {
return keys.join(', ');
},
secretType() {
return this._type;
},
typeDisplay() {
const mapped = DISPLAY_TYPES[this._type];

View File

@ -1,7 +1,11 @@
export default {
kind() {
targetKind() {
const spec = this.spec;
if ( !spec ) {
return 'unknown';
}
if ( spec.targetServiceName && spec.targetServiceNamespace ) {
return 'service';
}
@ -19,7 +23,7 @@ export default {
kindDisplay() {
// Satisfy eslint that it's a string...
const kind = `${ this.kind }`;
const kind = `${ this.targetKind }`;
switch ( kind ) {
case 'service':
@ -35,7 +39,7 @@ export default {
targetDisplay() {
// Satisfy eslint that it's a string...
const kind = `${ this.kind }`;
const kind = `${ this.targetKind }`;
switch ( kind ) {
case 'service':

View File

@ -0,0 +1,9 @@
export default {
app() {
return this.spec.app || this.status.computedApp || this.metadata.name;
},
namespaceApp() {
return `${ this.metadata.namespace }:${ this.app }`;
},
};

View File

@ -1,21 +1,31 @@
import day from 'dayjs';
import { insertAt } from '@/utils/array';
import { ADD_SIDECAR, _FLAGGED } from '@/config/query-params';
import { escapeHtml } from '@/utils/string';
import { DATE_FORMAT, TIME_FORMAT } from '@/store/prefs';
export default {
appKey() {
return `${ this.spec.namespace }/${ this.appName }`;
app() {
return this.spec.app || this.status.computedApp || this.metadata.name;
},
appName() {
return this.spec.app || this.metadata.name;
},
versionName() {
return this.spec.version || 'v0';
version() {
return this.spec.version || this.status.computedVersion;
},
nameDisplay() {
return `${ this.appName }:${ this.versionName }`;
return `${ this.app } (${ this.version })`;
},
namespaceNameDisplay() {
const namespace = this.metadata.namespace;
const name = this.metadata.name || this.id;
return `${ namespace }:${ name }`;
},
namespaceApp() {
return `${ this.metadata.namespace }:${ this.app }`;
},
imageDisplay() {
@ -26,6 +36,17 @@ export default {
.replace(/localhost:5442\/(.*)/i, '$1 (local)');
},
createdDisplay() {
const dateFormat = escapeHtml( this.$rootGetters['prefs/get'](DATE_FORMAT));
const timeFormat = escapeHtml( this.$rootGetters['prefs/get'](TIME_FORMAT));
return day(this.metadata.creationTimestamp).format(`${ dateFormat } ${ timeFormat }`);
},
versionWithDateDisplay() {
return `${ this.version } (${ this.createdDisplay })`;
},
scales() {
const status = this.status || {};
let scaleStatus = status.scaleStatus;
@ -37,10 +58,11 @@ export default {
}
const spec = (typeof this.spec.replicas === 'undefined' ? 1 : this.spec.replicas || 0);
const global = this.spec.global === true;
const current = status.computedReplicas || 0;
const available = scaleStatus.available || 0;
const current = (typeof this.status.computedReplicas === 'undefined' ? available : status.computedReplicas || 0);
const unavailable = scaleStatus.unavailable || 0;
const global = this.spec.global === true;
let desired = spec;
if ( global ) {
@ -177,6 +199,32 @@ export default {
};
},
weights() {
let current = 0;
let desired = 0;
const spec = this.spec.weight;
if ( !this.status ) {
return { current, desired };
}
const status = this.status.computedWeight;
if ( typeof status === 'number' ) {
current = status;
} else if ( typeof spec === 'number' ) {
current = spec;
}
if ( typeof spec === 'number' ) {
desired = spec;
} else if ( typeof status === 'number' ) {
desired = status;
}
return { current, desired };
},
async pauseOrResume(pause = true) {
try {
await this.patch({
@ -236,110 +284,4 @@ export default {
return this.goToEdit({ [ADD_SIDECAR]: _FLAGGED });
};
},
// @TODO fake
/*
pods() {
const out = [];
const status = this.status.scaleStatus;
if ( !status ) {
return out;
}
let idx = 1;
for ( let i = 0 ; i < status.ready ; i++ ) {
let state = 'active';
let transitioning = 'no';
if ( i >= this.spec.scale ) {
state = 'removing';
transitioning = 'yes';
}
out.push(store.createRecord({
type: 'pod',
name: `${ this.nameDisplay }-${ idx }`,
state,
transitioning,
containers: [
store.createRecord({
type: 'container',
name: `container${ idx }`,
state,
transitioning,
})
]
}));
idx++;
}
for ( let i = 0 ; i < status.available ; i++ ) {
out.push(store.createRecord({
type: 'pod',
name: `${ get(this, 'nameDisplay') }-${ idx }`,
state: 'not-ready',
transitioning: 'no',
containers: [
store.createRecord({
type: 'container',
name: `container${ idx }`,
state: 'not-ready',
transitioning: 'no',
})
]
}));
idx++;
}
for ( let i = 0 ; i < status.unavailable ; i++ ) {
out.push(store.createRecord({
type: 'pod',
name: `${ get(this, 'nameDisplay') }-${ idx }`,
state: 'creating',
transitioning: 'yes',
containers: [
store.createRecord({
type: 'container',
name: `container${ idx }`,
state: 'transitioning',
transitioning: 'yes',
})
]
}));
idx++;
}
return out;
},
partiallyUpdated: computed('scale', 'scaleStatus.updated', function() {
let scale = get(this, 'scale');
let status = get(this, 'scaleStatus');
if ( !status ) {
return false;
}
if ( scale > 0 && status.updated > 0 && scale > status.updated ) {
return true;
}
return false;
}),
updatedPercent: computed('scale', 'scaleStatus.updated', function() {
let scale = get(this, 'scale');
let status = get(this, 'scaleStatus');
return formatPercent(100 * status.updated / scale);
}),
*/
};

View File

@ -1,5 +1,6 @@
import fs from 'fs';
import path from 'path';
import { trimWhitespaceSsr } from './plugins/trim-whitespace';
require('dotenv').config();
@ -74,6 +75,8 @@ module.exports = {
cssSourceMap: true
},
render: { bundleRenderer: { directives: { trimWhitespace: trimWhitespaceSsr } } },
modern: true,
generate: { dir: outputDir },

View File

@ -1,34 +1,13 @@
<script>
import ResourceYaml from '@/components/ResourceYaml';
import ExplorerDetail, { asyncData } from '@/components/ExplorerDetail';
export default {
components: { ResourceYaml },
computed: {
doneRoute() {
const name = this.$route.name.replace(/(-namespace)?-id$/, '');
return name;
}
},
async asyncData(ctx) {
const { resource, namespace, id } = ctx.params;
const fqid = (namespace ? `${ namespace }/` : '') + id;
const obj = await ctx.store.dispatch('cluster/find', { type: resource, id: fqid });
const value = await obj.followLink('view', { headers: { accept: 'application/yaml' } });
return {
obj,
value: value.data
};
}
name: 'ExplorerGroupResourceId',
components: { ExplorerDetail },
asyncData
};
</script>
<template>
<div>
<ResourceYaml :obj="obj" :value="value" :done-route="doneRoute" />
</div>
<ExplorerDetail :async-data="_data" />
</template>

View File

@ -1,9 +1,13 @@
<script>
export { default } from '../_id.vue';
import ExplorerDetail, { asyncData } from '@/components/ExplorerDetail';
export default {
name: 'ExplorerGroupResourceNamespaceId',
components: { ExplorerDetail },
asyncData
};
</script>
<template>
<div>
<ResourceYaml :obj="obj" :value="value" :done-route="doneRoute" />
</div>
<ExplorerDetail :async-data="_data" />
</template>

View File

@ -0,0 +1,14 @@
<script>
import RioDetail, { watchQuery, asyncData } from '@/components/RioDetail';
export default {
name: 'RioResourceId',
components: { RioDetail },
asyncData,
watchQuery,
};
</script>
<template>
<RioDetail :async-data="_data" />
</template>

View File

@ -1,130 +1,14 @@
<script>
import CreateEditView from '@/mixins/create-edit-view';
import ResourceYaml from '@/components/ResourceYaml';
import { FRIENDLY } from '@/config/friendly';
import {
MODE, _VIEW, _EDIT, EDIT_YAML, _FLAGGED
} from '@/config/query-params';
import RioDetail, { watchQuery, asyncData } from '@/components/RioDetail';
export default {
components: { ResourceYaml },
mixins: { CreateEditView },
watchQuery: [MODE, EDIT_YAML],
data() {
const mode = this.$route.query.mode || _VIEW;
return { mode };
},
computed: {
isView() {
return this.mode === _VIEW;
},
isEdit() {
return this.mode === _EDIT;
},
type() {
return FRIENDLY[this.resource].type;
},
doneRoute() {
const name = this.$route.name.replace(/(-namespace)?-id$/, '');
return name;
},
doneParams() {
return this.$route.params;
},
parentLink() {
const name = this.doneRoute;
const params = this.donneParams;
const out = this.$router.resolve({ name, params }).href;
return out;
},
cruComponent() {
return () => import(`@/components/cru/${ this.type }`);
},
typeDisplay() {
return FRIENDLY[this.resource].singular;
},
},
async asyncData({ store, params, route }) {
const { resource, namespace, id } = params;
const fqid = `${ namespace }/${ id }`;
const type = FRIENDLY[resource].type;
const asYaml = route.query[EDIT_YAML] === _FLAGGED;
const obj = await store.dispatch('cluster/find', { type, id: fqid });
const model = await store.dispatch('cluster/clone', obj);
const view = await obj.followLink('view', { headers: { accept: 'application/yaml' } });
const out = {
asYaml,
resource,
model,
yaml: view.data,
originalModel: obj
};
return out;
},
methods: {
showActions() {
this.$store.commit('selection/show', {
resources: this.originalModel,
elem: this.$refs.actions,
});
},
}
name: 'RioNamespaceResourceId',
components: { RioDetail },
asyncData,
watchQuery
};
</script>
<template>
<div>
<ResourceYaml
v-if="asYaml"
:obj="model"
:value="yaml"
:done-route="doneRoute"
:parent-route="doneRoute"
:parent-params="doneParams"
/>
<template v-else>
<header>
<h1 v-trim-whitespace>
<span v-if="isEdit">Edit</span>
<nuxt-link v-trim-whitespace :to="parentLink">
{{ typeDisplay }}
</nuxt-link>: {{ originalModel.nameDisplay }}
</h1>
<div v-if="isView" class="actions">
<button ref="actions" class="btn btn-sm bg-primary actions" @click="showActions">
<i class="icon icon-actions" />
</button>
</div>
</header>
<component
:is="cruComponent"
v-model="model"
:original-value="originalModel"
:done-route="doneRoute"
:done-params="doneParams"
:parent-route="doneRoute"
:parent-params="doneParams"
:namespace-suffix-on-create="true"
:type-label="typeDisplay"
:mode="mode"
/>
</template>
</div>
<RioDetail :async-data="_data" />
</template>

View File

@ -58,6 +58,7 @@ export default {
return {
resource,
schema,
type,
model,
};
@ -79,7 +80,7 @@ export default {
v-model="model"
:done-route="doneRoute"
:done-params="doneParams"
:namespace-suffix-on-create="true"
:namespace-suffix-on-create="schema.attributes.namespaced"
:type-label="typeDisplay"
mode="create"
/>

View File

@ -59,7 +59,7 @@ export default {
const name = this.metadata.name || this.id;
if ( namespace ) {
return `${ namespace }/${ name }`;
return `${ namespace }:${ name }`;
}
return name;

View File

@ -1,14 +1,34 @@
import Vue from 'vue';
function trimEmptyTextNodes(el) {
export function trimWhitespace(el, dir) {
for (const node of el.childNodes) {
if (node.nodeType === Node.TEXT_NODE && node.data.trim() === '') {
node.remove();
if (node.nodeType === Node.TEXT_NODE ) {
const trimmed = node.data.trim();
if ( trimmed === '') {
node.remove();
} else if ( trimmed !== node.data ) {
node.data = trimmed;
}
}
}
}
export function trimWhitespaceSsr(el, dir) {
for ( const node of (el.children || []) ) {
if ( node.text ) {
const trimmed = node.text.trim();
if ( trimmed !== node.text ) {
node.text = trimmed;
}
} else if ( node.children ) {
trimWhitespaceSsr(node);
}
}
}
Vue.directive('trim-whitespace', {
inserted: trimEmptyTextNodes,
componentUpdated: trimEmptyTextNodes
inserted: trimWhitespace,
componentUpdated: trimWhitespace
});

View File

@ -2,11 +2,13 @@ import { get } from '@/utils/object';
import { filterBy } from '@/utils/array';
import { sortBy } from '@/utils/sort';
// groupAndFilterBy(services, 'default')
export function groupAndFilterOptions(ary, filterValue, {
filterKey = 'metadata.namespace',
const NOT_GROUPED = 'none';
groupKey = 'metadata.namespace',
// groupAndFilterBy(services, 'default')
export function groupAndFilterOptions(ary, filter, {
defaultFilterKey = 'metadata.namespace',
groupBy = 'metadata.namespace',
groupPrefix = 'Namespace: ',
itemLabelKey = 'nameDisplay',
@ -15,8 +17,11 @@ export function groupAndFilterOptions(ary, filterValue, {
} = {}) {
let matching;
if ( filterKey && filterValue ) {
matching = filterBy((ary || []), filterKey, filterValue);
if ( filter && typeof filter === 'object' ) {
matching = filterBy((ary || []), filter);
} else if ( filter ) {
// If you want to filter on a value that's false-y (false, null, undefined) use the object version of filter
matching = filterBy((ary || []), defaultFilterKey, filter);
} else {
matching = ary;
}
@ -24,31 +29,42 @@ export function groupAndFilterOptions(ary, filterValue, {
const groups = {};
for ( const match of matching ) {
const name = get(match, groupKey);
const name = groupBy ? get(match, groupBy) : NOT_GROUPED;
let entry = groups[name];
if ( !entry ) {
entry = {
group: `${ groupPrefix }${ name }`,
items: [],
items: {},
};
groups[name] = entry;
}
entry.items.push({
obj: match,
label: get(match, itemLabelKey),
value: get(match, itemValueKey),
});
const value = get(match, itemValueKey);
if ( !entry.items[value] ) {
entry.items[value] = {
obj: match,
label: get(match, itemLabelKey),
value
};
}
}
const out = Object.keys(groups).map((name) => {
const entry = groups[name];
entry.items = sortBy(entry.items, `obj.${ itemSortKey }`);
entry.items = sortBy(Object.values(entry.items), `obj.${ itemSortKey }`);
return entry;
});
return sortBy(out, 'group');
if ( groupBy ) {
return sortBy(out, 'group');
} else if ( out.length ) {
return out[0].items;
} else {
return [];
}
}