dashboard/shell/components/ResourceDetail/Masthead.vue

667 lines
16 KiB
Vue

<script>
import { KUBERNETES, PROJECT } from '@shell/config/labels-annotations';
import { FLEET, NAMESPACE, MANAGEMENT, HELM } from '@shell/config/types';
import ButtonGroup from '@shell/components/ButtonGroup';
import { BadgeState } from '@components/BadgeState';
import { Banner } from '@components/Banner';
import { get } from '@shell/utils/object';
import { NAME as FLEET_NAME } from '@shell/config/product/fleet';
import { HIDE_SENSITIVE } from '@shell/store/prefs';
import {
AS, _DETAIL, _CONFIG, _YAML, MODE, _CREATE, _EDIT, _VIEW, _UNFLAG, _GRAPH
} from '@shell/config/query-params';
import { ExtensionPoint, PanelLocation } from '@shell/core/types';
import ExtensionPanel from '@shell/components/ExtensionPanel';
import TabTitle from '@shell/components/TabTitle';
// i18n-uses resourceDetail.header.*
/**
* Resource Detail Masthead component.
*
* ToDo: this component seem to be picking up a lot of logic from special cases, could be simplified down to parameters and then customized per use-case via wrapper component
*/
export default {
name: 'MastheadResourceDetail',
components: {
BadgeState, Banner, ButtonGroup, ExtensionPanel, TabTitle
},
props: {
value: {
type: Object,
default: () => {
return {};
}
},
mode: {
type: String,
default: 'create'
},
realMode: {
type: String,
default: 'create'
},
as: {
type: String,
default: _YAML,
},
hasGraph: {
type: Boolean,
default: false
},
hasDetail: {
type: Boolean,
default: false
},
hasEdit: {
type: Boolean,
default: false
},
storeOverride: {
type: String,
default: null,
},
resource: {
type: String,
default: null,
},
resourceSubtype: {
type: String,
default: null,
},
parentRouteOverride: {
type: String,
default: null,
},
canViewYaml: {
type: Boolean,
default: false,
}
},
data() {
return {
DETAIL_VIEW: _DETAIL,
extensionType: ExtensionPoint.PANEL,
extensionLocation: PanelLocation.DETAILS_MASTHEAD,
};
},
computed: {
dev() {
return this.$store.getters['prefs/dev'];
},
schema() {
const inStore = this.storeOverride || this.$store.getters['currentStore'](this.resource);
return this.$store.getters[`${ inStore }/schemaFor`]( this.resource );
},
isView() {
return this.mode === _VIEW;
},
isEdit() {
return this.mode === _EDIT;
},
isCreate() {
return this.mode === _CREATE;
},
isNamespace() {
return this.schema?.id === NAMESPACE;
},
isProject() {
return this.schema?.id === MANAGEMENT.PROJECT;
},
isProjectHelmChart() {
return this.schema?.id === HELM.PROJECTHELMCHART;
},
hasMultipleNamespaces() {
return !!this.value.namespaces;
},
namespace() {
if (this.value?.metadata?.namespace) {
return this.value?.metadata?.namespace;
}
return null;
},
detailsAction() {
return this.value?.detailsAction;
},
shouldHifenize() {
return (this.mode === 'view' || this.mode === 'edit') && this.resourceSubtype?.length && this.value?.nameDisplay?.length;
},
namespaceLocation() {
if (!this.isNamespace) {
return this.value.namespaceLocation || {
name: 'c-cluster-product-resource-id',
params: {
cluster: this.$route.params.cluster,
product: this.$store.getters['productId'],
resource: NAMESPACE,
id: this.$route.params.namespace
}
};
}
return null;
},
isWorkspace() {
return this.$store.getters['productId'] === FLEET_NAME && !!this.value?.metadata?.namespace;
},
workspaceLocation() {
return {
name: 'c-cluster-product-resource-id',
params: {
cluster: this.$route.params.cluster,
product: this.$store.getters['productId'],
resource: FLEET.WORKSPACE,
id: this.$route.params.namespace
}
};
},
project() {
if (this.isNamespace) {
const cluster = this.$store.getters['currentCluster'];
if (cluster) {
const id = (this.value?.metadata?.labels || {})[PROJECT];
return this.$store.getters['management/byId'](MANAGEMENT.PROJECT, `${ cluster.id }/${ id }`);
}
}
return null;
},
banner() {
if (this.value?.stateObj?.error) {
const defaultErrorMessage = this.t('resourceDetail.masthead.defaultBannerMessage.error', undefined, true);
return {
color: 'error',
message: this.value.stateObj.message || defaultErrorMessage
};
}
if (this.value?.spec?.paused) {
return {
color: 'info',
message: this.t('asyncButton.pause.description')
};
}
if (this.value?.stateObj?.transitioning) {
const defaultTransitioningMessage = this.t('resourceDetail.masthead.defaultBannerMessage.transitioning', undefined, true);
return {
color: 'info',
message: this.value.stateObj.message || defaultTransitioningMessage
};
}
return null;
},
parent() {
const displayName = this.value?.parentNameOverride || this.$store.getters['type-map/labelFor'](this.schema);
const product = this.$store.getters['currentProduct'].name;
const defaultLocation = {
name: 'c-cluster-product-resource',
params: {
resource: this.resource,
product,
}
};
const location = this.value?.parentLocationOverride || defaultLocation;
if (this.parentRouteOverride) {
location.name = this.parentRouteOverride;
}
const typeOptions = this.$store.getters[`type-map/optionsFor`]( this.resource );
const out = {
displayName, location, ...typeOptions
};
return out;
},
hideSensitiveData() {
return this.$store.getters['prefs/get'](HIDE_SENSITIVE);
},
sensitiveOptions() {
return [
{
tooltipKey: 'resourceDetail.masthead.sensitive.hide',
icon: 'icon-hide',
value: true,
},
{
tooltipKey: 'resourceDetail.masthead.sensitive.show',
icon: 'icon-show',
value: false
}
];
},
viewOptions() {
const out = [];
if ( this.hasDetail ) {
out.push({
labelKey: 'resourceDetail.masthead.detail',
value: _DETAIL,
});
}
if ( this.hasEdit && this.parent?.showConfigView !== false) {
out.push({
labelKey: 'resourceDetail.masthead.config',
value: _CONFIG,
});
}
if ( this.hasGraph ) {
out.push({
labelKey: 'resourceDetail.masthead.graph',
value: _GRAPH,
});
}
if ( this.canViewYaml ) {
out.push({
labelKey: 'resourceDetail.masthead.yaml',
value: _YAML,
});
}
if ( out.length < 2 ) {
return null;
}
return out;
},
currentView: {
get() {
return this.as;
},
set(val) {
switch ( val ) {
case _DETAIL:
this.$router.applyQuery({
[MODE]: _UNFLAG,
[AS]: _UNFLAG,
});
break;
case _CONFIG:
this.$router.applyQuery({
[MODE]: _UNFLAG,
[AS]: _CONFIG,
});
break;
case _GRAPH:
this.$router.applyQuery({
[MODE]: _UNFLAG,
[AS]: _GRAPH,
});
break;
case _YAML:
this.$router.applyQuery({
[MODE]: _UNFLAG,
[AS]: _YAML,
});
break;
}
},
},
showSensitiveToggle() {
return !!this.value.hasSensitiveData && this.mode === _VIEW && this.as !== _YAML;
},
managedWarning() {
const { value } = this;
const labels = value?.metadata?.labels || {};
const managedBy = labels[KUBERNETES.MANAGED_BY] || '';
const appName = labels[KUBERNETES.MANAGED_NAME] || labels[KUBERNETES.INSTANCE] || '';
return {
show: this.mode === _EDIT && !!managedBy,
type: value?.kind || '',
hasName: appName ? 'yes' : 'no',
appName,
managedBy,
};
},
displayName() {
let displayName = this.value.nameDisplay;
if (this.isProjectHelmChart) {
displayName = this.value.projectDisplayName;
}
return this.shouldHifenize ? ` - ${ displayName }` : displayName;
},
location() {
const { parent } = this;
return parent?.location;
},
hideNamespaceLocation() {
return this.$store.getters['currentProduct'].hideNamespaceLocation || this.value.namespaceLocation === null;
},
resourceExternalLink() {
return this.value.resourceExternalLink;
},
},
methods: {
get,
showActions() {
this.$store.commit('action-menu/show', {
resources: this.value,
elem: this.$refs.actions,
});
},
toggleSensitiveData(e) {
this.$store.dispatch('prefs/set', { key: HIDE_SENSITIVE, value: !!e });
},
invokeDetailsAction() {
const action = this.detailsAction;
if (action) {
const fn = this.value[action.action];
if (fn) {
fn.apply(this.value, []);
}
}
}
}
};
</script>
<template>
<div class="masthead">
<header>
<div class="title">
<div class="primaryheader">
<h1>
<TabTitle
v-if="isCreate"
:showChild="false"
>
{{ parent.displayName }}
</TabTitle>
<TabTitle
v-else
:showChild="false"
>
{{ displayName }}
</TabTitle>
<router-link
v-if="location"
:to="location"
>
{{ parent.displayName }}:
</router-link>
<span v-else>{{ parent.displayName }}:</span>
<span v-if="value.detailPageHeaderActionOverride && value.detailPageHeaderActionOverride(realMode)">{{ value.detailPageHeaderActionOverride(realMode) }}</span>
<t
v-else
class="masthead-resource-title"
:k="'resourceDetail.header.' + realMode"
:subtype="resourceSubtype"
:name="displayName"
:escapehtml="false"
/>
<BadgeState
v-if="!isCreate && parent.showState"
class="masthead-state"
:value="value"
/>
<span
v-if="!isCreate && value.injectionEnabled"
class="masthead-istio"
>
<i
v-clean-tooltip="t('projectNamespaces.isIstioInjectionEnabled')"
class="icon icon-sm icon-istio"
/>
</span>
<a
v-if="dev && !!resourceExternalLink"
v-clean-tooltip="t(resourceExternalLink.tipsKey || 'generic.resourceExternalLinkTips')"
class="resource-external"
rel="nofollow noopener noreferrer"
target="_blank"
:href="resourceExternalLink.url"
>
<i class="icon icon-external-link" />
</a>
</h1>
</div>
<div
v-if="!isCreate"
class="subheader"
>
<span v-if="isNamespace && project">{{ t("resourceDetail.masthead.project") }}: <router-link :to="project.detailLocation">{{ project.nameDisplay }}</router-link></span>
<span v-else-if="isWorkspace">{{ t("resourceDetail.masthead.workspace") }}: <router-link :to="workspaceLocation">{{ namespace }}</router-link></span>
<span v-else-if="namespace && !hasMultipleNamespaces">
{{ t("resourceDetail.masthead.namespace") }}:
<router-link
v-if="!hideNamespaceLocation"
:to="namespaceLocation"
data-testid="masthead-subheader-namespace"
>
{{ namespace }}
</router-link>
<span v-else>
{{ namespace }}
</span>
</span>
<span v-if="parent.showAge">{{ t("resourceDetail.masthead.age") }}: <LiveDate
class="live-date"
:value="value.creationTimestamp"
/></span>
<span v-if="value.showPodRestarts">{{ t("resourceDetail.masthead.restartCount") }}:<span class="live-data"> {{ value.restartCount }}</span></span>
</div>
</div>
<slot name="right">
<div class="actions-container align-start">
<div class="actions">
<button
v-if="detailsAction && currentView === DETAIL_VIEW && isView"
type="button"
class="btn role-primary actions mr-10"
:disabled="!detailsAction.enabled"
@click="invokeDetailsAction"
>
{{ detailsAction.label }}
</button>
<ButtonGroup
v-if="showSensitiveToggle"
:value="!!hideSensitiveData"
icon-size="lg"
:options="sensitiveOptions"
class="mr-10"
@update:value="toggleSensitiveData"
/>
<ButtonGroup
v-if="viewOptions && isView"
v-model:value="currentView"
:options="viewOptions"
class="mr-10"
/>
<button
v-if="isView"
ref="actions"
data-testid="masthead-action-menu"
aria-haspopup="true"
type="button"
class="btn role-multi-action actions"
@click="showActions"
>
<i class="icon icon-actions" />
</button>
</div>
</div>
</slot>
</header>
<!-- Extension area -->
<ExtensionPanel
:resource="value"
:type="extensionType"
:location="extensionLocation"
/>
<Banner
v-if="banner && isView && !parent.hideBanner"
class="state-banner mb-10"
:color="banner.color"
:label="banner.message"
/>
<Banner
v-if="managedWarning.show"
color="warning"
class="mb-20"
:label="t('resourceDetail.masthead.managedWarning', managedWarning)"
/>
<slot />
</div>
</template>
<style lang='scss' scoped>
.masthead {
padding-bottom: 10px;
border-bottom: 1px solid var(--border);
margin-bottom: 10px;
}
HEADER {
margin: 0;
.title {
overflow: hidden;
}
}
.primaryheader {
display: flex;
flex-direction: row;
align-items: center;
h1 {
margin: 0;
overflow: hidden;
display: flex;
flex-direction: row;
align-items: center;
.masthead-resource-title {
padding: 0 8px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
}
.subheader{
display: flex;
flex-direction: row;
color: var(--input-label);
& > * {
margin: 5px 20px 5px 0px;
}
.live-data {
color: var(--body-text);
margin-left: 3px;
}
}
.state-banner {
margin: 3px 0 0 0;
}
.masthead-state {
font-size: initial;
}
.masthead-istio {
.icon {
vertical-align: middle;
color: var(--primary);
}
}
.left-right-split {
display: grid;
align-items: center;
.left-half {
grid-column: 1;
}
.right-half {
grid-column: 2;
}
}
div.actions-container > div.actions {
display: flex;
flex-direction: row;
justify-content: flex-end;
}
.resource-external {
font-size: 18px;
}
</style>