@kui-shell/plugin-client-common
Version:
Kui plugin that offers stylesheets
276 lines • 13.5 kB
JavaScript
/*
* Copyright 2020 The Kubernetes Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Debug from 'debug';
import React from 'react';
import { Tabs, Tab, TabTitleText } from '@patternfly/react-core/dist/esm/components/Tabs';
import { eventChannelUnsafe } from '@kui-shell/core/mdist/api/Events';
import { isButton, isResourceByReference, isResourceWithMetadata } from '@kui-shell/core/mdist/api/Response';
import { addRelevantModes, badgeRegistrar } from '@kui-shell/core/mdist/api/Sidecar';
import Badge from './Badge';
import KuiContext from '../../Client/context';
import ToolbarContainer from './ToolbarContainer';
import Toolbar from './Toolbar';
import BaseSidecar from './BaseSidecarV2';
import '../../../../web/css/static/ToolbarButton.scss';
import '../../../../web/scss/components/Sidecar/PatternFly.scss';
/** Lazily load KuiContent; see https://github.com/IBM/kui/issues/3746 */
const KuiContent = React.lazy(() => import('../../Content/KuiContent'));
const debug = Debug('plugin-sidecar/components/TopNavSidecar');
export function getStateFromMMR(tab, response) {
let allModes = response.modes.slice(0);
// consult the view registrar for registered view modes
// relevant to this resource
const cmd = '';
if (isResourceWithMetadata(response)) {
addRelevantModes(tab, allModes, cmd, { resource: response });
}
else if (isResourceByReference(response)) {
addRelevantModes(tab, allModes, cmd, response);
}
// obey the `order` constraints of the modes
allModes = allModes.sort((a, b) => {
return (a.order || 0) - (b.order || 0);
});
// defaultMode: if the response specified one, then look for it;
// otherwise, use the mode that considers itself default;
// otherwise use the first
const tabs = allModes.filter(_ => !isButton(_));
const defaultModeFromResponse = response.defaultMode ? tabs.findIndex(_ => _.mode === response.defaultMode) : -1;
const defaultModeFromModel = defaultModeFromResponse === -1 ? tabs.findIndex(_ => _.defaultMode) : defaultModeFromResponse;
const defaultMode = defaultModeFromModel === -1 ? 0 : defaultModeFromModel;
// re: as any: yay tsc, there are several open issue for this;
// it's related to isButton using generics
const buttonsFromRegistrar = allModes.filter(isButton);
const buttonsFromResponse = response.buttons || [];
const buttons = buttonsFromResponse.concat(buttonsFromRegistrar);
// toolbarText: if the default mode specified one, then use it;
// otherwise, use the one specified by response
const toolbarText = (tabs[defaultMode] && tabs[defaultMode].toolbarText) || response.toolbarText;
return {
currentTabIndex: defaultMode,
defaultMode,
tabs,
toolbarText,
viewButtons: buttons.filter(_ => !(_.kind === 'drilldown' && _.showRelatedResource)),
drilldownButtons: buttons.filter(_ => _.kind === 'drilldown' && _.showRelatedResource)
};
}
/**
*
* TopNavSidecar
* -----------------------
* | <TitleBar/> |
* -----------------------
* | nameHash? |
* | name |
* |---------------------|
* | Tab | Tab | ... | <Tab/> from here down - this is header()
* |---------------------| |
* | <Toolbar/> | <ToolbarContainer/> from here down
* |---------------------| | |
* | <KuiContent/> | | |
* | | to here to here
* -----------------------
* | <Toolbar/> | this is footer()
* -----------------------
*/
export default class TopNavSidecar extends BaseSidecar {
constructor(props) {
super(props);
this._didUpdateToolbar = this.didUpdateToolbar.bind(this);
this._onSelect = this.onSelect.bind(this);
this._bodyContentMemo = [];
this._style = { flexDirection: 'column' };
this.state = TopNavSidecar.getDerivedStateFromProps(props);
}
/** @return a `HistoryEntry` for the given `Response` */
static getDerivedStateFromProps(props, state) {
const { tab, response } = props;
if (!state || state.response !== response) {
const args = {
argsForMode: response.argsForMode,
argvNoOptions: props.argvNoOptions,
parsedOptions: props.parsedOptions
};
return Object.assign(state || {}, { response, toolbarText: response.toolbarText, args }, getStateFromMMR(tab, response));
}
else {
return state;
}
}
headerBodyStyle() {
return { 'flex-direction': 'column' };
}
/** return the pretty name or unadulterated name from the response */
prettyName() {
return (this.state.response &&
(this.state.response.prettyName || (this.state.response.metadata ? this.state.response.metadata.name : undefined)));
}
/** ToolbarContainer updated the toolbar */
didUpdateToolbar(toolbarText) {
this.setState({ toolbarText });
}
/** Tell the world that we have changed the focused mode */
broadcastFocusChange(idx) {
// de-focus the old mode
const oldMode = this.current.tabs[this.current.currentTabIndex];
eventChannelUnsafe.emit(`/mode/focus/off/tab/${this.props.uuid}/mode/${oldMode.mode}`, oldMode);
// re-focus the new mode
const newMode = this.current.tabs[idx];
eventChannelUnsafe.emit(`/mode/focus/on/tab/${this.props.uuid}/mode/${newMode.mode}`, newMode);
}
/** eventKey property for a Tab */
eventKey(idx) {
return idx;
}
/** idx from the encoded eventKey */
idxFromEventKey(eventKey) {
return eventKey;
}
/** User has changed selected Tab */
onSelect(_, eventKey) {
const idx = this.idxFromEventKey(eventKey);
// tell the views that we have changed focus
this.broadcastFocusChange(idx);
this.setState(curState => {
const toolbarText = curState.tabs[idx].toolbarText || curState.toolbarText;
return Object.assign({}, curState, { currentTabIndex: idx, toolbarText });
});
}
// first div used to be sidecar-top-stripe
tabs(executable) {
return (React.createElement("div", { className: "kui--sidecar-tabs-container zoomable full-height", onClick: this._stopPropagation },
React.createElement(Tabs, { className: "sidecar-bottom-stripe-mode-bits sidecar-bottom-stripe-button-container kui--sidecar-tabs", activeKey: this.eventKey(this.current.currentTabIndex), onSelect: this._onSelect, mountOnEnter: true }, this.current.tabs.map((mode, idx) => (React.createElement(Tab, { key: mode.mode, id: mode.mode, eventKey: this.eventKey(idx), className: "sidecar-bottom-stripe-button kui--sidecar-tab", title: React.createElement(TabTitleText, { className: "kui--sidecar-tab-label" }, mode.label || mode.mode), "data-mode": mode.mode, "data-is-selected": idx === this.current.currentTabIndex || undefined, onMouseDown: this._preventDefault }, this.tabContent(idx, executable)))))));
}
bodyContent(idx) {
if (!this._bodyContentMemo[idx]) {
this._bodyContentMemo[idx] = (React.createElement(KuiContent, { tab: this.props.tab, mode: this.current.tabs[idx], isActive: true /* idx === this.current.currentTabIndex */, args: this.state.args, response: this.state.response, execUUID: this.props.execUUID }));
}
return this._bodyContentMemo[idx];
}
tabContent(idx, executable) {
return (React.createElement("div", { className: "sidecar-content-container kui--tab-content", hidden: idx !== this.current.currentTabIndex || undefined },
React.createElement("div", { className: "custom-content" },
React.createElement(ToolbarContainer, { tab: this.props.tab, execUUID: this.props.execUUID, response: this.state.response, args: this.state.args, didUpdateToolbar: this._didUpdateToolbar, toolbarText: this.state.toolbarText, noAlerts: this.current.currentTabIndex !== this.current.defaultMode, buttons: executable ? this.current.viewButtons : [] }, this.bodyContent(idx)))));
}
/** Return a collection of badge elements */
badges() {
const badges = badgeRegistrar.filter(({ when }) => {
// filter out any irrelevant badges (for this resource)
try {
return when(this.state.response);
}
catch (err) {
debug('warning: registered badge threw an exception during filter', err);
return false;
}
});
return (badges &&
badges.length > 0 && (React.createElement("div", { className: "badges" }, badges.map(({ badge }, idx) => (React.createElement(Badge, { key: idx, spec: badge, tab: this.props.tab, response: this.state.response }))))));
}
header() {
const badges = this.badges();
if (badges) {
return (React.createElement("header", { className: "sidecar-header" },
React.createElement("div", { className: "header-main-content" },
React.createElement("div", { className: "kui--sidecar-header-and-toolbar" },
React.createElement("div", { className: "header-top-bits" },
React.createElement("div", { className: "header-left-bits" }),
React.createElement("div", { className: "header-right-bits" },
React.createElement("div", { className: "custom-header-content" }, badges)))))));
}
}
/**
* The footer offers drilldown buttons that we assume depend on the
* client supporting command execution. For example, a static single
* page web app would not allow for this kind of functionality.
*
*/
footer() {
return (this.props.executable && (React.createElement(Toolbar, { bottom: true, tab: this.props.tab, execUUID: this.props.execUUID, response: this.state.response, args: this.state.args, buttons: this.current.drilldownButtons })));
}
kindBreadcrumb() {
const { kind, onclick } = this.state.response;
return { label: kind, command: onclick && onclick.kind, className: 'kui--sidecar-kind' };
}
/** Show resource name as breadcrumb */
nameBreadcrumb() {
const { onclick } = this.state.response;
return {
label: this.prettyName(),
command: onclick && onclick.name,
isCurrentPage: true,
className: 'kui--sidecar-entity-name'
};
}
versionBreadcrumb() {
return this.state.response.version
? { label: this.state.response.version, className: 'kui--version-breadcrumb' }
: undefined;
}
nameHashBreadcrumb() {
const { onclick } = this.state.response;
return {
label: this.state.response && this.state.response.nameHash,
command: onclick && onclick.nameHash,
deemphasize: true,
className: 'kui--sidecar-entity-name-hash'
};
}
namespaceBreadcrumb() {
const { metadata: { namespace }, onclick } = this.state.response;
return {
label: namespace,
command: onclick && onclick.namespace,
deemphasize: true,
className: 'kui--sidecar-entity-namespace'
};
}
render() {
if (!this.current || !this.state.response) {
if (this.props.onRender) {
this.props.onRender(false);
}
return React.createElement("div", null);
}
else if (this.props.onRender) {
// needs to be async'd; see https://github.com/kubernetes-sigs/kui/issues/7539
setTimeout(() => this.props.onRender(true));
}
const nameBreadCrumbs = this.context.sidecarName === 'breadcrumb'
? [this.nameBreadcrumb(), this.versionBreadcrumb(), this.nameHashBreadcrumb()]
: [];
try {
const breadcrumbs = [this.kindBreadcrumb()]
.concat(nameBreadCrumbs)
.concat(this.namespaceBreadcrumb())
.filter(_ => _);
// Note: data-view helps with tests
return (React.createElement("div", { className: "kui--sidecar kui--sidecar-nested visible", ref: this.dom, "data-view": "topnav", onClick: this._stopPropagation },
this.title({
breadcrumbs
}),
React.createElement("div", { className: "kui--sidecar-header-and-body", style: this._style },
this.tabs(this.props.executable),
this.footer())));
}
catch (err) {
console.error(err);
}
}
}
TopNavSidecar.contextType = KuiContext;
//# sourceMappingURL=TopNavSidecarV2.js.map