mirror of https://github.com/rancher/dashboard.git
425 lines
9.1 KiB
Vue
425 lines
9.1 KiB
Vue
<script>
|
|
import jsyaml from 'js-yaml';
|
|
import YamlEditor, { EDITOR_MODES } from '@shell/components/YamlEditor';
|
|
import FileSelector from '@shell/components/form/FileSelector';
|
|
import Footer from '@shell/components/form/Footer';
|
|
import { ANNOTATIONS_TO_FOLD } from '@shell/config/labels-annotations';
|
|
import { ensureRegex } from '@shell/utils/string';
|
|
import { typeOf } from '@shell/utils/sort';
|
|
|
|
import {
|
|
_CREATE,
|
|
_VIEW,
|
|
PREVIEW,
|
|
_FLAGGED,
|
|
_UNFLAG,
|
|
_EDIT,
|
|
} from '@shell/config/query-params';
|
|
import { BEFORE_SAVE_HOOKS, AFTER_SAVE_HOOKS } from '@shell/mixins/child-hook';
|
|
import { exceptionToErrorsArray } from '@shell/utils/error';
|
|
|
|
export default {
|
|
components: {
|
|
Footer,
|
|
FileSelector,
|
|
YamlEditor
|
|
},
|
|
|
|
props: {
|
|
mode: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
|
|
value: {
|
|
type: Object,
|
|
required: true,
|
|
},
|
|
|
|
initialYamlForDiff: {
|
|
type: String,
|
|
default: null,
|
|
},
|
|
|
|
yaml: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
|
|
doneRoute: {
|
|
type: [String, Object],
|
|
default: null,
|
|
},
|
|
|
|
offerPreview: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
|
|
parentParams: {
|
|
type: Object,
|
|
default: null,
|
|
},
|
|
|
|
doneOverride: {
|
|
type: [Function, Object],
|
|
default: null
|
|
},
|
|
|
|
showFooter: {
|
|
type: Boolean,
|
|
default: true
|
|
},
|
|
|
|
applyHooks: {
|
|
type: Function,
|
|
default: null,
|
|
}
|
|
},
|
|
|
|
data() {
|
|
// Initial load with a preview showing no diff isn't very useful
|
|
this.$router.applyQuery({ [PREVIEW]: _UNFLAG });
|
|
|
|
return {
|
|
initialYaml: this.initialYamlForDiff || this.yaml,
|
|
currentYaml: this.yaml,
|
|
showPreview: false,
|
|
errors: null,
|
|
cm: null,
|
|
initialReady: true
|
|
};
|
|
},
|
|
|
|
computed: {
|
|
schema() {
|
|
const inStore = this.$store.getters['currentStore'](this.value.type);
|
|
|
|
return this.$store.getters[`${ inStore }/schemaFor`]( this.value.type );
|
|
},
|
|
|
|
isCreate() {
|
|
return this.mode === _CREATE;
|
|
},
|
|
|
|
isView() {
|
|
return this.mode === _VIEW;
|
|
},
|
|
|
|
isEdit() {
|
|
return this.mode === _EDIT;
|
|
},
|
|
|
|
editorMode() {
|
|
// Include the mode in the route as a dependency
|
|
// of this computed property so that the editor
|
|
// toggles when you navigate back and forth between
|
|
// edit and view.
|
|
if ( this.$route.query.mode === _VIEW || (this.isView && (this.$route.query.mode !== _EDIT || this.$route.query.mode !== _VIEW))) {
|
|
return EDITOR_MODES.VIEW_CODE;
|
|
} else if ( this.showPreview ) {
|
|
return EDITOR_MODES.DIFF_CODE;
|
|
}
|
|
|
|
return EDITOR_MODES.EDIT_CODE;
|
|
},
|
|
|
|
canDiff() {
|
|
return this.initialYaml !== this.currentYaml;
|
|
},
|
|
},
|
|
|
|
watch: {
|
|
yaml(neu) {
|
|
if ( this.mode === _VIEW ) {
|
|
this.currentYaml = neu;
|
|
}
|
|
},
|
|
|
|
mode(neu, old) {
|
|
// if this component is changing from viewing a resource to 'creating' that resource, it must actually be cloning
|
|
// clean yaml accordingly
|
|
if (neu === _CREATE && old === _VIEW) {
|
|
this.currentYaml = this.value.cleanYaml(this.yaml, neu);
|
|
}
|
|
}
|
|
},
|
|
|
|
methods: {
|
|
onInput(yaml) {
|
|
this.currentYaml = yaml;
|
|
this.onReady(this.cm);
|
|
},
|
|
|
|
onReady(cm) {
|
|
if (!this.initialReady) {
|
|
return;
|
|
}
|
|
this.initialReady = false;
|
|
|
|
this.cm = cm;
|
|
|
|
if ( this.isEdit ) {
|
|
cm.foldLinesMatching(/^status:\s*$/);
|
|
}
|
|
|
|
try {
|
|
const parsed = jsyaml.load(this.currentYaml);
|
|
const annotations = Object.keys(parsed?.metadata?.annotations || {});
|
|
const regexes = ANNOTATIONS_TO_FOLD.map(x => ensureRegex(x));
|
|
|
|
let foldAnnotations = false;
|
|
|
|
for ( const k of annotations ) {
|
|
if ( foldAnnotations ) {
|
|
break;
|
|
}
|
|
|
|
for ( const regex of regexes ) {
|
|
if ( k.match(regex) ) {
|
|
foldAnnotations = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( foldAnnotations ) {
|
|
cm.foldLinesMatching(/^\s+annotations:\s*$/);
|
|
}
|
|
} catch (e) {}
|
|
|
|
cm.foldLinesMatching(/managedFields/);
|
|
|
|
// regardless of edit or create we should probably fold all the comments so they dont get out of hand.
|
|
const saved = cm.getMode().fold;
|
|
|
|
cm.getMode().fold = 'yamlcomments';
|
|
cm.execCommand('foldAll');
|
|
cm.getMode().fold = saved;
|
|
},
|
|
|
|
onChanges(cm, changes) {
|
|
if ( changes.length !== 1 ) {
|
|
return;
|
|
}
|
|
|
|
const change = changes[0];
|
|
|
|
if ( change.from.line !== change.to.line ) {
|
|
return;
|
|
}
|
|
|
|
let line = change.from.line;
|
|
let str = cm.getLine(line);
|
|
let maxIndent = indentChars(str);
|
|
|
|
if ( maxIndent === null ) {
|
|
return;
|
|
}
|
|
|
|
cm.replaceRange('', { line, ch: 0 }, { line, ch: 1 }, '+input');
|
|
|
|
while ( line > 0 ) {
|
|
line--;
|
|
str = cm.getLine(line);
|
|
const indent = indentChars(str);
|
|
|
|
if ( indent === null ) {
|
|
break;
|
|
}
|
|
|
|
if ( indent < maxIndent ) {
|
|
cm.replaceRange('', { line, ch: 0 }, { line, ch: 1 }, '+input');
|
|
|
|
if ( indent === 0 ) {
|
|
break;
|
|
}
|
|
|
|
maxIndent = indent;
|
|
}
|
|
}
|
|
|
|
function indentChars(str) {
|
|
const match = str.match(/^#(\s+)/);
|
|
|
|
if ( match ) {
|
|
return match[1].length;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
},
|
|
|
|
updateValue(value) {
|
|
this.$refs.yamleditor.updateValue(value);
|
|
},
|
|
|
|
preview() {
|
|
this.updateValue(this.currentYaml);
|
|
this.showPreview = true;
|
|
this.$router.applyQuery({ [PREVIEW]: _FLAGGED });
|
|
},
|
|
|
|
unpreview() {
|
|
this.showPreview = false;
|
|
this.$router.applyQuery({ [PREVIEW]: _UNFLAG });
|
|
},
|
|
|
|
async save(buttonDone) {
|
|
const yaml = this.value.yamlForSave(this.currentYaml) || this.currentYaml;
|
|
|
|
try {
|
|
if ( this.applyHooks ) {
|
|
await this.applyHooks(BEFORE_SAVE_HOOKS);
|
|
}
|
|
|
|
try {
|
|
await this.value.saveYaml(yaml);
|
|
} catch (err) {
|
|
return onError.call(this, err);
|
|
}
|
|
|
|
if ( this.applyHooks ) {
|
|
await this.applyHooks(AFTER_SAVE_HOOKS);
|
|
}
|
|
|
|
buttonDone(true);
|
|
this.done();
|
|
} catch (err) {
|
|
return onError.call(this, err);
|
|
}
|
|
|
|
function onError(err) {
|
|
if ( err && err.response && err.response.data ) {
|
|
const body = err.response.data;
|
|
|
|
if ( body && body.message ) {
|
|
this.errors = [body.message];
|
|
} else {
|
|
this.errors = [err];
|
|
}
|
|
} else {
|
|
this.errors = [err];
|
|
}
|
|
|
|
buttonDone(false);
|
|
|
|
this.$emit('error', exceptionToErrorsArray(err));
|
|
}
|
|
},
|
|
|
|
done() {
|
|
if (this.doneOverride) {
|
|
return typeof (this.doneOverride) === 'function' ? this.doneOverride() : this.$router.replace(this.doneOverride);
|
|
}
|
|
if ( !this.doneRoute ) {
|
|
return;
|
|
}
|
|
if (typeOf(this.doneRoute) === 'object') {
|
|
this.$router.replace(this.doneRoute);
|
|
|
|
return;
|
|
}
|
|
this.$router.replace({
|
|
name: this.doneRoute,
|
|
params: { resource: this.value.type }
|
|
});
|
|
},
|
|
|
|
onFileSelected(value) {
|
|
const component = this.$refs.yamleditor;
|
|
|
|
if (component) {
|
|
component.updateValue(value);
|
|
}
|
|
},
|
|
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div class="root resource-yaml">
|
|
<YamlEditor
|
|
ref="yamleditor"
|
|
v-model="currentYaml"
|
|
:initial-yaml-values="initialYaml"
|
|
class="yaml-editor flex-content"
|
|
:editor-mode="editorMode"
|
|
@onInput="onInput"
|
|
@onReady="onReady"
|
|
@onChanges="onChanges"
|
|
/>
|
|
<slot
|
|
name="yamlFooter"
|
|
:currentYaml="currentYaml"
|
|
:showPreview="showPreview"
|
|
:yamlPreview="preview"
|
|
:yamlSave="save"
|
|
:yamlUnpreview="unpreview"
|
|
>
|
|
<Footer
|
|
v-if="showFooter"
|
|
:mode="mode"
|
|
:errors="errors"
|
|
@save="save"
|
|
@done="done"
|
|
>
|
|
<template
|
|
v-if="!isView"
|
|
#left
|
|
>
|
|
<FileSelector
|
|
class="btn role-secondary"
|
|
:label="t('generic.readFromFile')"
|
|
@selected="onFileSelected"
|
|
/>
|
|
</template>
|
|
<template
|
|
v-if="!isView"
|
|
#middle
|
|
>
|
|
<button
|
|
v-if="showPreview"
|
|
type="button"
|
|
class="btn role-secondary"
|
|
@click="unpreview"
|
|
>
|
|
<t k="resourceYaml.buttons.continue" />
|
|
</button>
|
|
<button
|
|
v-else-if="offerPreview"
|
|
:disabled="!canDiff"
|
|
type="button"
|
|
class="btn role-secondary"
|
|
@click="preview"
|
|
>
|
|
<t k="resourceYaml.buttons.diff" />
|
|
</button>
|
|
</template>
|
|
</Footer>
|
|
</slot>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang='scss' scoped>
|
|
.flex-content {
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex-grow: 1;
|
|
}
|
|
</style>
|
|
|
|
<style lang="scss">
|
|
.resource-yaml {
|
|
.yaml-editor {
|
|
min-height: 200px;
|
|
}
|
|
|
|
footer .actions {
|
|
text-align: right;
|
|
}
|
|
}
|
|
|
|
</style>
|