diff --git a/frontend/public/actions/features.ts b/frontend/public/actions/features.ts index e619761449b..77bfcbdb5cb 100644 --- a/frontend/public/actions/features.ts +++ b/frontend/public/actions/features.ts @@ -8,7 +8,7 @@ import { receivedResources } from './k8s'; import { coFetchJSON } from '../co-fetch'; import { MonitoringRoutes } from '../reducers/monitoring'; import { setMonitoringURL } from './monitoring'; -import { setCreateProjectMessage, setUser } from './ui'; +import { setConsoleLinks, setCreateProjectMessage, setUser } from './ui'; import { FLAGS } from '../const'; export enum ActionType { @@ -129,6 +129,18 @@ const detectUser = dispatch => coFetchJSON('api/kubernetes/apis/user.openshift.i }, ); +const detectConsoleLinks = dispatch => coFetchJSON('api/kubernetes/apis/console.openshift.io/v1/consolelinks') + .then( + (consoleLinks) => { + dispatch(setConsoleLinks(_.get(consoleLinks, 'items'))); + }, + err => { + if (!_.includes([401, 403, 404, 500], _.get(err, 'response.status'))) { + setTimeout(() => detectConsoleLinks(dispatch), 15000); + } + }, + ); + const projectListPath = `${k8sBasePath}/apis/project.openshift.io/v1/projects?limit=1`; const detectShowOpenShiftStartGuide = (dispatch, canListNS: boolean = false) => { // Skip the project check if we know the user can list all namespaces. This @@ -202,5 +214,6 @@ export const detectFeatures = () => (dispatch: Dispatch) => [ detectClusterVersion, detectUser, detectLoggingURL, + detectConsoleLinks, ...ssarCheckActions, ].forEach(detect => detect(dispatch)); diff --git a/frontend/public/actions/ui.ts b/frontend/public/actions/ui.ts index eef7090223f..bf1b72c5a35 100644 --- a/frontend/public/actions/ui.ts +++ b/frontend/public/actions/ui.ts @@ -35,6 +35,7 @@ export enum ActionType { UpdateOverviewGroupOptions = 'updateOverviewGroupOptions', UpdateOverviewFilterValue = 'updateOverviewFilterValue', UpdateTimestamps = 'updateTimestamps', + SetConsoleLinks = 'setConsoleLinks', } // URL routes that can be namespaced @@ -194,6 +195,7 @@ export const monitoringLoading = (key: 'alerts' | 'silences') => action(ActionTy export const monitoringLoaded = (key: 'alerts' | 'silences', data: any) => action(ActionType.SetMonitoringData, {key, data: {loaded: true, loadError: null, data}}); export const monitoringErrored = (key: 'alerts' | 'silences', loadError: any) => action(ActionType.SetMonitoringData, {key, data: {loaded: true, loadError, data: null}}); export const monitoringToggleGraphs = () => action(ActionType.ToggleMonitoringGraphs); +export const setConsoleLinks = (consoleLinks: string[]) => action(ActionType.SetConsoleLinks, {consoleLinks}); // TODO(alecmerdler): Implement all actions using `typesafe-actions` and add them to this export const uiActions = { @@ -218,6 +220,7 @@ const uiActions = { monitoringLoaded, monitoringErrored, monitoringToggleGraphs, + setConsoleLinks, }; export type UIAction = Action; diff --git a/frontend/public/components/masthead-toolbar.jsx b/frontend/public/components/masthead-toolbar.jsx index 29c888f60d8..1a4e4b95b1a 100644 --- a/frontend/public/components/masthead-toolbar.jsx +++ b/frontend/public/components/masthead-toolbar.jsx @@ -197,6 +197,10 @@ class MastheadToolbar_ extends React.Component { ]; } + _getAdditionalLinks(links, type) { + return _.sortBy(_.filter(links, link => link.spec.location === type), 'spec.text'); + } + _launchActions() { return [ { @@ -208,8 +212,9 @@ class MastheadToolbar_ extends React.Component { ]; } - _helpActions() { - return [{ + _helpActions(additionalHelpActions) { + const helpActions = []; + helpActions.push({ label: 'Documentation', callback: this._onDocumentation, externalLink: true, @@ -219,7 +224,21 @@ class MastheadToolbar_ extends React.Component { }, { label: 'About', callback: this._onAboutModal, - }]; + }, ...additionalHelpActions); + return helpActions; + } + + _getAdditionalActions(links) { + return _.map(links, link => { + return { + callback: (e) => { + e.preventDefault(); + window.open(link.spec.href,'_blank').opener = null; + }, + label: link.spec.text, + externalLink: true, + }; + }); } _renderMenuItems(actions) { @@ -232,9 +251,10 @@ class MastheadToolbar_ extends React.Component { } _renderMenu(mobile) { - const { flags } = this.props; + const { flags, consoleLinks } = this.props; const { isUserDropdownOpen, isKebabDropdownOpen, username } = this.state; - const helpActions = this._helpActions(); + const additionalUserActions = this._getAdditionalActions(this._getAdditionalLinks(consoleLinks, 'UserMenu')); + const helpActions = this._helpActions(this._getAdditionalActions(this._getAdditionalLinks(consoleLinks, 'HelpMenu'))); const launchActions = this._launchActions(); if (flagPending(flags[FLAGS.OPENSHIFT]) || flagPending(flags[FLAGS.AUTH_ENABLED]) || !username) { @@ -252,7 +272,7 @@ class MastheadToolbar_ extends React.Component { } }; - if (mobile) { + if (mobile || !_.isEmpty(additionalUserActions)) { actions.push({ separator: true, }); @@ -272,6 +292,16 @@ class MastheadToolbar_ extends React.Component { }); } + if (!_.isEmpty(additionalUserActions)) { + actions.unshift(...additionalUserActions); + + if (mobile) { + actions.unshift({ + separator: true, + }); + } + } + if (mobile) { actions.unshift(...helpActions); @@ -310,7 +340,7 @@ class MastheadToolbar_ extends React.Component { render() { const { isApplicationLauncherDropdownOpen, isHelpDropdownOpen, showAboutModal, statuspageData } = this.state; - const { flags } = this.props; + const { flags, consoleLinks } = this.props; const resources = [{ kind: clusterVersionReference, name: 'version', @@ -356,7 +386,7 @@ class MastheadToolbar_ extends React.Component { } isOpen={isHelpDropdownOpen} - dropdownItems={this._renderMenuItems(this._helpActions())} + dropdownItems={this._renderMenuItems(this._helpActions(this._getAdditionalActions(this._getAdditionalLinks(consoleLinks, 'HelpMenu'))))} /> @@ -375,7 +405,10 @@ class MastheadToolbar_ extends React.Component { } } -const mastheadToolbarStateToProps = state => ({ user: state.UI.get('user') }); +const mastheadToolbarStateToProps = state => ({ + user: state.UI.get('user'), + consoleLinks: state.UI.get('consoleLinks'), +}); export const MastheadToolbar = connect( mastheadToolbarStateToProps, diff --git a/frontend/public/models/index.ts b/frontend/public/models/index.ts index 9a26c457696..c11f1a96734 100644 --- a/frontend/public/models/index.ts +++ b/frontend/public/models/index.ts @@ -889,3 +889,15 @@ export const InfrastructureModel: K8sKind = { crd: true, }; +export const ConsoleLinkModel: K8sKind = { + label: 'Console Link', + labelPlural: 'Console Links', + apiVersion: 'v1', + apiGroup: 'console.openshift.io', + plural: 'consolelinks', + abbr: 'CL', + namespaced: false, + kind: 'ConsoleLink', + id: 'consolelink', + crd: true, +}; diff --git a/frontend/public/models/yaml-templates.ts b/frontend/public/models/yaml-templates.ts index a95a3bdf848..48ea1bedfdc 100644 --- a/frontend/public/models/yaml-templates.ts +++ b/frontend/public/models/yaml-templates.ts @@ -808,6 +808,15 @@ spec: apiVersion: machine.openshift.io/v1beta1 kind: MachineSet name: worker +`).setIn([referenceForModel(k8sModels.ConsoleLinkModel), 'default'], ` +apiVersion: console.openshift.io/v1 +kind: ConsoleLink +metadata: + name: example +spec: + href: 'https://www.example.com' + location: HelpMenu + text: Additional help menu link `); const pluginTemplates = ImmutableMap>() diff --git a/frontend/public/reducers/ui.ts b/frontend/public/reducers/ui.ts index 37f6dffdefc..edba22c84b6 100644 --- a/frontend/public/reducers/ui.ts +++ b/frontend/public/reducers/ui.ts @@ -62,6 +62,7 @@ export default (state: UIState, action: UIAction): UIState => { filterValue: '', }), user: {}, + consoleLinks: [], }); } @@ -160,6 +161,9 @@ export default (state: UIState, action: UIAction): UIState => { case ActionType.UpdateTimestamps: return state.set('lastTick', action.payload.lastTick); + case ActionType.SetConsoleLinks: + return state.set('consoleLinks', action.payload.consoleLinks); + default: break; }