dashboard/pages/c/_cluster/apps/charts/install.vue

1343 lines
40 KiB
Vue

<script>
import jsyaml from 'js-yaml';
import merge from 'lodash/merge';
import isEqual from 'lodash/isEqual';
import { mapPref, DIFF } from '@/store/prefs';
import Banner from '@/components/Banner';
import ButtonGroup from '@/components/ButtonGroup';
import ChartReadme from '@/components/ChartReadme';
import Checkbox from '@/components/form/Checkbox';
import LabeledSelect from '@/components/form/LabeledSelect';
import LazyImage from '@/components/LazyImage';
import Loading from '@/components/Loading';
import NameNsDescription from '@/components/form/NameNsDescription';
import ResourceCancelModal from '@/components/ResourceCancelModal';
import Questions from '@/components/Questions';
import Tabbed from '@/components/Tabbed';
import UnitInput from '@/components/form/UnitInput';
import YamlEditor, { EDITOR_MODES } from '@/components/YamlEditor';
import Wizard from '@/components/Wizard';
import ChartMixin from '@/mixins/chart';
import ChildHook, { BEFORE_SAVE_HOOKS, AFTER_SAVE_HOOKS } from '@/mixins/child-hook';
import { CATALOG, MANAGEMENT } from '@/config/types';
import {
CHART, FROM_CLUSTER, FROM_TOOLS, NAMESPACE, REPO, REPO_TYPE, VERSION, _FLAGGED
} from '@/config/query-params';
import { CATALOG as CATALOG_ANNOTATIONS, DESCRIPTION as DESCRIPTION_ANNOTATION, PROJECT } from '@/config/labels-annotations';
import { exceptionToErrorsArray } from '@/utils/error';
import { clone, diff, get, set } from '@/utils/object';
import { findBy, insertAt } from '@/utils/array';
import Vue from 'vue';
import { saferDump } from '@/utils/create-yaml';
import { DEFAULT_WORKSPACE } from '@/models/provisioning.cattle.io.cluster';
const VALUES_STATE = {
FORM: 'FORM',
YAML: 'YAML',
DIFF: 'DIFF'
};
export default {
name: 'Install',
components: {
Banner,
ButtonGroup,
ChartReadme,
Checkbox,
LabeledSelect,
LazyImage,
Loading,
NameNsDescription,
ResourceCancelModal,
Questions,
Tabbed,
UnitInput,
YamlEditor,
Wizard
},
mixins: [
ChildHook,
ChartMixin
],
async fetch() {
await this.fetchChart();
this.errors = [];
this.defaultRegistrySetting = await this.$store.dispatch('management/find', {
type: MANAGEMENT.SETTING,
id: 'system-default-registry'
});
this.serverUrlSetting = await this.$store.dispatch('management/find', {
type: MANAGEMENT.SETTING,
id: 'server-url'
});
if ( this.existing ) {
this.forceNamespace = this.existing.metadata.namespace;
this.nameDisabled = true;
} else if (this.$route.query[FROM_CLUSTER] === _FLAGGED) {
this.forceNamespace = DEFAULT_WORKSPACE;
} else if ( this.chart?.targetNamespace ) {
this.forceNamespace = this.chart.targetNamespace;
} else if ( this.query.appNamespace ) {
this.forceNamespace = this.query.appNamespace;
} else {
this.forceNamespace = null;
}
this.value = await this.$store.dispatch('cluster/create', {
type: 'chartInstallAction',
metadata: {
namespace: this.forceNamespace || this.$store.getters['defaultNamespace'],
name: this.existing?.spec?.name || this.query.appName || '',
}
});
if ( !this.existing) {
if ( this.chart?.targetName ) {
this.value.metadata.name = this.chart.targetName;
this.nameDisabled = true;
} else if ( this.query.appName ) {
this.value.metadata.name = this.query.appName;
} else {
this.nameDisabled = false;
}
if ( this.query.description ) {
this.value.setAnnotation(DESCRIPTION_ANNOTATION, this.query.description);
}
}
if (this.forceNamespace && !this.existing) {
let ns;
try {
ns = await this.$store.dispatch('cluster/find', { type: NAMESPACE, id: this.forceNamespace });
const project = ns.metadata.annotations[PROJECT];
if (project) {
this.project = project.replace(':', '/');
}
} catch {}
}
if ( !this.chart || !this.query.versionName) {
return;
}
if ( this.version && process.client ) {
await this.loadValuesComponent();
}
await this.loadChartSteps();
if ( !this.loadedVersion || this.loadedVersion !== this.version.key ) {
let userValues;
if ( this.loadedVersion ) {
// If changing charts once the page is loaded, diff from the chart you were
// previously on to get the actual customization, then apply onto the new chart values.
if ( this.showingYaml ) {
this.applyYamlToValues();
}
userValues = diff(this.loadedVersionValues, this.chartValues);
} else if ( this.existing ) {
// For an existing app, use the values from the previous install
userValues = clone(this.existing.spec?.values || {});
// For an existing app, use the values from the previous install
} else {
// For an new app, start empty
userValues = {};
}
this.removeGlobalValuesFrom(userValues);
this.chartValues = merge(merge({}, this.versionInfo?.values || {}), userValues);
this.valuesYaml = saferDump(this.chartValues);
// For YAML diff
if ( !this.loadedVersion ) {
this.originalYamlValues = this.valuesYaml;
}
this.loadedVersionValues = this.versionInfo?.values || {};
this.loadedVersion = this.version?.key;
}
this.updateStepOneReady();
this.preFormYamlOption = this.valuesComponent || this.hasQuestions ? VALUES_STATE.FORM : VALUES_STATE.YAML;
this.reademeWindowName = `${ this.stepperName }-${ this.version?.version }`;
},
data() {
const defaultCmdOpts = {
cleanupOnFail: false,
crds: true,
hooks: true,
force: false,
resetValues: false,
openApi: true,
wait: true,
timeout: 600,
historyMax: 5,
};
return {
defaultRegistrySetting: null,
serverUrlSetting: null,
chartValues: null,
originalYamlValues: null,
previousYamlValues: null,
errors: null,
existing: null,
forceNamespace: null,
loadedVersion: null,
loadedVersionValues: null,
mode: null,
value: null,
valuesComponent: null,
valuesYaml: '',
project: null,
reademeWindowName: null,
defaultCmdOpts,
customCmdOpts: { ...defaultCmdOpts },
nameDisabled: false,
preFormYamlOption: VALUES_STATE.YAML,
formYamlOption: VALUES_STATE.YAML,
showDiff: false,
showValuesComponent: true,
showQuestions: true,
showSlideIn: false,
componentHasTabs: false,
showCommandStep: false,
isNamespaceNew: false,
stepBasic: {
name: 'basics',
label: this.t('catalog.install.steps.basics.label'),
subtext: this.t('catalog.install.steps.basics.subtext'),
ready: true,
weight: 30
},
stepValues: {
name: 'helmValues',
label: this.t('catalog.install.steps.helmValues.label'),
subtext: this.t('catalog.install.steps.helmValues.subtext'),
ready: true,
weight: 20
},
stepCommands: {
name: 'helmCli',
label: this.t('catalog.install.steps.helmCli.label'),
subtext: this.t('catalog.install.steps.helmCli.subtext'),
ready: true,
weight: 10
},
customSteps: [
]
};
},
computed: {
namespaceIsNew() {
const all = this.$store.getters['cluster/all'](NAMESPACE);
const want = this.value?.metadata?.namespace;
if ( !want ) {
return false;
}
return !findBy(all, 'id', want);
},
showProject() {
return this.isRancher && !this.existing && this.forceNamespace;
},
projectOpts() {
const cluster = this.currentCluster;
const projects = this.$store.getters['management/all'](MANAGEMENT.PROJECT);
const out = projects.filter(x => x.spec.clusterName === cluster.id).map((project) => {
return {
id: project.id,
label: project.nameDisplay,
value: project.id
};
});
out.unshift({
id: 'none',
label: '(None)',
value: '',
});
return out;
},
charts() {
const current = this.existing?.matchingChart(true);
const out = this.$store.getters['catalog/charts'].filter((x) => {
if ( x.key === current?.key || x.chartName === current?.chartName ) {
return true;
}
if ( x.hidden && !this.showHidden ) {
return false;
}
if ( x.deprecated && !this.showDeprecated ) {
return false;
}
return true;
});
let last = '';
for ( let i = 0 ; i < out.length ; i++ ) {
if ( out[i].repoName !== last ) {
last = out[i].repoName;
insertAt(out, i, {
kind: 'label',
label: out[i].repoNameDisplay,
disabled: true
});
i++;
}
}
return out;
},
showNameEditor() {
return !this.nameDisabled || !this.forceNamespace;
},
showVersions() {
return this.chart?.versions.length > 1;
},
targetNamespace() {
if ( this.forceNamespace ) {
return this.forceNamespace;
} else if ( this.value?.metadata.namespace ) {
return this.value.metadata.namespace;
}
return 'default';
},
editorMode() {
if ( this.showDiff ) {
return EDITOR_MODES.DIFF_CODE;
}
return EDITOR_MODES.EDIT_CODE;
},
showingYaml() {
return this.formYamlOption === VALUES_STATE.YAML || ( !this.valuesComponent && !this.hasQuestions );
},
formYamlOptions() {
const options = [];
if (this.valuesComponent || this.hasQuestions) {
options.push({
labelKey: 'catalog.install.section.chartOptions',
value: VALUES_STATE.FORM,
});
}
options.push({
labelKey: 'catalog.install.section.valuesYaml',
value: VALUES_STATE.YAML,
}, {
labelKey: 'catalog.install.section.diff',
value: VALUES_STATE.DIFF,
// === quite obviously shouldn't work, but has been and still does. When the magic breaks address with heavier stringify/jsyaml.safeDump
disabled: this.formYamlOption === VALUES_STATE.FORM ? this.originalYamlValues === jsyaml.safeDump(this.chartValues || {}) : this.originalYamlValues === this.valuesYaml,
});
return options;
},
yamlDiffModeOptions() {
return [{
labelKey: 'resourceYaml.buttons.unified',
value: 'unified',
}, {
labelKey: 'resourceYaml.buttons.split',
value: 'split',
}];
},
stepperName() {
return this.existing?.nameDisplay || this.chart?.chartNameDisplay;
},
stepperSubtext() {
return this.existing && this.currentVersion !== this.targetVersion ? `${ this.currentVersion } > ${ this.targetVersion }` : this.targetVersion;
},
showingReadmeWindow() {
return !!this.$store.getters['wm/byId'](this.reademeWindowName);
},
diffMode: mapPref(DIFF),
step1Description() {
return this.$store.getters['i18n/withFallback']('catalog.install.steps.basics.description', { action: this.action, existing: !!this.existing }, '');
},
step2Description() {
return this.$store.getters['i18n/withFallback']('catalog.install.steps.helmValues.description', { action: this.action, existing: !!this.existing }, '');
},
step3Description() {
return this.$store.getters['i18n/withFallback']('catalog.install.steps.helmCli.description', { action: this.action, existing: !!this.existing }, '');
},
steps() {
let steps;
const type = this.version?.annotations?.[CATALOG_ANNOTATIONS.TYPE];
if ( type === CATALOG_ANNOTATIONS._CLUSTER_TPL ) {
steps = [this.stepValues];
} else {
steps = [
this.stepBasic,
this.stepValues,
...this.customSteps
];
}
if (this.showCommandStep) {
steps.push(this.stepCommands);
}
return steps.sort((a, b) => (b.weight || 0) - (a.weight || 0));
},
cmdOptions() {
return this.showCommandStep ? this.customCmdOpts : this.defaultCmdOpts;
},
namespaceNewAllowed() {
return !this.existing && !this.forceNamespace;
}
},
watch: {
'$route.query'(neu, old) {
if ( !isEqual(neu, old) ) {
this.$fetch();
}
},
'value.metadata.namespace'(neu, old) {
if (neu) {
const ns = this.$store.getters['cluster/byId'](NAMESPACE, this.value.metadata.namespace);
const project = ns?.metadata.annotations[PROJECT];
if (project) {
this.project = project.replace(':', '/');
}
}
},
preFormYamlOption(neu, old) {
if (neu === VALUES_STATE.FORM && this.valuesYaml !== this.previousYamlValues && !!this.$refs.cancelModal) {
this.$refs.cancelModal.show();
} else {
this.formYamlOption = neu;
}
},
formYamlOption(neu, old) {
switch (neu) {
case VALUES_STATE.FORM:
// Return to form, reset everything back to starting point
this.valuesYaml = this.previousYamlValues;
this.showValuesComponent = true;
this.showQuestions = true;
this.showDiff = false;
break;
case VALUES_STATE.YAML:
// Show the YAML preview
if (old === VALUES_STATE.FORM) {
this.valuesYaml = jsyaml.safeDump(this.chartValues || {});
this.previousYamlValues = this.valuesYaml;
}
this.showValuesComponent = false;
this.showQuestions = false;
this.showDiff = false;
break;
case VALUES_STATE.DIFF:
// Show the YAML diff
if (old === VALUES_STATE.FORM) {
this.valuesYaml = jsyaml.safeDump(this.chartValues || {});
this.previousYamlValues = this.valuesYaml;
}
this.showValuesComponent = false;
this.showQuestions = false;
this.showDiff = true;
break;
}
},
requires() {
this.updateStepOneReady();
},
warnings() {
this.updateStepOneReady();
},
},
async mounted() {
await this.loadValuesComponent();
await this.loadChartSteps();
window.scrollTop = 0;
// For easy access debugging...
if ( typeof window !== 'undefined' ) {
window.v = this.value;
window.c = this;
}
this.preFormYamlOption = this.valuesComponent || this.hasQuestions ? VALUES_STATE.FORM : VALUES_STATE.YAML;
},
beforeDestroy() {
this.$store.dispatch('wm/close', this.reademeWindowName, { root: true });
},
methods: {
async loadValuesComponent() {
// TODO: Remove RELEASE_NAME. This is only in until the component annotation is added to the OPA Gatekeeper chart
const component = this.version?.annotations?.[CATALOG_ANNOTATIONS.COMPONENT] || this.version?.annotations?.[CATALOG_ANNOTATIONS.RELEASE_NAME];
if ( component ) {
if ( this.$store.getters['catalog/haveComponent'](component) ) {
this.valuesComponent = this.$store.getters['catalog/importComponent'](component);
const loaded = await this.valuesComponent();
this.showValuesComponent = true;
this.componentHasTabs = loaded?.default?.hasTabs || false;
} else {
this.valuesComponent = null;
this.componentHasTabs = false;
this.showValuesComponent = false;
}
} else {
this.valuesComponent = null;
this.componentHasTabs = false;
this.showValuesComponent = false;
}
},
async loadChartSteps() {
const component = this.version?.annotations?.[CATALOG_ANNOTATIONS.COMPONENT] || this.version?.annotations?.[CATALOG_ANNOTATIONS.RELEASE_NAME];
if ( component ) {
const steps = await this.$store.getters['catalog/chartSteps'](component);
this.customSteps = await Promise.all( steps.map(cs => this.loadChartStep(cs)));
}
},
async loadChartStep(customStep) {
const loaded = await customStep.component();
const withFallBack = this.$store.getters['i18n/withFallback'];
return {
name: customStep.name,
label: withFallBack(loaded?.default?.label, null, customStep.name),
subtext: withFallBack(loaded?.default?.subtext, null, ''),
weight: loaded?.default?.weight,
ready: false,
hidden: true,
loading: true,
component: customStep.component,
};
},
selectChart(chart) {
if ( !chart ) {
return;
}
this.$router.applyQuery({
[REPO]: chart.repoName,
[REPO_TYPE]: chart.repoType,
[CHART]: chart.chartName,
[VERSION]: chart.versions[0].version
});
},
cancel() {
if ( this.existing ) {
this.done();
} else if (this.$route.query[FROM_TOOLS] === _FLAGGED) {
this.$router.replace(this.clusterToolsLocation());
} else if (this.$route.query[FROM_CLUSTER] === _FLAGGED) {
this.$router.replace(this.clustersLocation());
} else {
this.$router.replace(this.chartLocation(false));
}
},
done() {
if ( this.$route.query[FROM_TOOLS] === _FLAGGED ) {
this.$router.replace(this.clusterToolsLocation());
} else if (this.$route.query[FROM_CLUSTER] === _FLAGGED) {
this.$router.replace(this.clustersLocation());
} else {
// If the create app process fails helm validation then we still get here... so until this is fixed new apps will be taken to the
// generic apps list (existing apps will be taken to their detail page)
this.$router.replace(this.appLocation());
}
},
async finish(btnCb) {
try {
const isUpgrade = !!this.existing;
this.errors = [];
await this.applyHooks(BEFORE_SAVE_HOOKS);
const { errors, input } = this.actionInput(isUpgrade);
if ( errors?.length ) {
this.errors = errors;
btnCb(false);
return;
}
const res = await this.repo.doAction((isUpgrade ? 'upgrade' : 'install'), input);
this.operation = await this.$store.dispatch('cluster/find', {
type: CATALOG.OPERATION,
id: `${ res.operationNamespace }/${ res.operationName }`
});
try {
await this.operation.waitForLink('logs');
this.operation.openLogs();
} catch (e) {
// The wait times out eventually, move on...
}
await this.applyHooks(AFTER_SAVE_HOOKS);
btnCb(true);
this.done();
} catch (err) {
this.errors = exceptionToErrorsArray(err);
btnCb(false);
}
},
addGlobalValuesTo(values) {
let global = values.global;
if ( !global ) {
global = {};
set(values, 'global', global);
}
let cattle = global.cattle;
if ( !cattle ) {
cattle = {};
set(values.global, 'cattle', cattle);
}
const cluster = this.currentCluster;
const defaultRegistry = this.defaultRegistrySetting?.value || '';
const serverUrl = this.serverUrlSetting?.value || '';
const isWindows = cluster.providerOs === 'windows';
const pathPrefix = cluster.spec?.rancherKubernetesEngineConfig?.prefixPath || '';
const windowsPathPrefix = cluster.spec?.rancherKubernetesEngineConfig?.winPrefixPath || '';
setIfNotSet(cattle, 'clusterId', cluster.id);
setIfNotSet(cattle, 'clusterName', cluster.nameDisplay);
setIfNotSet(cattle, 'systemDefaultRegistry', defaultRegistry);
setIfNotSet(global, 'systemDefaultRegistry', defaultRegistry);
setIfNotSet(cattle, 'url', serverUrl);
setIfNotSet(cattle, 'rkePathPrefix', pathPrefix);
setIfNotSet(cattle, 'rkeWindowsPathPrefix', windowsPathPrefix);
if ( isWindows ) {
setIfNotSet(cattle, 'windows.enabled', true);
}
return values;
function setIfNotSet(obj, key, val) {
if ( typeof get(obj, key) === 'undefined' ) {
set(obj, key, val);
}
}
},
removeGlobalValuesFrom(values) {
if ( !values ) {
return;
}
const cluster = this.$store.getters['currentCluster'];
const defaultRegistry = this.defaultRegistrySetting?.value || '';
const serverUrl = this.serverUrlSetting?.value || '';
const isWindows = cluster.providerOs === 'windows';
const pathPrefix = cluster.spec?.rancherKubernetesEngineConfig?.prefixPath || '';
const windowsPathPrefix = cluster.spec?.rancherKubernetesEngineConfig?.winPrefixPath || '';
if ( values.global?.cattle ) {
deleteIfEqual(values.global.cattle, 'clusterId', cluster.id);
deleteIfEqual(values.global.cattle, 'clusterName', cluster.nameDisplay);
deleteIfEqual(values.global.cattle, 'systemDefaultRegistry', defaultRegistry);
deleteIfEqual(values.global.cattle, 'url', serverUrl);
deleteIfEqual(values.global.cattle, 'rkePathPrefix', pathPrefix);
deleteIfEqual(values.global.cattle, 'rkeWindowsPathPrefix', windowsPathPrefix);
if ( isWindows ) {
deleteIfEqual(values.global.cattle.windows, 'enabled', true);
}
}
if ( values.global?.cattle?.windows && !Object.keys(values.global.cattle.windows).length ) {
delete values.global.cattle.windows;
}
if ( values.global?.cattle && !Object.keys(values.global.cattle).length ) {
delete values.global.cattle;
}
if ( values.global ) {
deleteIfEqual(values.global, 'systemDefaultRegistry', defaultRegistry);
}
if ( !Object.keys(values.global || {}).length ) {
delete values.global;
}
return values;
function deleteIfEqual(obj, key, val) {
if ( get(obj, key) === val ) {
delete obj[key];
}
}
},
applyYamlToValues() {
try {
this.chartValues = jsyaml.safeLoad(this.valuesYaml);
} catch (err) {
return { errors: exceptionToErrorsArray(err) };
}
return { errors: [] };
},
actionInput(isUpgrade) {
const fromChart = this.versionInfo?.values || {};
const errors = [];
if ( this.showingYaml ) {
const { errors: yamlErrors } = this.applyYamlToValues();
errors.push(...yamlErrors);
}
// Only save the values that differ from the chart's standard values.yaml
const values = diff(fromChart, this.chartValues);
// Add our special blend of 11 herbs and global values
this.addGlobalValuesTo(values);
const form = JSON.parse(JSON.stringify(this.value));
const chart = {
chartName: this.chart.chartName,
version: this.version?.version || this.query.versionName,
releaseName: form.metadata.name,
description: form.metadata?.annotations?.[DESCRIPTION_ANNOTATION],
annotations: {
[CATALOG_ANNOTATIONS.SOURCE_REPO_TYPE]: this.chart.repoType,
[CATALOG_ANNOTATIONS.SOURCE_REPO_NAME]: this.chart.repoName
},
values,
};
if ( isUpgrade ) {
chart.resetValues = this.cmdOptions.resetValues;
}
const out = {
charts: [chart],
noHooks: this.cmdOptions.hooks === false,
timeout: this.cmdOptions.timeout > 0 ? `${ this.cmdOptions.timeout }s` : null,
wait: this.cmdOptions.wait === true,
namespace: form.metadata.namespace,
projectId: this.project,
};
if ( isUpgrade ) {
out.force = this.cmdOptions.force === true;
out.historyMax = this.cmdOptions.historyMax;
out.cleanupOnFail = this.cmdOptions.cleanupOnFail;
} else {
out.disableOpenAPIValidation = this.cmdOptions.openApi === false;
out.skipCRDs = this.cmdOptions.crds === false;
}
const more = [];
let auto = (this.version.annotations?.[CATALOG_ANNOTATIONS.AUTO_INSTALL] || '').split(/\s*,\s*/).filter(x => !!x).reverse();
for ( const constraint of auto ) {
const provider = this.$store.getters['catalog/versionSatisfying']({
constraint,
repoName: this.chart.repoName,
repoType: this.chart.repoType,
chartVersion: this.version.version,
});
if ( provider ) {
more.push(provider);
} else {
errors.push(`This chart requires ${ constraint } but no matching chart was found`);
}
}
auto = (this.version.annotations?.[CATALOG_ANNOTATIONS.AUTO_INSTALL_GVK] || '').split(/\s*,\s*/).filter(x => !!x).reverse();
for ( const gvr of auto ) {
const provider = this.$store.getters['catalog/versionProviding']({
gvr,
repoName: this.chart.repoName,
repoType: this.chart.repoType
});
if ( provider ) {
more.push(provider);
} else {
errors.push(`This chart requires another chart that provides ${ gvr }, but none was was found`);
}
}
for ( const dependency of more ) {
out.charts.unshift({
chartName: dependency.name,
version: dependency.version,
releaseName: dependency.annotations[CATALOG_ANNOTATIONS.RELEASE_NAME] || dependency.name,
projectId: this.project,
values: this.addGlobalValuesTo({}),
annotations: {
[CATALOG_ANNOTATIONS.SOURCE_REPO_TYPE]: dependency.repoType,
[CATALOG_ANNOTATIONS.SOURCE_REPO_NAME]: dependency.repoName
},
});
}
return { errors, input: out };
},
tabChanged() {
window.scrollTop = 0;
},
updateStepOneReady() {
const okRequires = !this.requires.length;
const okChart = !!this.chart;
this.steps[0].ready = okRequires && okChart;
},
getOptionLabel(opt) {
return opt?.chartNameDisplay;
},
showReadmeWindow() {
this.$store.dispatch('wm/open', {
id: this.reademeWindowName,
label: this.reademeWindowName,
icon: 'file',
component: 'ChartReadme',
attrs: { versionInfo: this.versionInfo }
}, { root: true });
},
updateStep(stepName, update) {
const step = this.steps.find(step => step.name === stepName);
if (step) {
for (const prop in update) {
Vue.set(step, prop, update[prop]);
}
}
}
},
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<div v-else class="install-steps">
<Wizard
v-if="value"
:steps="steps"
:errors="errors"
:edit-first-step="true"
:banner-title="stepperName"
:banner-title-subtext="stepperSubtext"
:finish-mode="action"
class="wizard"
@cancel="cancel"
@finish="finish"
>
<template v-for="customStep of customSteps" v-slot:[customStep.name]>
<component
:is="customStep.component"
:key="customStep.name"
@update="updateStep(customStep.name, $event)"
@errors="e=>errors.push(...e)"
/>
</template>
<template #bannerTitleImage>
<div class="logo-bg">
<LazyImage :src="chart ? chart.icon : ''" class="logo" />
</div>
</template>
<template #basics>
<div class="step__basic">
<Banner v-if="step1Description" color="info" class="description">
<span>{{ step1Description }}</span>
<span v-if="namespaceNewAllowed" class="mt-10">
{{ t('catalog.install.steps.basics.nsCreationDescription', {}, true) }}
</span>
</Banner>
<div v-if="requires.length || warnings.length" class="mb-30">
<Banner v-for="msg in requires" :key="msg" color="error">
<span v-html="msg" />
</Banner>
<Banner v-for="msg in warnings" :key="msg" color="warning">
<span v-html="msg" />
</Banner>
</div>
<div v-if="existing" class="row mb-10">
<div class="col span-4">
<!-- We have a chart, select a new version -->
<LabeledSelect
v-if="chart"
:label="t('catalog.install.version')"
:value="query.versionName"
:options="filteredVersions"
:selectable="version => !version.disabled"
@input="selectVersion"
/>
<!-- There is no chart, let the user try to select one -->
<LabeledSelect
v-else
:label="t('catalog.install.chart')"
:value="chart"
:options="charts"
:selectable="option => !option.disabled"
:get-option-label="opt => getOptionLabel(opt)"
option-key="key"
@input="selectChart($event)"
>
<template v-slot:option="opt">
<template v-if="opt.kind === 'divider'">
<hr />
</template>
<template v-else-if="opt.kind === 'label'">
<b style="position: relative; left: -2.5px;">{{ opt.label }}</b>
</template>
</template>
</LabeledSelect>
</div>
</div>
<NameNsDescription
v-if="chart"
v-model="value"
:mode="mode"
:name-disabled="nameDisabled"
:name-required="false"
:name-ns-hidden="!showNameEditor"
:force-namespace="forceNamespace"
:namespace-new-allowed="namespaceNewAllowed"
:extra-columns="showProject ? ['project'] : []"
:show-spacer="false"
:horizontal="false"
@isNamespaceNew="isNamespaceNew = $event"
>
<template v-if="showProject" #project>
<LabeledSelect
v-model="project"
:disabled="!namespaceIsNew"
:label="t('catalog.install.project')"
option-key="id"
:options="projectOpts"
:tooltip="!namespaceIsNew ? t('catalog.install.namespaceIsInProject', {namespace: value.metadata.namespace}, true) : ''"
:hover-tooltip="!namespaceIsNew"
:status="'info'"
/>
</template>
</NameNsDescription>
<div class="step__values__controls--spacer" style="flex:1">
&nbsp;
</div>
<Banner v-if="isNamespaceNew" color="info" v-html="t('catalog.install.steps.basics.createNamespace', {namespace: value.metadata.namespace}, true) ">
</Banner>
<Checkbox v-model="showCommandStep" class="mb-20" :label="t('catalog.install.steps.helmCli.checkbox', { action, existing: !!existing })" />
</div>
</template>
<template #helmValues>
<Banner v-if="step2Description" color="info" class="description">
{{ step2Description }}
</Banner>
<div class="step__values__controls">
<ButtonGroup
v-model="preFormYamlOption"
:options="formYamlOptions"
inactive-class="bg-disabled btn-sm"
active-class="bg-primary btn-sm"
:disabled="preFormYamlOption != formYamlOption"
></ButtonGroup>
<div class="step__values__controls--spacer">
&nbsp;
</div>
<ButtonGroup
v-if="showDiff"
v-model="diffMode"
:options="yamlDiffModeOptions"
inactive-class="bg-disabled btn-sm"
active-class="bg-primary btn-sm"
></ButtonGroup>
<div v-if="hasReadme && !showingReadmeWindow" class="btn-group">
<button type="button" class="btn bg-primary btn-sm" @click="showSlideIn = !showSlideIn">
{{ t('catalog.install.steps.helmValues.chartInfo.button') }}
</button>
</div>
</div>
<div class="scroll__container">
<div class="scroll__content">
<!-- Values (as Custom Component) -->
<template v-if="valuesComponent && showValuesComponent">
<Tabbed
v-if="componentHasTabs"
ref="tabs"
:side-tabs="true"
:class="{'with-name': showNameEditor}"
class="step__values__content"
@changed="tabChanged($event)"
>
<component
:is="valuesComponent"
v-model="chartValues"
:mode="mode"
:chart="chart"
class="step__values__content"
:existing="existing"
:version="version"
:version-info="versionInfo"
@warn="e=>errors.push(e)"
@register-before-hook="registerBeforeHook"
@register-after-hook="registerAfterHook"
/>
</Tabbed>
<template v-else>
<component
:is="valuesComponent"
v-if="valuesComponent"
v-model="chartValues"
:mode="mode"
:chart="chart"
class="step__values__content"
:existing="existing"
:version="version"
:version-info="versionInfo"
@warn="e=>errors.push(e)"
@register-before-hook="registerBeforeHook"
@register-after-hook="registerAfterHook"
/>
</template>
</template>
<!-- Values (as Questions) -->
<Tabbed
v-else-if="hasQuestions && showQuestions"
ref="tabs"
:side-tabs="true"
:class="{'with-name': showNameEditor}"
class="step__values__content"
@changed="tabChanged($event)"
>
<Questions
v-model="chartValues"
:mode="mode"
:source="versionInfo"
:target-namespace="targetNamespace"
/>
</Tabbed>
<!-- Values (as YAML) -->
<template v-else>
<YamlEditor
ref="yaml"
v-model="valuesYaml"
class="step__values__content"
:scrolling="true"
:initial-yaml-values="originalYamlValues"
:editor-mode="editorMode"
:hide-preview-buttons="true"
/>
</template>
</div>
</div>
<!-- Confirm loss of changes on toggle from yaml/preview to form -->
<ResourceCancelModal ref="cancelModal" :is-cancel-modal="false" :is-form="true" @cancel-cancel="preFormYamlOption=formYamlOption" @confirm-cancel="formYamlOption = preFormYamlOption;"></ResourceCancelModal>
</template>
<template #helmCli>
<Banner v-if="step3Description" color="info" class="description">
{{ step3Description }}
</Banner>
<div><Checkbox v-if="existing" v-model="customCmdOpts.cleanupOnFail" :label="t('catalog.install.helm.cleanupOnFail')" /></div>
<div><Checkbox v-if="!existing" v-model="customCmdOpts.crds" :label="t('catalog.install.helm.crds')" /></div>
<div><Checkbox v-model="customCmdOpts.hooks" :label="t('catalog.install.helm.hooks')" /></div>
<div><Checkbox v-if="existing" v-model="customCmdOpts.force" :label="t('catalog.install.helm.force')" /></div>
<div><Checkbox v-if="existing" v-model="customCmdOpts.resetValues" :label="t('catalog.install.helm.resetValues')" /></div>
<div><Checkbox v-if="!existing" v-model="customCmdOpts.openApi" :label="t('catalog.install.helm.openapi')" /></div>
<div><Checkbox v-model="customCmdOpts.wait" :label="t('catalog.install.helm.wait')" /></div>
<div style="display: block; max-width: 400px;" class="mt-10">
<UnitInput
v-model.number="customCmdOpts.timeout"
:label="t('catalog.install.helm.timeout.label')"
:suffix="t('catalog.install.helm.timeout.unit', {value: customCmdOpts.timeout})"
/>
</div>
<div style="display: block; max-width: 400px;" class="mt-10">
<UnitInput
v-if="existing"
v-model.number="customCmdOpts.historyMax"
:label="t('catalog.install.helm.historyMax.label')"
:suffix="t('catalog.install.helm.historyMax.unit', {value: customCmdOpts.historyMax})"
/>
</div>
</template>
</Wizard>
<div class="slideIn" :class="{'hide': false, 'slideIn__show': showSlideIn}">
<h2 class="slideIn__header">
{{ t('catalog.install.steps.helmValues.chartInfo.label') }}
<div class="slideIn__header__buttons">
<div v-tooltip="t('catalog.install.slideIn.dock')" class="slideIn__header__button" @click="showSlideIn = false; showReadmeWindow()">
<i class="icon icon-dock" />
</div>
<div class="slideIn__header__button" @click="showSlideIn = false">
<i class="icon icon-close" />
</div>
</div>
</h2>
<ChartReadme v-if="hasReadme" :version-info="versionInfo" class="chart-content__tabs" />
</div>
</div>
</template>
<style lang="scss" scoped>
$title-height: 50px;
$padding: 5px;
$slideout-width: 35%;
.install-steps {
position: relative;
overflow: hidden;
.description {
display: flex;
flex-direction: column;
margin-top: 0;
}
}
.wizard {
.logo-bg {
height: $title-height;
width: $title-height;
background-color: white;
border: $padding solid white;
border-radius: calc( 3 * var(--border-radius));
position: relative;
}
.logo {
max-height: $title-height - 2 * $padding;
max-width: $title-height - 2 * $padding;
position: absolute;
width: auto;
height: auto;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
}
}
.step {
&__basic {
display: flex;
flex-direction: column;
flex: 1;
.spacer {
line-height: 2;
}
}
&__values {
&__controls {
display: flex;
margin-bottom: 15px;
& > *:not(:last-of-type) {
margin-right: $padding * 2;
}
&--spacer {
flex: 1
}
}
&__content {
flex: 1;
::v-deep .tab-container {
overflow: auto;
}
}
}
}
.slideIn {
$slideout-width: 35%;
border-left: var(--header-border-size) solid var(--header-border);
position: absolute;
top: 0;
right: -$slideout-width;
height: 100%;
background-color: var(--topmenu-bg);
width: $slideout-width;
z-index: 10;
display: flex;
flex-direction: column;
padding: 10px;
transition: right .5s ease;
&__header {
display: flex;
align-items: center;
justify-content: space-between;
&__buttons {
display: flex;
}
&__button {
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 2px;
> i {
font-size: 20px;
opacity: 0.5;
}
&:hover {
background-color: var(--wm-closer-hover-bg);
}
}
}
.chart-content__tabs {
display: flex;
flex-direction: column;
flex: 1;
height: 0;
padding-bottom: 10px;
::v-deep .chart-readmes {
flex: 1;
overflow: auto;
}
}
&__show {
right: 0;
}
}
.scroll {
&__container {
$yaml-height: 200px;
display: flex;
flex: 1;
min-height: $yaml-height;
height: 0;
}
&__content {
display: flex;
flex: 1;
overflow: auto;
}
}
::v-deep .yaml-editor {
flex: 1
}
</style>