mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 09:22:43 +03:00
console: fix cloud console header active tab highlighting
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/4799 Co-authored-by: Nicolas Beaussart <7281023+beaussan@users.noreply.github.com> GitOrigin-RevId: bc5b1c99f555ec39a43fbad5407461e8419003fa
This commit is contained in:
parent
25bd8b6e0f
commit
27398f40f4
@ -0,0 +1,45 @@
|
|||||||
|
import { getPathRoot } from '../urlUtils';
|
||||||
|
|
||||||
|
describe('getPathRoot', () => {
|
||||||
|
describe('returns empty string with falsy paths', () => {
|
||||||
|
it.each([
|
||||||
|
{
|
||||||
|
path: undefined,
|
||||||
|
expected: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: null,
|
||||||
|
expected: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
expected: '',
|
||||||
|
},
|
||||||
|
])('for %o', ({ path, expected }) => {
|
||||||
|
expect(getPathRoot(path)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('returns the first part of the path', () => {
|
||||||
|
it.each([
|
||||||
|
{
|
||||||
|
path: '/foo/bar/doe',
|
||||||
|
expected: 'foo',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/foo/bar',
|
||||||
|
expected: 'foo',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/foo',
|
||||||
|
expected: 'foo',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
expected: '',
|
||||||
|
},
|
||||||
|
])('for %o', ({ path, expected }) => {
|
||||||
|
expect(getPathRoot(path)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,6 +1,6 @@
|
|||||||
export const getPathRoot = (path: string) => {
|
export const getPathRoot = (path: string | undefined | null = '') =>
|
||||||
return path.split('/')[1];
|
path?.split('/')[1] ?? '';
|
||||||
};
|
|
||||||
export const stripTrailingSlash = (url: string) => {
|
export const stripTrailingSlash = (url: string) => {
|
||||||
if (url && url.endsWith('/')) {
|
if (url && url.endsWith('/')) {
|
||||||
return url.slice(0, -1);
|
return url.slice(0, -1);
|
||||||
|
@ -51,6 +51,7 @@ import {
|
|||||||
setLoveConsentState,
|
setLoveConsentState,
|
||||||
setProClickState,
|
setProClickState,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
import { isBlockActive } from './Main.utils';
|
||||||
|
|
||||||
export const updateRequestHeaders = props => {
|
export const updateRequestHeaders = props => {
|
||||||
const { requestHeaders, dispatch } = props;
|
const { requestHeaders, dispatch } = props;
|
||||||
@ -79,6 +80,56 @@ export const updateRequestHeaders = props => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getSettingsSelectedMarker = pathname => {
|
||||||
|
const currentActiveBlock = getPathRoot(pathname);
|
||||||
|
|
||||||
|
if (currentActiveBlock === 'settings') {
|
||||||
|
return <span className={styles.selected} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSidebarItem = (
|
||||||
|
title,
|
||||||
|
icon,
|
||||||
|
tooltipText,
|
||||||
|
path,
|
||||||
|
appPrefix,
|
||||||
|
pathname,
|
||||||
|
isDefault = false
|
||||||
|
) => {
|
||||||
|
const itemTooltip = <Tooltip id={tooltipText}>{tooltipText}</Tooltip>;
|
||||||
|
const block = getPathRoot(path);
|
||||||
|
|
||||||
|
const isCurrentBlockActive = isBlockActive({
|
||||||
|
blockPath: path,
|
||||||
|
isDefaultBlock: isDefault,
|
||||||
|
pathname,
|
||||||
|
});
|
||||||
|
|
||||||
|
const className = isCurrentBlockActive ? styles.navSideBarActive : '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OverlayTrigger placement="right" overlay={itemTooltip}>
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
className={className}
|
||||||
|
to={appPrefix + path}
|
||||||
|
data-test={`${title.toLowerCase()}-tab-link`}
|
||||||
|
>
|
||||||
|
<div className={styles.iconCenter} data-test={block}>
|
||||||
|
{React.createElement(icon, {
|
||||||
|
'aria-hidden': true,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<p>{title}</p>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
</OverlayTrigger>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
class Main extends React.Component {
|
class Main extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -244,15 +295,15 @@ class Main extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
children,
|
children,
|
||||||
location,
|
|
||||||
migrationModeProgress,
|
|
||||||
currentSchema,
|
|
||||||
serverVersion,
|
|
||||||
metadata,
|
|
||||||
console_opts,
|
console_opts,
|
||||||
|
currentSchema,
|
||||||
currentSource,
|
currentSource,
|
||||||
dispatch,
|
dispatch,
|
||||||
|
metadata,
|
||||||
|
migrationModeProgress,
|
||||||
|
pathname,
|
||||||
schemaList,
|
schemaList,
|
||||||
|
serverVersion,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -262,9 +313,6 @@ class Main extends React.Component {
|
|||||||
|
|
||||||
const appPrefix = '';
|
const appPrefix = '';
|
||||||
|
|
||||||
const currentLocation = location.pathname;
|
|
||||||
const currentActiveBlock = getPathRoot(currentLocation);
|
|
||||||
|
|
||||||
const getMainContent = () => {
|
const getMainContent = () => {
|
||||||
let mainContent = null;
|
let mainContent = null;
|
||||||
|
|
||||||
@ -281,16 +329,6 @@ class Main extends React.Component {
|
|||||||
return mainContent;
|
return mainContent;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSettingsSelectedMarker = () => {
|
|
||||||
let metadataSelectedMarker = null;
|
|
||||||
|
|
||||||
if (currentActiveBlock === 'settings') {
|
|
||||||
metadataSelectedMarker = <span className={styles.selected} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return metadataSelectedMarker;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getMetadataStatusIcon = () => {
|
const getMetadataStatusIcon = () => {
|
||||||
if (metadata.inconsistentObjects.length === 0) {
|
if (metadata.inconsistentObjects.length === 0) {
|
||||||
return <FaCog size="1.3rem" className={styles.question} />;
|
return <FaCog size="1.3rem" className={styles.question} />;
|
||||||
@ -334,41 +372,7 @@ class Main extends React.Component {
|
|||||||
return adminSecretHtml;
|
return adminSecretHtml;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSidebarItem = (
|
const currentActiveBlock = getPathRoot(pathname);
|
||||||
title,
|
|
||||||
icon,
|
|
||||||
tooltipText,
|
|
||||||
path,
|
|
||||||
isDefault = false
|
|
||||||
) => {
|
|
||||||
const itemTooltip = <Tooltip id={tooltipText}>{tooltipText}</Tooltip>;
|
|
||||||
|
|
||||||
const block = getPathRoot(path);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<OverlayTrigger placement="right" overlay={itemTooltip}>
|
|
||||||
<li>
|
|
||||||
<Link
|
|
||||||
className={
|
|
||||||
currentActiveBlock === block ||
|
|
||||||
(isDefault && currentActiveBlock === '')
|
|
||||||
? styles.navSideBarActive
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
to={appPrefix + path}
|
|
||||||
data-test={`${title.toLowerCase()}-tab-link`}
|
|
||||||
>
|
|
||||||
<div className={styles.iconCenter} data-test={block}>
|
|
||||||
{React.createElement(icon, {
|
|
||||||
'aria-hidden': true,
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<p>{title}</p>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
</OverlayTrigger>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
@ -398,6 +402,8 @@ class Main extends React.Component {
|
|||||||
FaFlask,
|
FaFlask,
|
||||||
tooltips.apiExplorer,
|
tooltips.apiExplorer,
|
||||||
'/api/api-explorer',
|
'/api/api-explorer',
|
||||||
|
appPrefix,
|
||||||
|
pathname,
|
||||||
true
|
true
|
||||||
)}
|
)}
|
||||||
{getSidebarItem(
|
{getSidebarItem(
|
||||||
@ -408,25 +414,33 @@ class Main extends React.Component {
|
|||||||
? schemaList.length
|
? schemaList.length
|
||||||
? getSchemaBaseRoute(currentSchema, currentSource)
|
? getSchemaBaseRoute(currentSchema, currentSource)
|
||||||
: getDataSourceBaseRoute(currentSource)
|
: getDataSourceBaseRoute(currentSource)
|
||||||
: '/data'
|
: '/data',
|
||||||
|
appPrefix,
|
||||||
|
pathname
|
||||||
)}
|
)}
|
||||||
{getSidebarItem(
|
{getSidebarItem(
|
||||||
'Actions',
|
'Actions',
|
||||||
FaCogs,
|
FaCogs,
|
||||||
tooltips.actions,
|
tooltips.actions,
|
||||||
'/actions/manage/actions'
|
'/actions/manage/actions',
|
||||||
|
appPrefix,
|
||||||
|
pathname
|
||||||
)}
|
)}
|
||||||
{getSidebarItem(
|
{getSidebarItem(
|
||||||
'Remote Schemas',
|
'Remote Schemas',
|
||||||
FaPlug,
|
FaPlug,
|
||||||
tooltips.remoteSchema,
|
tooltips.remoteSchema,
|
||||||
'/remote-schemas/manage/schemas'
|
'/remote-schemas/manage/schemas',
|
||||||
|
appPrefix,
|
||||||
|
pathname
|
||||||
)}{' '}
|
)}{' '}
|
||||||
{getSidebarItem(
|
{getSidebarItem(
|
||||||
'Events',
|
'Events',
|
||||||
FaCloud,
|
FaCloud,
|
||||||
tooltips.events,
|
tooltips.events,
|
||||||
'/events/data/manage'
|
'/events/data/manage',
|
||||||
|
appPrefix,
|
||||||
|
pathname
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@ -453,7 +467,7 @@ class Main extends React.Component {
|
|||||||
<Link to="/settings">
|
<Link to="/settings">
|
||||||
<div className={styles.headerRightNavbarBtn}>
|
<div className={styles.headerRightNavbarBtn}>
|
||||||
{getMetadataStatusIcon()}
|
{getMetadataStatusIcon()}
|
||||||
{getSettingsSelectedMarker()}
|
{getSettingsSelectedMarker(pathname)}
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
<Help isSelected={currentActiveBlock === 'support'} />
|
<Help isSelected={currentActiveBlock === 'support'} />
|
||||||
@ -496,13 +510,13 @@ const mapStateToProps = (state, ownProps) => {
|
|||||||
return {
|
return {
|
||||||
...state.main,
|
...state.main,
|
||||||
header: state.header,
|
header: state.header,
|
||||||
pathname: ownProps.location.pathname,
|
|
||||||
currentSchema: state.tables.currentSchema,
|
currentSchema: state.tables.currentSchema,
|
||||||
currentSource: state.tables.currentDataSource,
|
currentSource: state.tables.currentDataSource,
|
||||||
metadata: state.metadata,
|
metadata: state.metadata,
|
||||||
console_opts: state.telemetry.console_opts,
|
console_opts: state.telemetry.console_opts,
|
||||||
requestHeaders: state.tables.dataHeaders,
|
requestHeaders: state.tables.dataHeaders,
|
||||||
schemaList: state.tables.schemaList,
|
schemaList: state.tables.schemaList,
|
||||||
|
pathname: ownProps.location.pathname,
|
||||||
inconsistentInheritedRole:
|
inconsistentInheritedRole:
|
||||||
state.tables.modify.permissionsState.inconsistentInhertiedRole,
|
state.tables.modify.permissionsState.inconsistentInhertiedRole,
|
||||||
};
|
};
|
||||||
|
21
console/src/components/Main/Main.utils.ts
Normal file
21
console/src/components/Main/Main.utils.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { getPathRoot } from '../Common/utils/urlUtils';
|
||||||
|
|
||||||
|
type IsBlockActiveArgs = {
|
||||||
|
blockPath: string;
|
||||||
|
pathname: string;
|
||||||
|
isDefaultBlock: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isBlockActive = ({
|
||||||
|
blockPath,
|
||||||
|
isDefaultBlock,
|
||||||
|
pathname,
|
||||||
|
}: IsBlockActiveArgs) => {
|
||||||
|
const block = getPathRoot(blockPath);
|
||||||
|
const currentActiveBlock = getPathRoot(pathname);
|
||||||
|
|
||||||
|
return (
|
||||||
|
currentActiveBlock === block ||
|
||||||
|
(isDefaultBlock && currentActiveBlock === '')
|
||||||
|
);
|
||||||
|
};
|
25
console/src/components/Main/__tests__/Main.utils.spec.ts
Normal file
25
console/src/components/Main/__tests__/Main.utils.spec.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { isBlockActive } from '../Main.utils';
|
||||||
|
|
||||||
|
describe('isBlockActive', () => {
|
||||||
|
it.each([
|
||||||
|
{
|
||||||
|
blockPath: '/api/api-explorer',
|
||||||
|
isDefaultBlock: true,
|
||||||
|
pathname: '/',
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
blockPath: '/data/default',
|
||||||
|
isDefaultBlock: false,
|
||||||
|
pathname: '/data/default',
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
])('for %o', ({ blockPath, isDefaultBlock, pathname, expected }) => {
|
||||||
|
const currentActiveBlock = isBlockActive({
|
||||||
|
blockPath,
|
||||||
|
isDefaultBlock,
|
||||||
|
pathname,
|
||||||
|
});
|
||||||
|
expect(currentActiveBlock).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
@ -6,7 +6,7 @@ export interface ConsoleConfig {
|
|||||||
type: string;
|
type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getEnvVarAsString = (value: string) => {
|
export const getEnvVarAsString = (value: string) => {
|
||||||
if (!value || value === 'undefined') return undefined;
|
if (!value || value === 'undefined') return undefined;
|
||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user