UNPKG

@kui-shell/plugin-client-common

Version:

Kui plugin that offers stylesheets

276 lines 13.5 kB
/* * 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