dashboard/shell/edit/ui.cattle.io.navlink.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>