mirror of https://github.com/rancher/dashboard.git
372 lines
9.3 KiB
Vue
372 lines
9.3 KiB
Vue
<script>
|
|
import { cleanForNew } from '@/plugins/steve/normalize';
|
|
import CreateEditView from '@/mixins/create-edit-view';
|
|
import ResourceYaml from '@/components/ResourceYaml';
|
|
import {
|
|
MODE, _VIEW, _EDIT, _CLONE, _STAGE,
|
|
AS_YAML, _FLAGGED, _CREATE,
|
|
} from '@/config/query-params';
|
|
import { SCHEMA } from '@/config/types';
|
|
import { createYaml } from '@/utils/create-yaml';
|
|
import Masthead from '@/components/ResourceDetail/Masthead';
|
|
import DetailTop from '@/components/DetailTop';
|
|
import FileSelector from '@/components/form/FileSelector';
|
|
import GenericResourceDetail from './Generic';
|
|
|
|
// Components can't have asyncData, only pages.
|
|
// So you have to call this in the page and pass it in as a prop.
|
|
export async function asyncData(ctx) {
|
|
const { params, store, route } = ctx;
|
|
const resource = params.resource;
|
|
const hasCustomEdit = store.getters['type-map/hasCustomEdit'](resource);
|
|
const hasCustomDetail = store.getters['type-map/hasCustomDetail'](resource);
|
|
const realMode = realModeFor(route.query.mode, params.id);
|
|
|
|
if ( hasCustomEdit && [_EDIT, _CREATE, _STAGE].includes(realMode) ) {
|
|
const importer = store.getters['type-map/importEdit'](resource);
|
|
const component = (await importer())?.default;
|
|
|
|
if ( component?.asyncData ) {
|
|
return component.asyncData(ctx);
|
|
}
|
|
}
|
|
|
|
if ( hasCustomDetail && realMode === _VIEW ) {
|
|
const importer = store.getters['type-map/importDetail'](resource);
|
|
const component = (await importer())?.default;
|
|
|
|
if ( component?.asyncData ) {
|
|
return component.asyncData(ctx);
|
|
}
|
|
}
|
|
|
|
return defaultAsyncData(ctx);
|
|
}
|
|
|
|
function realModeFor(query, id) {
|
|
if ( id ) {
|
|
return query || _VIEW;
|
|
} else {
|
|
return _CREATE;
|
|
}
|
|
}
|
|
|
|
// You can pass in a resource from a edit/someType.vue to use this but with a different type
|
|
// e.g. for workload to create a deployment
|
|
export async function defaultAsyncData(ctx, resource, parentOverride) {
|
|
const { store, params, route } = ctx;
|
|
|
|
const inStore = store.getters['currentProduct']?.inStore;
|
|
|
|
// eslint-disable-next-line prefer-const
|
|
let { namespace, id } = params;
|
|
|
|
if ( !resource ) {
|
|
resource = params.resource;
|
|
}
|
|
|
|
// There are 5 "real" modes that you can start in: view, edit, create, stage, clone
|
|
// These are mapped down to the 3 regular page modes that create-edit-view components
|
|
// know about: view, edit, create (stage and clone become "create")
|
|
const realMode = realModeFor(route.query.mode, id);
|
|
|
|
const hasCustomDetail = store.getters['type-map/hasCustomDetail'](resource);
|
|
const hasCustomEdit = store.getters['type-map/hasCustomEdit'](resource);
|
|
const asYamlInit = (route.query[AS_YAML] === _FLAGGED) || (realMode !== _VIEW && !hasCustomEdit);
|
|
const schema = store.getters[`${ inStore }/schemaFor`](resource);
|
|
const schemas = store.getters[`${ inStore }/all`](SCHEMA);
|
|
|
|
let originalModel, model, yaml;
|
|
|
|
if ( realMode === _CREATE ) {
|
|
if ( !namespace ) {
|
|
namespace = store.getters['defaultNamespace'];
|
|
}
|
|
|
|
const data = { type: resource };
|
|
|
|
if ( schema.attributes?.namespaced ) {
|
|
data.metadata = { namespace };
|
|
}
|
|
|
|
originalModel = await store.dispatch(`${ inStore }/create`, data);
|
|
model = originalModel;
|
|
yaml = createYaml(schemas, resource, data);
|
|
} else {
|
|
let fqid = id;
|
|
|
|
if ( schema.attributes?.namespaced && namespace ) {
|
|
fqid = `${ namespace }/${ fqid }`;
|
|
}
|
|
|
|
originalModel = await store.dispatch(`${ inStore }/find`, {
|
|
type: resource,
|
|
id: fqid,
|
|
opt: { watch: true }
|
|
});
|
|
if (realMode === _VIEW) {
|
|
model = originalModel;
|
|
} else {
|
|
model = await store.dispatch(`${ inStore }/clone`, { resource: originalModel });
|
|
}
|
|
|
|
const yamlOpt = { headers: { accept: 'application/yaml' } };
|
|
|
|
if ( originalModel.hasLink('rioview') ) {
|
|
yaml = (await originalModel.followLink('rioview', yamlOpt)).data;
|
|
} else if ( originalModel.hasLink('view') ) {
|
|
yaml = (await originalModel.followLink('view', yamlOpt)).data;
|
|
}
|
|
|
|
if ( realMode === _CLONE || realMode === _STAGE ) {
|
|
cleanForNew(model);
|
|
yaml = model.cleanYaml(yaml, realMode);
|
|
}
|
|
}
|
|
|
|
let mode = realMode;
|
|
|
|
if ( realMode === _STAGE || realMode === _CLONE ) {
|
|
mode = _CREATE;
|
|
}
|
|
|
|
/*******
|
|
* Important: these need to be declared below as props too if you want to use them
|
|
*******/
|
|
const out = {
|
|
parentOverride,
|
|
hasCustomDetail,
|
|
hasCustomEdit,
|
|
resource,
|
|
model,
|
|
asYamlInit,
|
|
yaml,
|
|
originalModel,
|
|
mode,
|
|
realMode,
|
|
value: model
|
|
};
|
|
/*******
|
|
* Important: these need to be declared below as props too if you want to use them
|
|
*******/
|
|
|
|
return out;
|
|
}
|
|
|
|
export const watchQuery = [MODE, AS_YAML];
|
|
|
|
export default {
|
|
components: {
|
|
DetailTop, FileSelector, ResourceYaml, Masthead, GenericResourceDetail
|
|
},
|
|
mixins: [CreateEditView],
|
|
|
|
props: {
|
|
hasCustomDetail: {
|
|
type: Boolean,
|
|
default: null,
|
|
},
|
|
hasCustomEdit: {
|
|
type: Boolean,
|
|
default: null,
|
|
},
|
|
resource: {
|
|
type: String,
|
|
default: null,
|
|
},
|
|
model: {
|
|
type: Object,
|
|
default: null,
|
|
},
|
|
asYamlInit: {
|
|
type: Boolean,
|
|
default: null,
|
|
},
|
|
yaml: {
|
|
type: String,
|
|
default: null,
|
|
},
|
|
originalModel: {
|
|
type: Object,
|
|
default: null,
|
|
},
|
|
mode: {
|
|
type: String,
|
|
default: null
|
|
},
|
|
parentOverride: {
|
|
type: Object,
|
|
default: null
|
|
}
|
|
},
|
|
|
|
data() {
|
|
if ( this.mode === _CREATE && this.value.applyDefaults ) {
|
|
this.value.applyDefaults(this, this.mode);
|
|
}
|
|
|
|
// asYamlInit is taken from route query and passed as prop from _id page; asYaml is saved in local data to be manipulated by Masthead
|
|
const {
|
|
asYamlInit: asYaml,
|
|
value: currentValue,
|
|
} = this;
|
|
|
|
return {
|
|
asYaml,
|
|
currentValue,
|
|
detailComponent: this.$store.getters['type-map/importDetail'](this.resource),
|
|
editComponent: this.$store.getters['type-map/importEdit'](this.resource),
|
|
};
|
|
},
|
|
|
|
computed: {
|
|
realMode() {
|
|
// There are 5 "real" modes that you can start in: view, edit, create, stage, clone
|
|
// These are mapped down to the 3 regular page modes that create-edit-view components
|
|
// know about: view, edit, create (stage and clone become "create")
|
|
const realMode = realModeFor(this.$route.query.mode, this.model?.id);
|
|
|
|
return realMode;
|
|
},
|
|
|
|
isView() {
|
|
return this.mode === _VIEW;
|
|
},
|
|
|
|
isEdit() {
|
|
return this.mode === _EDIT;
|
|
},
|
|
|
|
offerPreview() {
|
|
return [_EDIT, _CLONE, _STAGE].includes(this.mode);
|
|
},
|
|
|
|
showComponent() {
|
|
if ( this.isView ) {
|
|
if (this.hasCustomDetail) {
|
|
return this.detailComponent;
|
|
} else if (this.hasCustomEdit) {
|
|
return this.editComponent;
|
|
} else {
|
|
return GenericResourceDetail;
|
|
}
|
|
} else if ( this.hasCustomEdit ) {
|
|
return this.editComponent;
|
|
}
|
|
|
|
return null;
|
|
},
|
|
},
|
|
|
|
watch: {
|
|
asYamlInit(neu) {
|
|
this.asYaml = neu;
|
|
},
|
|
},
|
|
|
|
methods: {
|
|
// reading yamls from files is most easily tracked when done down in the component that handles other yaml-editing input, YamlEditor, but visually the button to upload lives up here
|
|
onFileSelected(value) {
|
|
const component = this.$refs.resourceyaml;
|
|
|
|
if (component) {
|
|
component.updateValue(value);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<Masthead
|
|
:value="originalModel"
|
|
:mode="mode"
|
|
:real-mode="realMode"
|
|
:as-yaml.sync="asYaml"
|
|
:parent-override="parentOverride"
|
|
:has-detail-or-edit="(hasCustomDetail || hasCustomEdit)"
|
|
>
|
|
<template v-if="!isView && asYaml" #right>
|
|
<div class="text-right">
|
|
<FileSelector ref="fileSelector" class="btn role-tertiary" :label="t('generic.readFromFile')" @selected="onFileSelected" />
|
|
</div>
|
|
</template>
|
|
</Masthead>
|
|
<DetailTop
|
|
v-if="isView && !asYaml"
|
|
:value="originalModel"
|
|
/>
|
|
<template v-if="asYaml">
|
|
<ResourceYaml
|
|
ref="resourceyaml"
|
|
:value="model"
|
|
:mode="mode"
|
|
:yaml="yaml"
|
|
:offer-preview="offerPreview"
|
|
:done-route="doneRoute"
|
|
:done-override="model.doneOverride"
|
|
/>
|
|
</template>
|
|
<template v-else>
|
|
<component
|
|
:is="showComponent"
|
|
v-model="model"
|
|
:original-value="originalModel"
|
|
:done-route="doneRoute"
|
|
:done-params="doneParams"
|
|
:mode="mode"
|
|
:real-mode="realMode"
|
|
:value="model"
|
|
v-bind="_data"
|
|
/>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang='scss' scoped>
|
|
.actions > * {
|
|
display: inline-block;
|
|
}
|
|
|
|
.flat {
|
|
border-collapse: collapse;
|
|
table-layout: fixed;
|
|
width: 100%;
|
|
& th{
|
|
padding-bottom: 1rem;
|
|
text-align: left;
|
|
font-weight: normal;
|
|
color: var(--secondary);
|
|
}
|
|
|
|
& :not(THEAD) tr{
|
|
border-bottom: 1px solid var(--border);
|
|
|
|
& td {
|
|
padding: 10px 0 10px 0;
|
|
}
|
|
}
|
|
& tr td:last-child, th:last-child{
|
|
text-align: right;
|
|
}
|
|
|
|
& tr td:first-child, th:first-child{
|
|
text-align: left;
|
|
margin-left: 15px;
|
|
}
|
|
|
|
& .click-row a{
|
|
color: var(--input-text);
|
|
}
|
|
|
|
& .click-row:hover{
|
|
@extend .faded;
|
|
}
|
|
|
|
& .faded {
|
|
opacity: 0.3
|
|
}
|
|
}
|
|
</style>
|