Merge pull request #576 from mantis-toboggan-md/masthead

masthead changes
This commit is contained in:
Vincent Fiduccia 2020-04-30 12:25:04 -05:00 committed by GitHub
commit 967c775b81
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 297 additions and 76 deletions

View File

@ -0,0 +1,92 @@
<script>
export default {
data() {
// make a map of all route names to validate programatically generated names
const allRoutes = this.$router.options.routes;
const allRouteMap = allRoutes.reduce((all, route) => {
all[route.name] = route;
return all;
}, {});
const { name, params } = this.$route;
const crumbPieces = name.split('-');
let crumbLocations = [];
crumbPieces.forEach((piece, i) => {
let nextName = piece;
if (crumbLocations[i - 1]) {
nextName = ( `${ crumbLocations[i - 1].name }-${ piece }`);
}
crumbLocations.push({
name: nextName,
params: this.paramsFor(nextName, params)
});
});
// remove root route 'c'
crumbLocations.shift();
// filter invalid routes
crumbLocations = crumbLocations.filter((location) => {
return (allRouteMap[location.name] && this.displayName(location, params));
});
return {
crumbLocations, params, crumbPieces, allRouteMap
};
},
methods: {
paramsFor(crumbName, params = this.params) {
const pieces = crumbName.split('-');
const out = {};
pieces.forEach((piece) => {
if (params[piece]) {
out[piece] = params[piece];
}
});
return out;
},
displayName(location, params = this.params) {
const pieces = location.name.split('-');
const lastPiece = pieces[pieces.length - 1];
if (lastPiece === 'resource') {
const resourceType = params[lastPiece];
const schema = this.$store.getters['cluster/schemaFor'](resourceType);
if (schema) {
return this.$store.getters['type-map/pluralLabelFor'](schema);
}
}
return params[lastPiece];
},
}
};
</script>
<template>
<div class="row">
<div v-for="(location, i) in crumbLocations" :key="location.name">
<span v-if="i > 0" class="divider">/</span>
<span v-if="i===crumbLocations.length-1">{{ displayName(location) }}</span>
<nuxt-link v-else :to="location">
{{ displayName(location) }}
</nuxt-link>
</div>
</div>
</template>
<style>
.breadcrumbs .divider {
margin: 0px 5px 0px 5px
}
</style>

View File

@ -0,0 +1,155 @@
<script>
import { PROJECT } from '../../config/labels-annotations';
import BreadCrumbs from '@/components/BreadCrumbs';
import { NAMESPACE, EXTERNAL } from '@/config/types';
export default {
components: { BreadCrumbs },
props: {
value: {
type: Object,
default: () => {
return {};
}
},
mode: {
type: String,
default: 'create'
},
realMode: {
type: String,
default: 'create'
},
doneRoute: {
type: String,
default: ''
},
asYaml: {
type: Boolean,
default: false
},
hasDetail: {
type: Boolean,
default: false
}
},
computed: {
schema() {
return this.$store.getters['cluster/schemaFor']( this.value.type );
},
h1() {
const typeLink = this.$router.resolve({
name: this.doneRoute,
params: this.$route.params
}).href;
const out = this.$store.getters['i18n/t'](`resourceDetail.header.${ this.realMode }`, {
typeLink,
type: this.$store.getters['type-map/singularLabelFor'](this.schema),
name: this.value.nameDisplay,
});
return out;
},
isNamespace() {
return this.schema.id === NAMESPACE;
},
namespace() {
if (this.value?.metadata?.namespace) {
return this.value?.metadata?.namespace;
}
return null;
},
namespaceLocation() {
if (!this.isNamespace) {
return {
name: 'c-cluster-resource-id',
params: {
cluster: this.$route.params.cluster,
resource: NAMESPACE,
id: this.$route.params.namespace
}
};
}
return null;
},
project() {
if (this.isNamespace) {
const id = (this.value?.metadata?.labels || {})[PROJECT];
return this.$store.getters['clusterExternal/byId'](EXTERNAL.PROJECT, id);
} else {
return null;
}
}
},
methods: {
showActions() {
this.$store.commit('action-menu/show', {
resources: this.value,
elem: this.$refs.actions,
});
},
toggleYaml() {
const out = !this.asYaml;
this.$emit('update:asYaml', out);
}
}
};
</script>
<template>
<header>
<BreadCrumbs class="breadcrumbs" />
<div>
<h1 v-html="h1" />
<!-- //TODO use nuxt-link for an internal project detail page once it exists -->
<span v-if="isNamespace">Project: {{ project.nameDisplay }}</span>
<span v-else-if="namespace">Namespace: <nuxt-link :to="namespaceLocation">{{ namespace }}</nuxt-link></span>
</div>
<div v-if="mode==='view'" class="actions">
<!-- //TODO remove check for custom detail component once there is a generic detail -->
<div v-if="hasDetail" class="yaml-toggle">
<button id="yaml-on" :disabled="asYaml" class="btn btn-sm role-primary" @click="toggleYaml">
YAML
</button>
<button id="yaml-off" :disabled="!asYaml" class="btn btn-sm role-primary" @click="toggleYaml">
Overview
</button>
</div>
<button ref="actions" aria-haspopup="true" type="button" class="btn btn-sm role-multi-action actions" @click="showActions">
<i class="icon icon-actions" />
</button>
</div>
</header>
</template>
<style lang='scss'>
.yaml-toggle{
display: inline-flex;
& #yaml-on{
border-radius: calc(var(--border-radius) * 2) 0px 0px calc(var(--border-radius) * 2);
}
& #yaml-off{
border-radius: 0px calc(var(--border-radius) * 2) calc(var(--border-radius) * 2) 0px;
}
}
</style>

