mirror of https://github.com/rancher/dashboard.git
667 lines
16 KiB
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>
|