Skip to content

Commit e583a03

Browse files
feat: update breadcrumbs (#432)
1 parent ef2b353 commit e583a03

File tree

17 files changed

+407
-139
lines changed

17 files changed

+407
-139
lines changed

src/components/NodeHostWrapper/NodeHostWrapper.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ export const NodeHostWrapper = ({node, getNodeRef}: NodeHostWrapperProps) => {
2626

2727
const isNodeAvailable = !isUnavailableNode(node);
2828
const nodeRef = isNodeAvailable && getNodeRef ? getNodeRef(node) + 'internal' : undefined;
29+
const nodePath = isNodeAvailable
30+
? getDefaultNodePath(node.NodeId, {
31+
tenantName: node.TenantName,
32+
})
33+
: undefined;
2934

3035
return (
3136
<div className={b()}>
@@ -39,7 +44,7 @@ export const NodeHostWrapper = ({node, getNodeRef}: NodeHostWrapperProps) => {
3944
<EntityStatus
4045
name={node.Host}
4146
status={node.SystemState}
42-
path={isNodeAvailable ? getDefaultNodePath(node.NodeId) : undefined}
47+
path={nodePath}
4348
hasClipboardButton
4449
className={b('host')}
4550
/>

src/components/Tablet/Tablet.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,32 @@ const b = cn('tablet');
1414

1515
interface TabletProps {
1616
tablet?: TTabletStateInfo;
17+
tenantName?: string;
1718
}
1819

19-
export const Tablet = ({tablet = {}}: TabletProps) => {
20-
const {TabletId: id} = tablet;
20+
export const Tablet = ({tablet = {}, tenantName}: TabletProps) => {
21+
const {TabletId: id, NodeId, Type, State} = tablet;
2122
const status = tablet.Overall?.toLowerCase();
2223

24+
const tabletPath =
25+
id &&
26+
createHref(
27+
routes.tablet,
28+
{id},
29+
{
30+
nodeId: NodeId,
31+
type: Type,
32+
state: State,
33+
tenantName,
34+
},
35+
);
36+
2337
return (
2438
<ContentWithPopup
2539
className={b('wrapper')}
2640
content={<TabletTooltipContent data={tablet} className={b('popup-content')} />}
2741
>
28-
<InternalLink to={id && createHref(routes.tablet, {id})}>
42+
<InternalLink to={tabletPath}>
2943
<div className={b({status})}>
3044
<div className={b('type')}>{[getTabletLabel(tablet.Type)]}</div>
3145
</div>

src/containers/App/Content.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react';
2-
import {Switch, Route, Redirect, Router} from 'react-router-dom';
2+
import {Switch, Route, Redirect, Router, useLocation} from 'react-router-dom';
33
import cn from 'bem-cn-lite';
44
import {connect} from 'react-redux';
55

@@ -28,6 +28,8 @@ import {clusterTabsIds} from '../Cluster/utils';
2828
const b = cn('app');
2929

3030
export function Content(props) {
31+
const location = useLocation();
32+
3133
const {singleClusterMode} = props;
3234
const isClustersPage =
3335
location.pathname.includes('/clusters') ||
@@ -54,7 +56,7 @@ export function Content(props) {
5456
};
5557
return (
5658
<React.Fragment>
57-
{!isClustersPage && <Header clusterName={props.clusterName} />}
59+
{!isClustersPage && <Header mainPage={props.mainPage} />}
5860
<main className={b('main')}>{renderRoute()}</main>
5961
<ReduxTooltip />
6062
<AppIcons />
@@ -66,6 +68,7 @@ Content.propTypes = {
6668
singleClusterMode: PropTypes.bool,
6769
children: PropTypes.node,
6870
clusterName: PropTypes.string,
71+
mainPage: PropTypes.object,
6972
};
7073

7174
function ContentWrapper(props) {

src/containers/Cluster/Cluster.tsx

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type {AdditionalClusterProps, AdditionalVersionsProps} from '../../types/
1010
import type {AdditionalNodesInfo} from '../../utils/nodes';
1111
import routes from '../../routes';
1212

13-
import {setHeader} from '../../store/reducers/header';
13+
import {setHeaderBreadcrumbs} from '../../store/reducers/header/header';
1414
import {getClusterInfo} from '../../store/reducers/cluster/cluster';
1515
import {getClusterNodes} from '../../store/reducers/clusterNodes/clusterNodes';
1616
import {parseNodesToVersionsValues, parseVersionsToVersionToColorMap} from '../../utils/versions';
@@ -80,14 +80,7 @@ function Cluster({
8080
);
8181

8282
useEffect(() => {
83-
dispatch(
84-
setHeader([
85-
{
86-
text: Name || 'Cluster',
87-
link: getClusterPath(),
88-
},
89-
]),
90-
);
83+
dispatch(setHeaderBreadcrumbs('cluster', {}));
9184
}, [dispatch, Name]);
9285

9386
const versionToColor = useMemo(() => {

src/containers/Header/Header.scss

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,13 @@
1313
border-bottom: 1px solid var(--yc-color-line-generic);
1414

1515
@include body2-typography;
16+
17+
&__breadcrumb {
18+
display: flex;
19+
align-items: center;
20+
21+
&__icon {
22+
margin-right: 3px;
23+
}
24+
}
1625
}

src/containers/Header/Header.tsx

Lines changed: 68 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
1+
import {useEffect, useMemo} from 'react';
2+
import {useHistory, useLocation} from 'react-router';
3+
import {useDispatch} from 'react-redux';
14
import block from 'bem-cn-lite';
2-
import {useHistory} from 'react-router';
35

4-
import {Breadcrumbs, BreadcrumbsItem} from '@gravity-ui/uikit';
6+
import {Breadcrumbs, Icon} from '@gravity-ui/uikit';
57

68
import {ExternalLinkWithIcon} from '../../components/ExternalLinkWithIcon/ExternalLinkWithIcon';
79

810
import {backend, customBackend} from '../../store';
9-
import {HeaderItemType} from '../../store/reducers/header';
11+
import {getClusterInfo} from '../../store/reducers/cluster/cluster';
1012
import {useTypedSelector} from '../../utils/hooks';
1113
import {DEVELOPER_UI} from '../../utils/constants';
14+
import {parseQuery} from '../../routes';
15+
16+
import {RawBreadcrumbItem, getBreadcrumbs} from './breadcrumbs';
1217

1318
import './Header.scss';
1419

@@ -22,30 +27,80 @@ const getInternalLink = (singleClusterMode: boolean) => {
2227
return backend + '/internal';
2328
};
2429

25-
function Header() {
26-
const {singleClusterMode, header}: {singleClusterMode: boolean; header: HeaderItemType[]} =
27-
useTypedSelector((state) => state);
30+
interface HeaderProps {
31+
mainPage?: RawBreadcrumbItem;
32+
}
2833

34+
function Header({mainPage}: HeaderProps) {
35+
const dispatch = useDispatch();
2936
const history = useHistory();
37+
const location = useLocation();
3038

31-
const renderHeader = () => {
32-
const breadcrumbItems = header.reduce((acc, el) => {
39+
const singleClusterMode = useTypedSelector((state) => state.singleClusterMode);
40+
const {page, pageBreadcrumbsOptions} = useTypedSelector((state) => state.header);
41+
const {data} = useTypedSelector((state) => state.cluster);
42+
43+
const queryParams = parseQuery(location);
44+
45+
const clusterNameFromQuery = queryParams.clusterName?.toString();
46+
47+
const clusterNameFinal = data?.Name || clusterNameFromQuery;
48+
49+
useEffect(() => {
50+
dispatch(getClusterInfo(clusterNameFromQuery));
51+
}, [dispatch, clusterNameFromQuery]);
52+
53+
const breadcrumbItems = useMemo(() => {
54+
const rawBreadcrumbs: RawBreadcrumbItem[] = [];
55+
let options = pageBreadcrumbsOptions;
56+
57+
if (mainPage) {
58+
rawBreadcrumbs.push(mainPage);
59+
}
60+
61+
if (clusterNameFinal) {
62+
options = {
63+
...pageBreadcrumbsOptions,
64+
clusterName: clusterNameFinal,
65+
};
66+
}
67+
68+
const breadcrumbs = getBreadcrumbs(page, options, rawBreadcrumbs, queryParams);
69+
70+
return breadcrumbs.map((item) => {
3371
const action = () => {
34-
if (el.link) {
35-
history.push(el.link);
72+
if (item.link) {
73+
history.push(item.link);
3674
}
3775
};
38-
acc.push({text: el.text, action});
39-
return acc;
40-
}, [] as BreadcrumbsItem[]);
76+
return {...item, action};
77+
});
78+
}, [clusterNameFinal, mainPage, history, queryParams, page, pageBreadcrumbsOptions]);
4179

80+
const renderHeader = () => {
4281
return (
4382
<header className={b()}>
4483
<div>
4584
<Breadcrumbs
4685
items={breadcrumbItems}
4786
lastDisplayedItemsCount={1}
4887
firstDisplayedItemsCount={1}
88+
renderItemContent={({icon, text}) => {
89+
if (!icon) {
90+
return text;
91+
}
92+
return (
93+
<span className={b('breadcrumb')}>
94+
<Icon
95+
width={16}
96+
height={16}
97+
data={icon}
98+
className={b('breadcrumb__icon')}
99+
/>
100+
{text}
101+
</span>
102+
);
103+
}}
49104
/>
50105
</div>
51106

src/containers/Header/breadcrumbs.ts

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import nodesRightIcon from '@gravity-ui/icons/svgs/nodes-right.svg';
2+
import databaseIcon from '@gravity-ui/icons/svgs/database.svg';
3+
4+
import type {
5+
BreadcrumbsOptions,
6+
ClusterBreadcrumbsOptions,
7+
NodeBreadcrumbsOptions,
8+
Page,
9+
TabletBreadcrumbsOptions,
10+
TabletsBreadcrumbsOptions,
11+
TenantBreadcrumbsOptions,
12+
} from '../../store/reducers/header/types';
13+
import routes, {createHref} from '../../routes';
14+
15+
import {getClusterPath} from '../Cluster/utils';
16+
import {getTenantPath} from '../Tenant/TenantPages';
17+
import {getDefaultNodePath} from '../Node/NodePages';
18+
19+
const prepareTenantName = (tenantName: string) => {
20+
return tenantName.startsWith('/') ? tenantName.slice(1) : tenantName;
21+
};
22+
23+
export interface RawBreadcrumbItem {
24+
text: string;
25+
link?: string;
26+
icon?: SVGIconData;
27+
}
28+
29+
const getClusterBreadcrumbs = (
30+
options: ClusterBreadcrumbsOptions,
31+
query = {},
32+
): RawBreadcrumbItem[] => {
33+
const {clusterName, clusterTab} = options;
34+
35+
return [
36+
{
37+
text: clusterName || 'Cluster',
38+
link: getClusterPath(clusterTab, query),
39+
icon: nodesRightIcon,
40+
},
41+
];
42+
};
43+
44+
const getTenantBreadcrumbs = (
45+
options: TenantBreadcrumbsOptions,
46+
query = {},
47+
): RawBreadcrumbItem[] => {
48+
const {tenantName} = options;
49+
50+
const text = tenantName ? prepareTenantName(tenantName) : 'Tenant';
51+
const link = tenantName ? getTenantPath({...query, name: tenantName}) : undefined;
52+
53+
return [...getClusterBreadcrumbs(options, query), {text, link, icon: databaseIcon}];
54+
};
55+
56+
const getNodeBreadcrumbs = (options: NodeBreadcrumbsOptions, query = {}): RawBreadcrumbItem[] => {
57+
const {tenantName, nodeId} = options;
58+
59+
let breadcrumbs: RawBreadcrumbItem[];
60+
61+
// Compute nodes have tenantName, storage nodes doesn't
62+
const isStorageNode = !tenantName;
63+
64+
if (isStorageNode) {
65+
breadcrumbs = getClusterBreadcrumbs(options, query);
66+
} else {
67+
breadcrumbs = getTenantBreadcrumbs(options, query);
68+
}
69+
70+
const text = nodeId ? `Node ${nodeId}` : 'Node';
71+
const link = nodeId ? getDefaultNodePath(nodeId, query) : undefined;
72+
73+
breadcrumbs.push({text, link});
74+
75+
return breadcrumbs;
76+
};
77+
78+
const getTabletsBreadcrubms = (
79+
options: TabletsBreadcrumbsOptions,
80+
query = {},
81+
): RawBreadcrumbItem[] => {
82+
const {tenantName, nodeIds, state, type} = options;
83+
84+
let breadcrumbs: RawBreadcrumbItem[];
85+
86+
// Cluster system tablets don't have tenantName
87+
if (tenantName) {
88+
breadcrumbs = getTenantBreadcrumbs(options, query);
89+
} else {
90+
breadcrumbs = getClusterBreadcrumbs(options, query);
91+
}
92+
93+
const link = createHref(routes.tabletsFilters, undefined, {
94+
nodeIds,
95+
state,
96+
type,
97+
path: tenantName,
98+
});
99+
100+
breadcrumbs.push({text: 'Tablets', link});
101+
102+
return breadcrumbs;
103+
};
104+
105+
const getTabletBreadcrubms = (
106+
options: TabletBreadcrumbsOptions,
107+
query = {},
108+
): RawBreadcrumbItem[] => {
109+
const {tabletId} = options;
110+
111+
const breadcrumbs = getTabletsBreadcrubms(options, query);
112+
113+
breadcrumbs.push({
114+
text: tabletId || 'Tablet',
115+
});
116+
117+
return breadcrumbs;
118+
};
119+
120+
export const getBreadcrumbs = (
121+
page: Page,
122+
options: BreadcrumbsOptions,
123+
rawBreadcrumbs: RawBreadcrumbItem[] = [],
124+
query = {},
125+
) => {
126+
switch (page) {
127+
case 'cluster': {
128+
return [...rawBreadcrumbs, ...getClusterBreadcrumbs(options, query)];
129+
}
130+
case 'tenant': {
131+
return [...rawBreadcrumbs, ...getTenantBreadcrumbs(options, query)];
132+
}
133+
case 'node': {
134+
return [...rawBreadcrumbs, ...getNodeBreadcrumbs(options, query)];
135+
}
136+
case 'tablets': {
137+
return [...rawBreadcrumbs, ...getTabletsBreadcrubms(options, query)];
138+
}
139+
case 'tablet': {
140+
return [...rawBreadcrumbs, ...getTabletBreadcrubms(options, query)];
141+
}
142+
default: {
143+
return rawBreadcrumbs;
144+
}
145+
}
146+
};

0 commit comments

Comments
 (0)