dashboard/components/ResourceDetail/index.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>