diff --git a/packages/ui/src/ui/UIFactory.tsx b/packages/ui/src/ui/UIFactory.tsx index b0b5adfe7..475bbbe55 100644 --- a/packages/ui/src/ui/UIFactory.tsx +++ b/packages/ui/src/ui/UIFactory.tsx @@ -99,6 +99,19 @@ export type AclRoleActionsType = Partial) => boolean; + position: {before: TabName} | {after: TabName}; +}; + export interface UIFactory { getClusterAppearance(cluster?: string): undefined | ClusterAppearance; @@ -389,6 +402,8 @@ export interface UIFactory { getAclPermissionsSettings(): typeof PERMISSIONS_SETTINGS; onChytAliasSqlClick(params: {alias: string; cluster: string}): void; + + getNavigationExtraTabs(): Array; } const experimentalPages: string[] = []; @@ -662,6 +677,10 @@ const uiFactory: UIFactory = { }, onChytAliasSqlClick() {}, + + getNavigationExtraTabs() { + return []; + }, }; function configureUIFactoryItem(k: K, redefinition: UIFactory[K]) { diff --git a/packages/ui/src/ui/pages/navigation/Navigation/ContentViewer/helpers/getComponentByMode.ts b/packages/ui/src/ui/pages/navigation/Navigation/ContentViewer/helpers/getComponentByMode.ts index 241d4c163..02910cf3d 100644 --- a/packages/ui/src/ui/pages/navigation/Navigation/ContentViewer/helpers/getComponentByMode.ts +++ b/packages/ui/src/ui/pages/navigation/Navigation/ContentViewer/helpers/getComponentByMode.ts @@ -11,23 +11,35 @@ import UserAttributes from '../../../../../pages/navigation/tabs/UserAttributes/ import TableMountConfig from '../../../../../pages/navigation/tabs/TableMountConfig/TableMountConfig'; import {Tab} from '../../../../../constants/navigation'; import {Fragment} from 'react'; +import UIFactory from '../../../../../UIFactory'; -const supportedAttributeTypes = { - acl: ACL, - locks: Locks, - schema: Schema, - tablets: Tablets, - attributes: Attributes, - tablet_errors: TabletErrors, - user_attributes: UserAttributes, - [Tab.ACCESS_LOG]: AccessLog, - [Tab.AUTO]: Fragment, - [Tab.CONSUMER]: Consumer, - [Tab.MOUNT_CONFIG]: TableMountConfig, - [Tab.QUEUE]: Queue, +const getSupportedAttributeTypes = () => { + const supportedAttributeTypes: Record = { + acl: ACL, + locks: Locks, + schema: Schema, + tablets: Tablets, + attributes: Attributes, + tablet_errors: TabletErrors, + user_attributes: UserAttributes, + [Tab.ACCESS_LOG]: AccessLog, + [Tab.AUTO]: Fragment, + [Tab.CONSUMER]: Consumer, + [Tab.MOUNT_CONFIG]: TableMountConfig, + [Tab.QUEUE]: Queue, + }; + + UIFactory.getNavigationExtraTabs().forEach((tab) => { + supportedAttributeTypes[tab.value] = tab.component; + }); + + return supportedAttributeTypes; }; -export default (mode: string) => - mode in supportedAttributeTypes +export default (mode: string) => { + const supportedAttributeTypes = getSupportedAttributeTypes(); + + return mode in supportedAttributeTypes ? supportedAttributeTypes[mode as keyof typeof supportedAttributeTypes] : undefined; +}; diff --git a/packages/ui/src/ui/pages/navigation/Navigation/Navigation.js b/packages/ui/src/ui/pages/navigation/Navigation/Navigation.js index 2bcac3455..d942207e8 100644 --- a/packages/ui/src/ui/pages/navigation/Navigation/Navigation.js +++ b/packages/ui/src/ui/pages/navigation/Navigation/Navigation.js @@ -3,7 +3,6 @@ import {StickyContainer} from 'react-sticky'; import {connect, useSelector} from 'react-redux'; import PropTypes from 'prop-types'; import hammer from '../../../common/hammer'; -import ypath from '../../../common/thor/ypath'; import cn from 'bem-cn-lite'; import _ from 'lodash'; import {getCluster} from '../../../store/selectors/global'; @@ -30,7 +29,6 @@ import {LOADING_STATUS} from '../../../constants/index'; import {onTransactionChange, setMode, updateView} from '../../../store/actions/navigation'; import { - getAttributes, getError, getIdmSupport, getLoadState, @@ -40,7 +38,7 @@ import { getType, isNavigationFinalLoadState, } from '../../../store/selectors/navigation'; -import {getEffectiveMode, getSupportedTabs} from '../../../store/selectors/navigation/navigation'; +import {getEffectiveMode, getTabs} from '../../../store/selectors/navigation/navigation'; import {NavigationPermissionsNotice} from './NavigationPermissionsNotice'; import {useRumMeasureStop} from '../../../rum/RumUiContext'; import {useAppRumMeasureStart} from '../../../rum/rum-app-measures'; @@ -56,7 +54,6 @@ import CreateACOModal from '../modals/CreateACOModal'; import Button from '../../../components/Button/Button'; import Icon from '../../../components/Icon/Icon'; import {showNavigationAttributesEditor} from '../../../store/actions/navigation/modals/attributes-editor'; -import {getTabletErrorsCount} from '../../../store/selectors/navigation/tabs/tablet-errors'; import {getPermissionDeniedError} from '../../../utils/errors'; import {getParentPath} from '../../../utils/navigation'; import UIFactory from '../../../UIFactory'; @@ -96,8 +93,6 @@ class Navigation extends Component { transaction: PropTypes.string, parsedPath: PropTypes.object, type: PropTypes.string, - supportedTabs: PropTypes.object.isRequired, // actually it's ES6 Set - attributes: PropTypes.object.isRequired, hasError: PropTypes.bool, error: PropTypes.shape({ @@ -150,147 +145,24 @@ class Navigation extends Component { } get items() { - const {setMode, attributes, tabletErrorsCount} = this.props; - const isACO = attributes?.type === 'access_control_object'; - - return [ - { - value: Tab.CONSUMER, - title: 'Go to consumer [Alt+R]', - hotkey: [ - { - keys: 'alt+r', - handler: () => setMode(Tab.CONSUMER), - scope: 'all', - }, - ], - }, - { - value: Tab.CONTENT, - title: 'Go to content [Alt+C]', - text: isACO ? 'Principal ACL' : undefined, - hotkey: [ - { - keys: 'alt+c', - handler: () => setMode(Tab.CONTENT), - scope: 'all', - }, - ], - }, - { - value: Tab.QUEUE, - title: 'Go to queue [Alt+Q]', - hotkey: [ - { - keys: 'alt+q', - handler: () => setMode(Tab.QUEUE), - scope: 'all', - }, - ], - }, - { - value: Tab.ATTRIBUTES, - title: 'Go to attributes [Alt+A]', - hotkey: [ - { - keys: 'alt+a', - handler: () => setMode(Tab.ATTRIBUTES), - scope: 'all', - }, - ], - caption: 'Attributes', - }, - { - value: Tab.USER_ATTRIBUTES, - title: 'Go to user attributes [Alt+U]', - hotkey: [ - { - keys: 'alt+u', - handler: () => setMode(Tab.USER_ATTRIBUTES), - scope: 'all', - }, - ], - caption: 'User Attributes', - }, - { - value: Tab.MOUNT_CONFIG, - title: 'Go to mount config', - hotkey: [ - { - keys: 'alt+m', - handler: () => setMode(Tab.MOUNT_CONFIG), - scope: 'all', - }, - ], - }, - { - value: Tab.ACL, - title: 'Go to ACL [Alt+P]', - hotkey: [ - { - keys: 'alt+p', - handler: () => setMode(Tab.ACL), - scope: 'all', - }, - ], - caption: 'ACL', - }, - { - value: Tab.ACCESS_LOG, - title: 'Access log', - }, - { - value: Tab.LOCKS, - title: 'Go to locks [Alt+L]', - hotkey: [ - { - keys: 'alt+l', - handler: () => setMode(Tab.LOCKS), - scope: 'all', - }, - ], - counter: ypath.getValue(attributes, '/lock_count'), - }, - { - value: Tab.ANNOTATION, - title: 'Go to annotation [Alt+N]', - hotkey: [ - { - keys: 'alt+n', - handler: () => setMode(Tab.ACL), - scope: 'all', - }, - ], - caption: 'Annotation', - }, - { - value: Tab.SCHEMA, - title: 'Go to schema [Alt+S]', - hotkey: [ - { - keys: 'alt+s', - handler: () => setMode(Tab.SCHEMA), - scope: 'all', - }, - ], - }, - { - value: Tab.TABLETS, - title: 'Go to tablets [Alt+T]', - hotkey: [ - { - keys: 'alt+t', - handler: () => setMode(Tab.TABLETS), - scope: 'all', - }, - ], - }, - { - value: Tab.TABLET_ERRORS, - title: 'Go to tablets errors', - counter: tabletErrorsCount, - }, - ]; + const {tabs, setMode} = this.props; + + return tabs.map((tab) => { + if (tab.hotkey) { + return { + ...tab, + hotkey: tab.hotkey.map(({keys, tab, scope}) => { + return { + keys, + scope, + handler: () => setMode(tab), + }; + }), + }; + } + + return tab; + }); } onTabChange = (value) => { @@ -299,11 +171,11 @@ class Navigation extends Component { }; renderTabs() { - const {mode, supportedTabs, tabSize} = this.props; + const {mode, tabSize} = this.props; const items = _.map(this.items, (item) => ({ ...item, text: item.text || hammer.format['ReadableField'](item.value), - show: supportedTabs.has(item.value), + show: true, })); return ( @@ -469,8 +341,6 @@ function mapStateToProps(state) { mode: getEffectiveMode(state), type: getType(state), isIdmSupported: getIdmSupport(state), - supportedTabs: getSupportedTabs(state), - attributes: getAttributes(state), error: getError(state), hasError, loaded, @@ -479,7 +349,7 @@ function mapStateToProps(state) { transaction: getTransaction(state), cluster: getCluster(state), tabSize: UI_TAB_SIZE, - tabletErrorsCount: getTabletErrorsCount(state), + tabs: getTabs(state), }; } diff --git a/packages/ui/src/ui/store/actions/navigation/index.js b/packages/ui/src/ui/store/actions/navigation/index.js index 984c8c219..d0d18615c 100644 --- a/packages/ui/src/ui/store/actions/navigation/index.js +++ b/packages/ui/src/ui/store/actions/navigation/index.js @@ -6,7 +6,6 @@ import {RumMeasureTypes} from '../../../rum/rum-measure-types'; import {isPathAutoCorrectionSettingEnabled} from '../../../store/selectors/settings'; import {getPath, getTransaction} from '../../../store/selectors/navigation'; -import {getDefaultMode} from '../../../store/selectors/navigation/navigation'; import { autoCorrectPath, @@ -35,6 +34,7 @@ import {checkPermissions} from '../../../utils/acl/acl-api'; import {getAnnotation} from './tabs/annotation'; import {loadTabletErrorsCount} from './tabs/tablet-errors'; import {isSupportedEffectiveExpiration} from '../../../store/selectors/thor/support'; +import {getTabs} from '../../../store/selectors/navigation/navigation'; export function updateView(settings = {}) { return (dispatch, getState) => { @@ -216,11 +216,11 @@ export function updateView(settings = {}) { export function setMode(mode) { return (dispatch, getState) => { - const defaultMode = getDefaultMode(getState()); + const [firstTab] = getTabs(getState()); dispatch({ type: SET_MODE, - data: mode === defaultMode ? Tab.AUTO : mode, + data: mode === firstTab?.value ? Tab.AUTO : mode, }); }; } diff --git a/packages/ui/src/ui/store/selectors/navigation/navigation.ts b/packages/ui/src/ui/store/selectors/navigation/navigation.ts index 86304eb61..7bdc06629 100644 --- a/packages/ui/src/ui/store/selectors/navigation/navigation.ts +++ b/packages/ui/src/ui/store/selectors/navigation/navigation.ts @@ -6,13 +6,14 @@ import unipika from '../../../common/thor/unipika'; import type {RootState} from '../../../store/reducers'; import type {ValueOf, YTError} from '../../../types'; -import {getParsedPath, getPath, getTransaction} from './index'; +import {getAttributes, getParsedPath, getPath, getTransaction} from './index'; import {ParsedPath, prepareNavigationState} from '../../../utils/navigation'; import {Tab} from '../../../constants/navigation/index'; import {getTableMountConfigHasData} from '../../../store/selectors/navigation/content/table-mount-config'; import {getAccessLogBasePath} from '../../../config'; import {getTabletErrorsCount} from '../../../store/selectors/navigation/tabs/tablet-errors'; +import UIFactory from '../../../UIFactory'; export function getNavigationPathAttributesLoadState(state: RootState) { return state.navigation.navigation.loadState; @@ -110,14 +111,213 @@ export const getSupportedTabs = createSelector( supportedTabletErrors = [Tab.TABLET_ERRORS]; } - return new Set([...alwaysSupportedTabs, ...supportedByAttribute, ...supportedTabletErrors]); + const supportedTabs = new Set([ + ...alwaysSupportedTabs, + ...supportedByAttribute, + ...supportedTabletErrors, + ]); + + UIFactory.getNavigationExtraTabs().forEach((tab) => { + if (tab.isSupported(attributes)) { + supportedTabs.add(tab.value); + } + }); + + return supportedTabs; }, ); -export const getDefaultMode = createSelector([getSupportedTabs], (supportedTabs) => - supportedTabs.has(Tab.CONSUMER) ? Tab.CONSUMER : Tab.CONTENT, -); +export const getTabs = createSelector( + [getSupportedTabs, getTabletErrorsCount, getAttributes], + (supportedTabs, tabletErrorsCount, attributes) => { + const isACO = attributes?.type === 'access_control_object'; + + const tabs: { + value: string; + title: string; + hotkey?: { + keys: string; + tab: string; + scope: string; + }[]; + text?: string; + caption?: string; + counter?: number; + }[] = [ + { + value: Tab.CONSUMER, + title: 'Go to consumer [Alt+R]', + hotkey: [ + { + keys: 'alt+r', + tab: Tab.CONSUMER, + scope: 'all', + }, + ], + }, + { + value: Tab.CONTENT, + title: 'Go to content [Alt+C]', + text: isACO ? 'Principal ACL' : undefined, + hotkey: [ + { + keys: 'alt+c', + tab: Tab.CONTENT, + scope: 'all', + }, + ], + }, + { + value: Tab.QUEUE, + title: 'Go to queue [Alt+Q]', + hotkey: [ + { + keys: 'alt+q', + tab: Tab.QUEUE, + scope: 'all', + }, + ], + }, + { + value: Tab.ATTRIBUTES, + title: 'Go to attributes [Alt+A]', + hotkey: [ + { + keys: 'alt+a', + tab: Tab.ATTRIBUTES, + scope: 'all', + }, + ], + caption: 'Attributes', + }, + { + value: Tab.USER_ATTRIBUTES, + title: 'Go to user attributes [Alt+U]', + hotkey: [ + { + keys: 'alt+u', + tab: Tab.USER_ATTRIBUTES, + scope: 'all', + }, + ], + caption: 'User Attributes', + }, + { + value: Tab.MOUNT_CONFIG, + title: 'Go to mount config', + hotkey: [ + { + keys: 'alt+m', + tab: Tab.MOUNT_CONFIG, + scope: 'all', + }, + ], + }, + { + value: Tab.ACL, + title: 'Go to ACL [Alt+P]', + hotkey: [ + { + keys: 'alt+p', + tab: Tab.ACL, + scope: 'all', + }, + ], + caption: 'ACL', + }, + { + value: Tab.ACCESS_LOG, + title: 'Access log', + }, + { + value: Tab.LOCKS, + title: 'Go to locks [Alt+L]', + hotkey: [ + { + keys: 'alt+l', + tab: Tab.LOCKS, + scope: 'all', + }, + ], + counter: ypath.getValue(attributes, '/lock_count'), + }, + { + value: Tab.ANNOTATION, + title: 'Go to annotation [Alt+N]', + hotkey: [ + { + keys: 'alt+n', + tab: Tab.ACL, + scope: 'all', + }, + ], + caption: 'Annotation', + }, + { + value: Tab.SCHEMA, + title: 'Go to schema [Alt+S]', + hotkey: [ + { + keys: 'alt+s', + tab: Tab.SCHEMA, + scope: 'all', + }, + ], + }, + { + value: Tab.TABLETS, + title: 'Go to tablets [Alt+T]', + hotkey: [ + { + keys: 'alt+t', + tab: Tab.TABLETS, + scope: 'all', + }, + ], + }, + { + value: Tab.TABLET_ERRORS, + title: 'Go to tablets errors', + counter: tabletErrorsCount, + }, + ]; + + UIFactory.getNavigationExtraTabs().forEach((extraTab) => { + for (let i = 0; i < tabs.length; i++) { + let indexOffset = 0; + let tabToFind; + + if ('before' in extraTab.position) { + tabToFind = extraTab.position.before; + } -export const getEffectiveMode = createSelector([getMode, getDefaultMode], (mode, defaultMode) => - mode === Tab.AUTO ? defaultMode : mode, + if ('after' in extraTab.position) { + tabToFind = extraTab.position.after; + indexOffset = 1; + } + + if (tabs[i].value === tabToFind) { + const newTab = { + value: extraTab.value, + title: extraTab.title, + hotkey: extraTab.hotkey + ? [{keys: extraTab.hotkey, tab: extraTab.value, scope: 'all'}] + : undefined, + text: extraTab.text, + caption: extraTab.caption, + }; + tabs.splice(i + indexOffset, 0, newTab); + break; + } + } + }); + + return tabs.filter((tab) => supportedTabs.has(tab.value)); + }, ); + +export const getEffectiveMode = createSelector([getMode, getTabs], (mode, tabs) => { + const [firstTab] = tabs; + + return mode === Tab.AUTO ? firstTab.value : mode; +});