mirror of https://github.com/rancher/dashboard.git
431 lines
12 KiB
Vue
431 lines
12 KiB
Vue
<script>
|
|
import { isNull, isUndefined } from 'lodash';
|
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
|
import { SERVICE } from '@shell/config/types';
|
|
import { PROTOCOLS } from '@shell/config/schema';
|
|
import CruResource from '@shell/components/CruResource';
|
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
import { RadioGroup } from '@components/Form/Radio';
|
|
import FileImageSelector from '@shell/components/form/FileImageSelector';
|
|
import NameNsDescription, { normalizeName } from '@shell/components/form/NameNsDescription';
|
|
import Tab from '@shell/components/Tabbed/Tab';
|
|
import Tabbed from '@shell/components/Tabbed';
|
|
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
|
|
|
|
const LINK_TYPE_URL = 'url';
|
|
const LINK_TYPE_SERVICE = 'service';
|
|
const LINK_TARGET_NAMED = 'LINK_TARGET_NAMED';
|
|
const LINK_TARGET_BLANK = '_blank';
|
|
const LINK_TARGET_SELF = '_self';
|
|
|
|
export default {
|
|
mixins: [CreateEditView],
|
|
components: {
|
|
CruResource,
|
|
LabeledInput,
|
|
RadioGroup,
|
|
NameNsDescription,
|
|
Tabbed,
|
|
Tab,
|
|
FileImageSelector,
|
|
LabeledSelect
|
|
},
|
|
data() {
|
|
return {
|
|
targetOptions: [
|
|
{
|
|
value: LINK_TARGET_BLANK,
|
|
label: this.t('navLink.tabs.target.option.blank'),
|
|
},
|
|
{
|
|
value: LINK_TARGET_SELF,
|
|
label: this.t('navLink.tabs.target.option.self'),
|
|
},
|
|
{
|
|
value: LINK_TARGET_NAMED,
|
|
label: this.t('navLink.tabs.target.option.named'),
|
|
}
|
|
],
|
|
urlTypeOptions: [
|
|
{
|
|
value: LINK_TYPE_URL,
|
|
label: this.t('navLink.tabs.link.type.url')
|
|
},
|
|
{
|
|
value: LINK_TYPE_SERVICE,
|
|
label: this.t('navLink.tabs.link.type.service')
|
|
}
|
|
],
|
|
currentLinkType: null,
|
|
targetName: null,
|
|
currentTarget: LINK_TARGET_BLANK,
|
|
protocolsOptions: PROTOCOLS,
|
|
services: [],
|
|
currentService: null,
|
|
};
|
|
},
|
|
props: {
|
|
value: {
|
|
type: Object,
|
|
required: true,
|
|
default: () => {}
|
|
},
|
|
mode: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
},
|
|
computed: {
|
|
/**
|
|
* Identify type of navLink and clear model on value change
|
|
*/
|
|
linkType: {
|
|
get() {
|
|
return this.currentLinkType;
|
|
},
|
|
set(type) {
|
|
switch (type) {
|
|
case LINK_TYPE_URL:
|
|
delete this.value.spec.toService;
|
|
this.$set(this.value.spec, 'toURL', '');
|
|
break;
|
|
case LINK_TYPE_SERVICE:
|
|
delete this.value.spec.toURL;
|
|
this.$set(this.value.spec, 'toService', {});
|
|
break;
|
|
// No default
|
|
}
|
|
this.currentLinkType = type;
|
|
}
|
|
},
|
|
isService() {
|
|
return Boolean(this.value.spec.toService);
|
|
},
|
|
isURL() {
|
|
return !isNull(this.value.spec.toURL) && !isUndefined(this.value.spec.toURL);
|
|
},
|
|
isNamedWindow() {
|
|
return this.currentTarget === LINK_TARGET_NAMED;
|
|
},
|
|
mappedServices() {
|
|
return this.services.map(({ id, metadata: { name, namespace } }) => ({
|
|
label: id, id, name, namespace
|
|
}) );
|
|
},
|
|
},
|
|
async fetch() {
|
|
this.services = await this.$store
|
|
.dispatch('cluster/findAll', { type: SERVICE });
|
|
},
|
|
methods: {
|
|
/**
|
|
* Set the target of the navLink
|
|
* It will assign namedWindow value for named target cases
|
|
* @param {string} value
|
|
*/
|
|
setTargetValue(value) {
|
|
switch (value) {
|
|
case LINK_TARGET_SELF:
|
|
case LINK_TARGET_BLANK:
|
|
this.$set(this.value.spec, 'target', value);
|
|
break;
|
|
default:
|
|
this.$set(this.value.spec, 'target', this.targetName);
|
|
break;
|
|
}
|
|
},
|
|
/**
|
|
* Identify target option based on value and update UI accordingly
|
|
* Note: Custom target name is not directly bound to the resource
|
|
*/
|
|
setTargetOption() {
|
|
const value = this.value.spec.target;
|
|
|
|
switch (value) {
|
|
case LINK_TARGET_SELF:
|
|
case LINK_TARGET_BLANK:
|
|
this.currentTarget = value;
|
|
break;
|
|
default:
|
|
this.currentTarget = LINK_TARGET_NAMED;
|
|
this.targetName = value;
|
|
break;
|
|
}
|
|
},
|
|
/**
|
|
* Set URL type based on existing data
|
|
*/
|
|
setUrlType() {
|
|
if (this.isURL) {
|
|
this.currentLinkType = LINK_TYPE_URL;
|
|
}
|
|
if (this.isService) {
|
|
this.currentLinkType = LINK_TYPE_SERVICE;
|
|
}
|
|
},
|
|
/**
|
|
* Initialize resource on creation
|
|
*/
|
|
setDefaultValues() {
|
|
if (!this.value.spec) {
|
|
// Link to URL is set as default option from the data
|
|
this.$set(this.value, 'spec', { toURL: '' });
|
|
}
|
|
if (!this.value.metadata) {
|
|
this.$set(this.value, 'metadata', {});
|
|
}
|
|
if (!this.value.spec.target) {
|
|
this.$set(this.value.spec, 'target', LINK_TARGET_BLANK);
|
|
}
|
|
},
|
|
/**
|
|
* Set namespace and name from the selected service
|
|
* @param {label: string, id: string, name: string, namespace: string} service
|
|
*/
|
|
setService(service) {
|
|
if (service) {
|
|
const { name, namespace } = service;
|
|
|
|
this.$set(this.value.spec.toService, 'name', name);
|
|
this.$set(this.value.spec.toService, 'namespace', namespace);
|
|
}
|
|
},
|
|
/**
|
|
* Set paired values of namespace and name for the service
|
|
*/
|
|
setCurrentService() {
|
|
const name = this.value.spec.toService?.name;
|
|
const namespace = this.value.spec.toService?.namespace;
|
|
|
|
if (name && namespace) {
|
|
this.currentService = `${ namespace }/${ name }`;
|
|
}
|
|
},
|
|
/**
|
|
* Generate automatically kebab case for the displayed label
|
|
*/
|
|
setName() {
|
|
this.$set(this.value.metadata, 'name', normalizeName(this.value.spec.label));
|
|
},
|
|
/**
|
|
* Get error chained validation based on existing label
|
|
* @param {string} label
|
|
*/
|
|
getError(label) {
|
|
return this.$store.getters['i18n/t']('validation.required', { key: this.t(label) });
|
|
},
|
|
/**
|
|
* Verify all fields are fulfilled or display footer notification
|
|
*/
|
|
validate() {
|
|
const errors = [];
|
|
|
|
switch (this.currentLinkType) {
|
|
case LINK_TYPE_URL:
|
|
if (!this.value.spec.toURL) {
|
|
errors.push(this.getError('navLink.tabs.link.toURL.label'));
|
|
}
|
|
break;
|
|
|
|
case LINK_TYPE_SERVICE:
|
|
if (!this.value.spec.toService.name || !this.value.spec.toService.namespace) {
|
|
errors.push(this.getError(`navLink.tabs.link.toService.service.label`));
|
|
}
|
|
|
|
if (!this.value.spec.toService.scheme) {
|
|
errors.push(this.getError(`navLink.tabs.link.toService.scheme.label`));
|
|
}
|
|
break;
|
|
|
|
// no default
|
|
}
|
|
|
|
if (!this.value.metadata.name) {
|
|
errors.push(this.getError('navLink.name.label'));
|
|
}
|
|
|
|
if (errors.length) {
|
|
throw (errors.join('\n'));
|
|
}
|
|
},
|
|
},
|
|
created() {
|
|
this.setDefaultValues();
|
|
this.setUrlType();
|
|
this.setTargetOption();
|
|
this.setCurrentService();
|
|
// Validate error presence by allowing or resetting submission process
|
|
if (this.registerBeforeHook) {
|
|
this.registerBeforeHook(this.validate);
|
|
}
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<CruResource
|
|
:can-yaml="!isCreate"
|
|
:mode="mode"
|
|
:resource="value"
|
|
:errors="errors"
|
|
:cancel-event="true"
|
|
@error="e=>errors = e"
|
|
@finish="save"
|
|
@cancel="done()"
|
|
>
|
|
<NameNsDescription
|
|
v-model="value"
|
|
:namespaced="false"
|
|
:namespace-disabled="true"
|
|
:mode="mode"
|
|
name-key="metadata.name"
|
|
description-key="spec.label"
|
|
name-label="navLink.name.label"
|
|
name-placeholder="navLink.name.placeholder"
|
|
description-label="navLink.label.label"
|
|
description-placeholder="navLink.label.placeholder"
|
|
/>
|
|
|
|
<Tabbed
|
|
:side-tabs="true"
|
|
>
|
|
<Tab
|
|
name="link"
|
|
:label="t('navLink.tabs.link.label')"
|
|
>
|
|
<div class="row mb-20">
|
|
<div class="col span-6">
|
|
<RadioGroup
|
|
v-model="linkType"
|
|
name="type"
|
|
:mode="mode"
|
|
:options="urlTypeOptions"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<template v-if="isURL">
|
|
<div class="row mb-20">
|
|
<LabeledInput
|
|
v-model="value.spec.toURL"
|
|
:mode="mode"
|
|
:label="t('navLink.tabs.link.toURL.label')"
|
|
:required="isURL"
|
|
:placeholder="t('navLink.tabs.link.toURL.placeholder')"
|
|
/>
|
|
</div>
|
|
</template>
|
|
<template v-if="isService">
|
|
<div class="row mb-20">
|
|
<div class="col span-2">
|
|
<LabeledSelect
|
|
v-model="value.spec.toService.scheme"
|
|
:mode="mode"
|
|
:label="t('navLink.tabs.link.toService.scheme.label')"
|
|
:required="isService"
|
|
:options="protocolsOptions"
|
|
:placeholder="t('navLink.tabs.link.toService.scheme.placeholder')"
|
|
/>
|
|
</div>
|
|
<div class="col span-5">
|
|
<LabeledSelect
|
|
v-model="currentService"
|
|
:mode="mode"
|
|
:label="t('navLink.tabs.link.toService.service.label')"
|
|
:options="mappedServices"
|
|
:required="isService"
|
|
:placeholder="t('navLink.tabs.link.toService.service.placeholder')"
|
|
@input="setService"
|
|
/>
|
|
</div>
|
|
<div class="col span-2">
|
|
<LabeledInput
|
|
v-model="value.spec.toService.port"
|
|
:mode="mode"
|
|
:label="t('navLink.tabs.link.toService.port.label')"
|
|
type="number"
|
|
:placeholder="t('navLink.tabs.link.toService.port.placeholder')"
|
|
/>
|
|
</div>
|
|
<div class="col span-3">
|
|
<LabeledInput
|
|
v-model="value.spec.toService.path"
|
|
:mode="mode"
|
|
:label="t('navLink.tabs.link.toService.path.label')"
|
|
:placeholder="t('navLink.tabs.link.toService.path.placeholder')"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</Tab>
|
|
|
|
<Tab
|
|
name="target"
|
|
:label="t('navLink.tabs.target.label')"
|
|
>
|
|
<div class="row mb-20">
|
|
<div class="col span-6">
|
|
<RadioGroup
|
|
v-model="currentTarget"
|
|
name="type"
|
|
:mode="mode"
|
|
:options="targetOptions"
|
|
:required="true"
|
|
@input="setTargetValue($event)"
|
|
/>
|
|
</div>
|
|
<div class="col span-6">
|
|
<LabeledInput
|
|
v-if="isNamedWindow"
|
|
v-model="targetName"
|
|
:mode="mode"
|
|
:label="t('navLink.tabs.target.namedValue.label')"
|
|
@input="setTargetValue($event);"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</Tab>
|
|
|
|
<Tab
|
|
name="group"
|
|
:label="t('navLink.tabs.group.label')"
|
|
>
|
|
<div class="row mb-20">
|
|
<div class="col span-6">
|
|
<LabeledInput
|
|
v-model="value.spec.group"
|
|
:mode="mode"
|
|
:tooltip="t('navLink.tabs.group.group.tooltip')"
|
|
:label="t('navLink.tabs.group.group.label')"
|
|
/>
|
|
</div>
|
|
<div class="col span-6">
|
|
<LabeledInput
|
|
v-model="value.spec.sideLabel"
|
|
:mode="mode"
|
|
:label="t('navLink.tabs.group.sideLabel.label')"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row mb-20">
|
|
<div class="col span-6">
|
|
<LabeledInput
|
|
v-model="value.spec.description"
|
|
:mode="mode"
|
|
:label="t('navLink.tabs.group.description.label')"
|
|
/>
|
|
</div>
|
|
<div class="col span-2">
|
|
<FileImageSelector
|
|
v-model="value.spec.iconSrc"
|
|
:mode="mode"
|
|
:label="t('navLink.tabs.group.iconSrc.label')"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</Tab>
|
|
</Tabbed>
|
|
</CruResource>
|
|
</template>
|