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

1090 lines
31 KiB
Vue

<script>
import isEqual from 'lodash/isEqual';
import jsyaml from 'js-yaml';
import merge from 'lodash/merge';
import { mapState } from 'vuex';
import AsyncButton from '@/components/AsyncButton';
import Banner from '@/components/Banner';
import Checkbox from '@/components/form/Checkbox';
import CruResourceFooter from '@/components/CruResourceFooter';
import LabeledSelect from '@/components/form/LabeledSelect';
import LazyImage from '@/components/LazyImage';
import Loading from '@/components/Loading';
import Markdown from '@/components/Markdown';
import NameNsDescription from '@/components/form/NameNsDescription';
import Questions from '@/components/Questions';
import Tab from '@/components/Tabbed/Tab';
import Tabbed from '@/components/Tabbed';
import UnitInput from '@/components/form/UnitInput';
import YamlEditor, { EDITOR_MODES } from '@/components/YamlEditor';
import { CATALOG, MANAGEMENT } from '@/config/types';
import {
REPO_TYPE, REPO, CHART, VERSION, NAMESPACE, NAME, DESCRIPTION as DESCRIPTION_QUERY, _CREATE, _EDIT,
} from '@/config/query-params';
import { CATALOG as CATALOG_ANNOTATIONS, DESCRIPTION as DESCRIPTION_ANNOTATION } from '@/config/labels-annotations';
import { exceptionToErrorsArray, stringify } from '@/utils/error';
import { clone, diff, get, set } from '@/utils/object';
import { findBy } from '@/utils/array';
import ChildHook, { BEFORE_SAVE_HOOKS, AFTER_SAVE_HOOKS } from '@/mixins/child-hook';
export default {
name: 'Install',
components: {
AsyncButton,
Banner,
Checkbox,
CruResourceFooter,
LabeledSelect,
LazyImage,
Loading,
Markdown,
NameNsDescription,
Questions,
Tab,
Tabbed,
UnitInput,
YamlEditor,
},
mixins: [ChildHook],
async fetch() {
this.warnings = [];
this.requires = [];
this.errors = [];
const query = this.$route.query;
await this.$store.dispatch('catalog/load');
this.defaultRegistrySetting = await this.$store.dispatch('management/find', {
type: MANAGEMENT.SETTING,
id: 'system-default-registry'
});
const repoType = query[REPO_TYPE];
const repoName = query[REPO];
const chartName = query[CHART];
let versionName = query[VERSION];
const appNamespace = query[NAMESPACE] || '';
const appName = query[NAME] || '';
if ( this.repo && chartName ) {
this.chart = this.$store.getters['catalog/chart']({
repoType,
repoName,
chartName,
includeHidden: true,
});
}
if ( appNamespace && appName ) {
// Explicitly asking for edit
try {
this.existing = await this.$store.dispatch('cluster/find', {
type: CATALOG.APP,
id: `${ appNamespace }/${ appName }`,
});
this.mode = _EDIT;
} catch (e) {
this.mode = _CREATE;
this.existing = null;
}
} else if ( this.chart?.targetNamespace && this.chart?.targetName ) {
// Asking to install a special chart with fixed namespace/name
// so edit it if there's an existing install
try {
this.existing = await this.$store.dispatch('cluster/find', {
type: CATALOG.APP,
id: `${ this.chart.targetNamespace }/${ this.chart.targetName }`,
});
this.mode = _EDIT;
} catch (e) {
this.mode = _CREATE;
this.existing = null;
}
} else {
// Regular create
this.mode = _CREATE;
}
this.value = await this.$store.dispatch('cluster/create', {
type: 'chartInstallAction',
metadata: {
namespace: this.existing ? this.existing.spec.namespace : appNamespace,
name: this.existing ? this.existing.spec.name : appName,
}
});
if ( this.existing ) {
this.forceNamespace = this.existing.metadata.namespace;
this.nameDisabled = true;
} else {
if ( this.chart?.targetNamespace ) {
this.forceNamespace = this.chart.targetNamespace;
} else if ( query[NAMESPACE] ) {
this.forceNamespace = query[NAMESPACE];
} else {
this.forceNamespace = null;
}
if ( this.chart?.targetName ) {
this.value.metadata.name = this.chart.targetName;
this.nameDisabled = true;
} else if ( query[NAME] ) {
this.value.metadata.name = query[name];
} else {
this.nameDisabled = false;
}
if ( query[DESCRIPTION_QUERY] ) {
this.value.setAnnotation(DESCRIPTION_ANNOTATION, query[DESCRIPTION_QUERY]);
}
}
if ( !this.chart ) {
return;
}
if ( !versionName && this.chart.versions?.length ) {
versionName = this.chart.versions[0].version;
}
if ( !versionName ) {
return;
}
this.version = this.$store.getters['catalog/version']({
repoType, repoName, chartName, versionName
});
try {
this.versionInfo = await this.$store.dispatch('catalog/getVersionInfo', {
repoType, repoName, chartName, versionName
});
} catch (e) {
console.error(e); // eslint-disable-line no-console
throw e;
}
if ( this.version && process.client ) {
await this.loadValuesComponent();
}
const required = (this.version.annotations?.[CATALOG_ANNOTATIONS.REQUIRES_GVK] || '').split(/\s*,\s*/).filter(x => !!x).reverse();
if ( required.length ) {
for ( const gvr of required ) {
if ( this.$store.getters['catalog/isInstalled']({ gvr }) ) {
continue;
}
const provider = this.$store.getters['catalog/versionProviding']({
gvr,
repoName: this.chart.repoName,
repoType: this.chart.repoType
});
const url = this.$router.resolve({
name: 'c-cluster-apps-install',
params: {
cluster: this.$route.params.cluster,
product: this.$store.getters['productId'],
},
query: {
[REPO_TYPE]: provider.repoType,
[REPO]: provider.repoName,
[CHART]: provider.name,
[VERSION]: provider.version,
}
}).href;
if ( provider ) {
this.requires.push(`<a href="${ url }">${ provider.name }</a> must be installed before you can install this chart.`);
} else {
this.warnings.push(`This chart requires another chart that provides ${ gvr }, but none was was found`);
}
}
}
// const updateValues = (this.existing && !this.chartValues) ||
// (!this.existing && (!this.loadedVersion || this.loadedVersion !== this.version.key) );
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 = jsyaml.safeDump(this.chartValues || {});
if ( this.valuesYaml === '{}\n' ) {
this.valuesYaml = '';
}
// For YAML diff
if ( !this.loadedVersion ) {
this.originalYamlValues = this.valuesYaml;
}
this.loadedVersionValues = this.versionInfo.values;
this.loadedVersion = this.version.key;
}
},
data() {
return {
defaultRegistrySetting: null,
chart: null,
chartValues: null,
originalYamlValues: null,
previousYamlValues: null,
errors: null,
existing: null,
forceNamespace: null,
loadedVersion: null,
loadedVersionValues: null,
mode: null,
value: null,
valuesComponent: null,
valuesYaml: '',
version: null,
versionInfo: null,
project: null,
requires: [],
warnings: [],
crds: true,
cleanupOnFail: false,
force: false,
hooks: true,
nameDisabled: false,
openApi: true,
resetValues: false,
defaultTab: 'appReadme',
showPreview: false,
showDiff: false,
showValuesComponent: true,
showQuestions: true,
componentHasTabs: false,
wait: true,
historyMax: 5,
timeout: 600,
};
},
computed: {
...mapState(['isMultiCluster']),
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.isMultiCluster && !this.existing && this.namespaceIsNew;
},
projectOpts() {
const cluster = this.$store.getters['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: null,
});
return out;
},
isValuesTab() {
const tabName = this.$refs.tabs?.activeTabName;
return tabName && !['appReadme', 'helm', 'readme'].includes(tabName);
},
charts() {
const currentKey = this.existing?.matchingChart(true)?.key;
return this.$store.getters['catalog/charts'].filter((x) => {
if ( x.key === currentKey ) {
return true;
}
return !x.deprecated && !x.hidden;
});
},
repo() {
const query = this.$route.query;
const repoType = query[REPO_TYPE];
const repoName = query[REPO];
return this.$store.getters['catalog/repo']({ repoType, repoName });
},
showReadme() {
return !!this.versionInfo?.readme;
},
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;
},
hasQuestions() {
return this.versionInfo && !!this.versionInfo.questions;
},
showingYaml() {
return this.showPreview || ( !this.valuesComponent && !this.hasQuestions );
},
},
watch: {
'$route.query'(neu, old) {
if ( !isEqual(neu, old) ) {
this.$fetch();
}
},
},
mounted() {
this.loadValuesComponent();
window.scrollTop = 0;
// For easy access debugging...
if ( typeof window !== 'undefined' ) {
window.v = this.value;
window.c = this;
}
},
methods: {
stringify,
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;
}
},
selectChart(chart, version) {
if ( !chart ) {
return;
}
this.$router.applyQuery({
[REPO]: chart.repoName,
[REPO_TYPE]: chart.repoType,
[CHART]: chart.chartName,
[VERSION]: version || chart.versions[0].version
});
},
selectVersion(version) {
this.$router.applyQuery({ [VERSION]: version });
},
preview() {
this.valuesYaml = jsyaml.safeDump(this.chartValues || {});
this.previousYamlValues = this.valuesYaml;
this.showPreview = true;
this.showValuesComponent = false;
this.showQuestions = false;
this.$nextTick(() => {
this.$refs.tabs.select('values-yaml');
});
},
unpreview() {
this.showPreview = false;
this.showValuesComponent = true;
this.showQuestions = true;
},
diff() {
this.showDiff = true;
},
undiff() {
this.showDiff = false;
},
cancel(reallyCancel) {
if (!reallyCancel && this.showPreview) {
return this.resetFromBack();
}
if ( this.existing ) {
this.done();
} else {
this.$router.replace({ name: 'c-cluster-apps' });
}
},
async resetFromBack() {
await this.unpreview();
await this.undiff();
this.valuesYaml = this.previousYamlValues;
},
done() {
this.$router.replace({
name: `c-cluster-product-resource`,
params: {
product: this.$store.getters['productId'],
cluster: this.$store.getters['clusterId'],
resource: CATALOG.APP,
}
});
},
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.$store.getters['currentCluster'];
const defaultRegistry = this.defaultRegistrySetting?.value || '';
setIfNotSet(cattle, 'clusterId', cluster.id);
setIfNotSet(cattle, 'clusterName', cluster.nameDisplay);
setIfNotSet(cattle, 'systemDefaultRegistry', defaultRegistry);
setIfNotSet(global, 'systemDefaultRegistry', defaultRegistry);
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 || '';
deleteIfEqual(values, 'systemDefaultRegistry', defaultRegistry);
if ( values.global?.cattle ) {
deleteIfEqual(values.global.cattle, 'clusterId', cluster.id);
deleteIfEqual(values.global.cattle, 'clusterName', cluster.nameDisplay);
deleteIfEqual(values.global.cattle, 'systemDefaultRegistry', defaultRegistry);
}
if ( values.global?.cattle && !Object.keys(values.global.cattle).length ) {
delete values.global.cattle;
}
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) };
}
},
actionInput(isUpgrade) {
const fromChart = this.versionInfo?.values || {};
if ( this.showingYaml ) {
this.applyYamlToValues();
}
// 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,
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.resetValues;
}
const errors = [];
const out = {
charts: [chart],
noHooks: this.hooks === false,
timeout: this.timeout > 0 ? `${ this.timeout }s` : null,
wait: this.wait === true,
namespace: form.metadata.namespace,
projectId: this.project,
};
if ( isUpgrade ) {
out.force = this.force === true;
out.historyMax = this.historyMax;
out.cleanupOnFail = this.cleanupOnFail;
} else {
out.disableOpenAPIValidation = this.openApi === false;
out.skipCRDs = this.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({ tab }) {
window.scrollTop = 0;
if ( tab.name === 'values-yaml' ) {
this.$nextTick(() => {
if ( this.$refs.yaml ) {
this.$refs.yaml.refresh();
this.$refs.yaml.focus();
}
});
}
},
getOptionLabel(opt) {
return opt?.chartDisplayName;
},
},
};
</script>
<template>
<Loading v-if="$fetchState.pending" />
<form v-else>
<h1 v-if="existing">
<t k="catalog.install.header.upgrade" :name="existing.nameDisplay" />
</h1>
<h1 v-else-if="chart">
<t k="catalog.install.header.install" :name="chart.chartDisplayName" />
</h1>
<h1 v-else>
<t k="catalog.install.header.installGeneric" />
</h1>
<div v-if="chart" class="chart-info mb-20">
<div class="logo-container">
<div class="logo-bg">
<LazyImage :src="chart.icon" class="logo" />
</div>
</div>
<div class="description">
<p>
{{ chart.description }}
</p>
</div>
</div>
<template v-if="requires.length || warnings.length">
<Banner v-for="msg in requires" :key="msg" color="warning">
<span v-html="msg" />
</Banner>
<Banner v-for="msg in warnings" :key="msg" color="error">
<span v-html="msg" />
</Banner>
<div class="mt-20 text-center">
<button type="button" class="btn role-primary" @click="cancel">
<t k="generic.cancel" />
</button>
</div>
</template>
<template v-else>
<div class="row mb-20">
<div class="col span-6">
<LabeledSelect
:label="t('catalog.install.chart')"
:value="chart"
:options="charts"
:get-option-label="opt => getOptionLabel(opt)"
option-key="key"
@input="selectChart($event)"
/>
</div>
<div v-if="chart" class="col span-6">
<LabeledSelect
:label="t('catalog.install.version')"
:value="$route.query.version"
option-label="version"
option-key="version"
:reduce="opt=>opt.version"
:options="chart.versions"
@input="selectVersion($event)"
/>
</div>
</div>
<div v-if="chart && value">
<NameNsDescription
v-model="value"
:mode="mode"
:name-disabled="nameDisabled"
:name-ns-hidden="!showNameEditor"
:force-namespace="forceNamespace"
:namespace-new-allowed="!existing && !forceNamespace"
:extra-columns="showProject ? ['project'] : []"
>
<template v-if="showProject" #project>
<LabeledSelect v-model="project" :label="t('catalog.install.project')" option-key="id" :options="projectOpts" />
</template>
</NameNsDescription>
<Tabbed
ref="tabs"
:side-tabs="true"
:class="{'with-name': showNameEditor}"
:default-tab="defaultTab"
@changed="tabChanged($event)"
>
<Tab name="appReadme" :label="t('catalog.install.section.appReadme')" :weight="100">
<Markdown v-if="versionInfo && versionInfo.appReadme" v-model="versionInfo.appReadme" class="md md-desc" />
<Markdown v-else :value="t('catalog.install.appReadmeGeneric')" class="md md-desc" />
</Tab>
<template v-if="valuesComponent && showValuesComponent">
<component
:is="valuesComponent"
v-if="componentHasTabs"
v-model="chartValues"
:mode="mode"
:chart="chart"
:existing="existing"
:version="version"
:version-info="versionInfo"
@warn="e=>warnings.push(e)"
@register-before-hook="registerBeforeHook"
@register-after-hook="registerAfterHook"
/>
<Tab
v-else
name="values-form"
:label="t('catalog.install.section.chartOptions')"
>
<component
:is="valuesComponent"
v-if="valuesComponent"
v-model="chartValues"
:mode="mode"
:chart="chart"
:existing="existing"
:version="version"
:version-info="versionInfo"
@warn="e=>warnings.push(e)"
@register-before-hook="registerBeforeHook"
@register-after-hook="registerAfterHook"
/>
<Tab
v-else
name="values-form"
:label="t('catalog.install.section.chartOptions')"
>
<component
:is="valuesComponent"
v-if="valuesComponent"
v-model="chartValues"
:mode="mode"
:chart="chart"
:existing="existing"
:version="version"
:version-info="versionInfo"
@warn="e=>warnings.push(e)"
@register-before-hook="registerBeforeHook"
@register-after-hook="registerAfterHook"
/>
</Tab>
</tab>
</template>
<Questions
v-else-if="hasQuestions && showQuestions"
v-model="chartValues"
:mode="mode"
:chart="chart"
:version="version"
:version-info="versionInfo"
:target-namespace="targetNamespace"
/>
<Tab v-else name="values-yaml" :label="t('catalog.install.section.valuesYaml')">
<YamlEditor
ref="yaml"
v-model="valuesYaml"
:scrolling="false"
:initial-yaml-values="originalYamlValues"
:editor-mode="editorMode"
/>
</Tab>
<Tab v-if="showReadme" name="readme" :label="t('catalog.install.section.readme')" :weight="-1">
<Markdown v-if="showReadme" ref="readme" v-model="versionInfo.readme" class="md readme" />
</Tab>
<Tab name="helm" :label="t('catalog.install.section.helm')" :weight="-2">
<div><Checkbox v-if="existing" v-model="cleanupOnFail" :label="t('catalog.install.helm.cleanupOnFail')" /></div>
<div><Checkbox v-if="!existing" v-model="crds" :label="t('catalog.install.helm.crds')" /></div>
<div><Checkbox v-model="hooks" :label="t('catalog.install.helm.hooks')" /></div>
<div><Checkbox v-if="existing" v-model="force" :label="t('catalog.install.helm.force')" /></div>
<div><Checkbox v-if="existing" v-model="resetValues" :label="t('catalog.install.helm.resetValues')" /></div>
<div><Checkbox v-if="!existing" v-model="openApi" :label="t('catalog.install.helm.openapi')" /></div>
<div><Checkbox v-model="wait" :label="t('catalog.install.helm.wait')" /></div>
<div style="display: block; max-width: 400px;" class="mt-10">
<UnitInput
v-model.number="timeout"
:label="t('catalog.install.helm.timeout.label')"
:suffix="t('catalog.install.helm.timeout.unit', {value: timeout})"
/>
</div>
<div style="display: block; max-width: 400px;" class="mt-10">
<UnitInput
v-if="existing"
v-model.number="historyMax"
:label="t('catalog.install.helm.historyMax.label')"
:suffix="t('catalog.install.helm.historyMax.unit', {value: historyMax})"
/>
</div>
</Tab>
</Tabbed>
<div v-for="(err, idx) in errors" :key="idx">
<Banner color="error" :label="stringify(err)" />
</div>
<CruResourceFooter
done-route="c-cluster-apps"
:mode="mode"
:finish-button-mode="(existing ? 'upgrade' : 'install')"
:is-form="true"
@cancel-confirmed="cancel"
>
<template #default="{checkCancel}">
<template v-if="(!!valuesComponent || hasQuestions) && !showValuesComponent && !showQuestions">
<button
v-if="showDiff"
type="button"
class="btn role-secondary"
@click="undiff"
>
<t k="resourceYaml.buttons.continue" />
</button>
<button
v-else
:disabled="valuesYaml === originalYamlValues"
type="button"
class="btn role-secondary"
@click="diff"
>
<t k="resourceYaml.buttons.diff" />
</button>
</template>
<button
v-if="(showValuesComponent || hasQuestions) && isValuesTab && !showPreview"
type="button"
class="btn role-secondary"
@click="preview"
>
{{ t("cruResource.previewYaml") }}
</button>
<div>
<button
v-if="showPreview && !showDiff"
type="button"
class="btn role-secondary"
@click="valuesYaml === originalYamlValues ? resetFromBack() : checkCancel(false)"
>
<t k="cruResource.backToForm" />
</button>
<AsyncButton
:mode="(existing ? 'upgrade' : 'install')"
@click="finish"
/>
</div>
</template>
</CruResourceFooter>
</div>
</template>
</form>
</template>
<style lang="scss" scoped>
$desc-height: 100px;
$padding: 5px;
.md {
overflow: auto;
max-width: 100%;
::v-deep {
* + H1,
* + H2,
* + H3,
* + H4,
* + H5,
* + H6 {
margin-top: 40px;
}
}
}
.md-desc > H1:first-child {
display: none;
}
.chart-info {
margin-top: 10px;
display: flex;
height: $desc-height;
align-items: center;
.logo-container {
height: $desc-height;
width: $desc-height;
text-align: center;
}
.logo-bg {
height: $desc-height;
width: $desc-height;
background-color: white;
border: $padding solid white;
border-radius: calc( 3 * var(--border-radius));
position: relative;
}
.logo {
max-height: $desc-height - 2 * $padding;
max-width: $desc-height - 2 * $padding;
position: absolute;
width: auto;
height: auto;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
}
.description {
flex-grow: 1;
padding-left: 20px;
// width: calc(100% - #{$sideways-tabs-width});
// height: $desc-height;
overflow: auto;
color: var(--secondary);
.name {
margin: #{-1 * $padding} 0 0 0;
}
}
}
</style>