View File

@ -8,6 +8,7 @@ import {
} from '@/config/query-params';
import { SCHEMA } from '@/config/types';
import { createYaml } from '@/utils/create-yaml';
import Masthead from '@/components/ResourceDetail/Masthead';
// Components can't have asyncData, only pages.
// So you have to call this in the page and pass it in as a prop.
@ -51,6 +52,7 @@ function realModeFor(query, id) {
// e.g. for workload to create a deployment
export async function defaultAsyncData(ctx, resource) {
const { store, params, route } = ctx;
// eslint-disable-next-line prefer-const
let { namespace, id } = params;
@ -65,7 +67,7 @@ export async function defaultAsyncData(ctx, resource) {
const hasCustomDetail = store.getters['type-map/hasCustomDetail'](resource);
const hasCustomEdit = store.getters['type-map/hasCustomEdit'](resource);
const asYaml = (route.query[AS_YAML] === _FLAGGED) || (realMode === _VIEW && !hasCustomDetail) || (realMode !== _VIEW && !hasCustomEdit);
const asYamlInit = (route.query[AS_YAML] === _FLAGGED) || (realMode === _VIEW && !hasCustomDetail) || (realMode !== _VIEW && !hasCustomEdit);
const schema = store.getters['cluster/schemaFor'](resource);
let originalModel, model, yaml;
@ -86,9 +88,7 @@ export async function defaultAsyncData(ctx, resource) {
originalModel = await store.dispatch('cluster/create', data);
model = await store.dispatch('cluster/clone', { resource: originalModel });
if ( asYaml ) {
yaml = createYaml(schemas, resource, data);
}
yaml = createYaml(schemas, resource, data);
} else {
let fqid = id;
@ -107,11 +107,9 @@ export async function defaultAsyncData(ctx, resource) {
model.applyDefaults(ctx, realMode);
}
if ( asYaml ) {
const link = originalModel.hasLink('rioview') ? 'rioview' : 'view';
const link = originalModel.hasLink('rioview') ? 'rioview' : 'view';
yaml = (await originalModel.followLink(link, { headers: { accept: 'application/yaml' } })).data;
}
yaml = (await originalModel.followLink(link, { headers: { accept: 'application/yaml' } })).data;
}
let mode = realMode;
@ -128,11 +126,12 @@ export async function defaultAsyncData(ctx, resource) {
hasCustomEdit,
resource,
model,
asYaml,
asYamlInit,
yaml,
originalModel,
mode,
realMode
realMode,
route
};
/*******
* Important: these need to be declared below as props too if you want to use them
@ -144,7 +143,7 @@ export async function defaultAsyncData(ctx, resource) {
export const watchQuery = [MODE, AS_YAML];
export default {
components: { ResourceYaml },
components: { ResourceYaml, Masthead },
mixins: { CreateEditView },
props: {
@ -164,7 +163,7 @@ export default {
type: Object,
default: null,
},
asYaml: {
asYamlInit: {
type: Boolean,
default: null,
},
@ -183,6 +182,12 @@ export default {
realMode: {
type: String,
default: null
},
route: {
type: Object,
default: () => {
return {};
}
}
},
@ -199,7 +204,11 @@ export default {
});
}
// 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 asYaml = this.asYamlInit;
return {
asYaml,
isCustomYamlEditor: false,
currentValue: this.value,
detailComponent: this.$store.getters['type-map/importDetail'](this.resource),
@ -210,9 +219,6 @@ export default {
},
computed: {
schema() {
return this.$store.getters['cluster/schemaFor']( this.model.type );
},
isView() {
return this.mode === _VIEW;
@ -251,44 +257,20 @@ export default {
return null;
},
h1() {
const typeLink = this.$router.resolve({
name: this.doneRoute,
params: this.$route.params
}).href;
const out = this.$store.getters['i18n/t'](`resourceDetail.header.${ this.realMode }`, {
typeLink,
type: this.$store.getters['type-map/singularLabelFor'](this.schema),
name: this.originalModel?.nameDisplay,
});
return out;
},
},
methods: {
showActions() {
this.$store.commit('action-menu/show', {
resources: this.originalModel,
elem: this.$refs.actions,
});
},
}
};
</script>
<template>
<div>
<header>
<h1 v-html="h1" />
<div v-if="isView" class="actions">
<button ref="actions" aria-haspopup="true" type="button" class="btn btn-sm role-multi-action actions" @click="showActions">
<i class="icon icon-actions" />
</button>
</div>
</header>
<Masthead
:value="originalModel"
:mode="mode"
:done-route="doneRoute"
:real-mode="realMode"
:as-yaml.sync="asYaml"
:has-detail="hasCustomDetail"
/>
<template v-if="asYaml">
<ResourceYaml
:model="model"
@ -355,29 +337,4 @@ export default {
opacity: 0.5
}
}
.detail-top{
display: flex;
flex-wrap: wrap;
background: var(--box-bg);
border: solid thin var(--border);
border-radius: var(--border-radius);
& > * {
margin-right: 20px;
padding: 10px 0 10px 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: flex-start;
&:not(:last-child){
border-right: 1px solid var(--border);
}
& >:not(:first-child){
color: var(--input-label);
padding: 3px;
}
}
}
</style>

View File

@ -107,7 +107,7 @@ export default {
/>
</div>
</section>
<ResourceTabs v-model="value" />
<ResourceTabs v-model="value" :mode="mode" />
<Footer
:mode="mode"

View File

@ -17,7 +17,7 @@ import Networking from '@/edit/workload/Networking';
import Footer from '@/components/form/Footer';
import Job from '@/edit/workload/Job';
import WorkloadPorts from '@/edit/workload/WorkloadPorts';
import { defaultAsyncData } from '@/components/ResourceDetail.vue';
import { defaultAsyncData } from '@/components/ResourceDetail';
import { _EDIT } from '@/config/query-params';
import ResourceTabs from '@/components/form/ResourceTabs';

View File

@ -253,7 +253,8 @@ export default {
HEADER {
display: grid;
grid-template-areas: "title actions";
grid-template-areas: "breadcrumbs breadcrumbs"
"title actions";
grid-template-columns: "auto min-content";
margin-bottom: 20px;
@ -271,6 +272,10 @@ export default {
grid-area: actions;
text-align: right;
}
.breadcrumbs {
grid-area: breadcrumbs;
}
}
}

View File

@ -2,9 +2,14 @@
import ResourceTable from '@/components/ResourceTable';
import Favorite from '@/components/nav/Favorite';
import { AS_YAML, _FLAGGED } from '@/config/query-params';
import BreadCrumbs from '@/components/BreadCrumbs';
export default {
components: { ResourceTable, Favorite },
components: {
ResourceTable,
Favorite,
BreadCrumbs
},
data() {
const params = { ...this.$route.params };
@ -28,6 +33,7 @@ export default {
}).href;
return {
route: this.$route,
listComponent,
formRoute,
yamlRoute,
@ -109,6 +115,8 @@ export default {
<template>
<div>
<header>
<BreadCrumbs class="breadcrumbs" :route="route" />
<h1>
{{ typeDisplay }} <Favorite :resource="resource" />
</h1>

View File

@ -135,6 +135,10 @@ export function pluralize(count, singular, plural) {
}
}
if (!count) {
return plural;
}
if (count === 1) {
return `${ count } ${ singular }`;
} else {