Skip to content

Commit e0131a4

Browse files
authored
Merge pull request #5854 from fhlavac/react_menubar
Vertical navbar
2 parents aa9be06 + 4eb3f7d commit e0131a4

File tree

14 files changed

+1534
-197
lines changed

14 files changed

+1534
-197
lines changed

app/assets/stylesheets/patternfly_overrides.scss

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1078,3 +1078,230 @@ table.table-compressed tr td .piechart {
10781078
table.c3-tooltip td {
10791079
background-color: #393f44;
10801080
}
1081+
1082+
.nav-pf-vertical {
1083+
.menu-list-group-item {
1084+
border-bottom: 1px solid rgb(3, 3, 3);
1085+
border-top: 1px solid rgb(3, 3, 3);
1086+
box-sizing: border-box;
1087+
padding: 0;
1088+
margin-bottom: -1px;
1089+
position: relative;
1090+
&:first-child {
1091+
border-top: none;
1092+
}
1093+
a {
1094+
background-color: transparent;
1095+
color: #d1d1d1;
1096+
cursor: pointer;
1097+
display: block;
1098+
font-size: 14px;
1099+
font-weight: 400;
1100+
height: 63px;
1101+
line-height: 26px;
1102+
padding: 17px 20px 17px 25px;
1103+
position: relative;
1104+
white-space: nowrap;
1105+
width: 225px;
1106+
text-decoration: none;
1107+
.pficon, .fa {
1108+
color: #72767b;
1109+
float: left;
1110+
font-size: 20px;
1111+
line-height: 26px;
1112+
margin-right: 10px;
1113+
text-align: center;
1114+
width: 24px;
1115+
}
1116+
}
1117+
.list-group-item-value {
1118+
flex: 1;
1119+
max-width: none;
1120+
padding-right: 15px;
1121+
line-height: 25px;
1122+
overflow: hidden;
1123+
text-overflow: ellipsis;
1124+
}
1125+
}
1126+
.menu-list-group-item.active {
1127+
a {
1128+
background-color: #393f44;
1129+
color: #fff;
1130+
font-weight: 600;
1131+
.pficon, .fa {
1132+
color: #39a5dc;
1133+
}
1134+
}
1135+
a:before {
1136+
background: #39a5dc;
1137+
content: " ";
1138+
height: 100%;
1139+
left: 0;
1140+
position: absolute;
1141+
top: 0;
1142+
width: 3px;
1143+
}
1144+
.nav-pf-secondary-nav {
1145+
.nav-item-pf-header {
1146+
a {
1147+
::before {
1148+
opacity: 1;
1149+
}
1150+
}
1151+
}
1152+
}
1153+
}
1154+
.menu-list-group-item:hover {
1155+
a {
1156+
background-color: #393f44;
1157+
color: #fff;
1158+
font-weight: 600;
1159+
width: calc(225px + 1px);
1160+
z-index: 1031;
1161+
.pficon, .fa {
1162+
color: #39a5dc;
1163+
}
1164+
}
1165+
}
1166+
.nav-pf-secondary-nav {
1167+
.nav-item-pf-header {
1168+
a {
1169+
::before {
1170+
font-family: "FontAwesome";
1171+
content: '\f08d';
1172+
color: #0099d3;
1173+
margin-right: 7px;
1174+
opacity: 0;
1175+
}
1176+
&.collapsed {
1177+
::before {
1178+
transform: rotate(45deg);
1179+
display: inline-block;
1180+
}
1181+
}
1182+
color: #fff;
1183+
opacity: 1;
1184+
font-size: 16px;
1185+
font-family: "Open Sans", Helvetica, Arial, sans-serif;
1186+
font-weight: 400;
1187+
cursor: pointer;
1188+
padding: 0;
1189+
height: auto;
1190+
background: transparent;
1191+
z-index: 0;
1192+
}
1193+
}
1194+
.menu-list-group-item {
1195+
border: none;
1196+
padding: 0 0 5px 0;
1197+
width: 225px;
1198+
margin-bottom: -1px;
1199+
&.tertiary-nav-item-pf {
1200+
a {
1201+
::after {
1202+
color: #72767b;
1203+
content: "\f105";
1204+
display: block;
1205+
font-family: "FontAwesome";
1206+
font-size: 20px;
1207+
line-height: 20px;
1208+
padding: 0;
1209+
position: absolute;
1210+
right: 20px;
1211+
top: 4px;
1212+
}
1213+
}
1214+
}
1215+
a {
1216+
background-color: #393f44;
1217+
color: #d1d1d1;
1218+
font-size: 12px;
1219+
font-weight: inherit;
1220+
height: inherit;
1221+
margin-left: 20px;
1222+
width: calc(225px - 20px);
1223+
display: flex;
1224+
opacity: 1;
1225+
padding: 4px 0 2px 0;
1226+
.list-group-item-value {
1227+
padding-left: 5px;
1228+
flex: 1;
1229+
max-width: none;
1230+
padding-right: 15px;
1231+
}
1232+
}
1233+
}
1234+
.menu-list-group-item.active, .menu-list-group-item:hover {
1235+
a {
1236+
background-color: #4d5258;
1237+
color: #fff;
1238+
font-weight: 600;
1239+
}
1240+
}
1241+
a:before {
1242+
display: none;
1243+
}
1244+
.menu-list-group-item:hover {
1245+
a {
1246+
color: #fff;
1247+
font-weight: 600;
1248+
}
1249+
}
1250+
}
1251+
1252+
.nav-pf-tertiary-nav{
1253+
.nav-item-pf-header {
1254+
a {
1255+
font-size: 16px;
1256+
font-weight: 400 !important;
1257+
margin: 0;
1258+
padding: 0;
1259+
background-color: #4d5258;
1260+
color: #fff;
1261+
::after {
1262+
display: none !important;
1263+
}
1264+
}
1265+
}
1266+
.menu-list-group-item {
1267+
a {
1268+
background-color: #4d5258;
1269+
span {
1270+
font-weight: 400;
1271+
color: #fff;
1272+
}
1273+
::after {
1274+
display: none !important;
1275+
}
1276+
}
1277+
}
1278+
.menu-list-group-item.active, .menu-list-group-item:hover {
1279+
a {
1280+
background-color: #393f44;
1281+
span {
1282+
font-weight: 600;
1283+
}
1284+
}
1285+
}
1286+
}
1287+
1288+
&.collapsed {
1289+
.menu-list-group-item, .menu-list-group-item.active {
1290+
>a.top-level {
1291+
>.list-group-item-value {
1292+
display: none;
1293+
}
1294+
padding: 17px 0 17px 25px;
1295+
width: 75px;
1296+
&::after {
1297+
right: 10px;
1298+
}
1299+
}
1300+
>a.top-level:hover {
1301+
width: calc(75px + 1px);
1302+
}
1303+
}
1304+
}
1305+
}
1306+
1307+
Lines changed: 49 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,64 @@
11
module ApplicationHelper
22
module Navbar
3+
def menu_to_json(placement = :default)
4+
structure = []
5+
Menu::Manager.menu(placement) do |menu_section|
6+
next unless menu_section.visible?
7+
8+
structure << item_to_hash(menu_section)
9+
end
10+
structure
11+
end
12+
13+
def item_to_hash(item)
14+
{
15+
:id => item.id,
16+
:title => item.name,
17+
:iconClass => item.icon,
18+
:href => item.link_params[:href],
19+
:type => item.type,
20+
:preventHref => !item.href,
21+
:visible => item.visible?,
22+
:active => item_active?(item),
23+
:items => item.items.to_a.map(&method(:item_to_hash))
24+
}
25+
end
26+
327
# FIXME: The 'active' below is an active section not an item. That is wrong.
428
# What works is the "legacy" part that compares @layout to item.id.
529
# This assumes that these matches -- @layout and item.id. Moving forward we
630
# need to remove that assumption. However to do that we need figure some way
731
# to identify the active menu item here.
8-
def item_nav_class(item)
9-
active = controller.menu_section_id(controller.params) || @layout.to_sym
32+
def item_active?(item)
33+
if item.leaf?
34+
# FIXME: remove @layout condition when every controller sets menu_section properly
35+
active = controller.menu_section_id(controller.params) || @layout.to_sym
36+
item.id.to_sym == active || item.id.to_sym == @layout.to_sym
37+
else
38+
return section_nav_class_iframe(item) if params[:action] == 'iframe'
1039

11-
# FIXME: remove @layout condition when every controller sets menu_section properly
12-
item.id.to_sym == active || item.id.to_sym == @layout.to_sym ? 'active' : nil
13-
end
40+
active = controller.menu_section_id(controller.params) || @layout.to_sym
1441

15-
# special handling for custom menu sections and items
16-
def section_nav_class_iframe(section)
17-
if params[:sid].present?
18-
section.id.to_s == params[:sid] ? 'active' : nil
19-
elsif params[:id].present?
20-
section.contains_item_id?(params[:id]) ? 'active' : nil
21-
end
22-
end
23-
24-
def section_nav_class(section)
25-
return section_nav_class_iframe(section) if params[:action] == 'iframe'
26-
27-
active = controller.menu_section_id(controller.params) || @layout.to_sym
42+
if item.parent.nil?
43+
# first-level, fallback to old logic for now
44+
# FIXME: exception behavior to remove
45+
active = 'my_tasks' if %w[my_tasks all_tasks].include?(@layout)
46+
active = 'cloud_volume' if @layout == 'cloud_volume_snapshot' || @layout == 'cloud_volume_backup'
47+
active = 'cloud_object_store_container' if @layout == 'cloud_object_store_object'
48+
active = active.to_sym
49+
end
2850

29-
if section.parent.nil?
30-
# first-level, fallback to old logic for now
31-
# FIXME: exception behavior to remove
32-
active = 'my_tasks' if %w[my_tasks all_tasks].include?(@layout)
33-
active = 'cloud_volume' if @layout == 'cloud_volume_snapshot' || @layout == 'cloud_volume_backup'
34-
active = 'cloud_object_store_container' if @layout == 'cloud_object_store_object'
35-
active = active.to_sym
51+
# FIXME: remove to_s, to_sym once all items use symbol ids
52+
item.id.to_sym == active ||
53+
item.contains_item_id?(active.to_s) ||
54+
item.contains_item_id?(active.to_sym)
3655
end
56+
end
3757

38-
return 'active' if section.id.to_sym == active
39-
40-
# FIXME: remove to_s, to_sym once all items use symbol ids
41-
return 'active' if section.contains_item_id?(active.to_s)
42-
return 'active' if section.contains_item_id?(active.to_sym)
58+
# special handling for custom menu sections and items
59+
def section_nav_class_iframe(section)
60+
params[:sid].present? && section.id.to_s == params[:sid] ||
61+
params[:id].present? && section.contains_item_id?(params[:id])
4362
end
4463
end
4564
end
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
export const getHrefByType = (type, href, id) => {
2+
switch (type) {
3+
case 'big_iframe':
4+
return `/dashboard/iframe?id=${id}`;
5+
case 'modal':
6+
return undefined;
7+
default:
8+
return href;
9+
}
10+
};
11+
12+
export const getTargetByType = type => (type === 'new_window' ? '_blank' : '_self');
13+
14+
export const getItemId = id => `menu_item_${id}`;
15+
16+
export const getSectionId = id => `menu_section_${id}`;
17+
18+
export const getIdByCategory = (isSection, id) => (isSection ? getSectionId(id) : getItemId(id));
19+
20+
export const handleUnsavedChanges = (type) => {
21+
if (type === 'modal') {
22+
return sendDataWithRx({ type: 'showAboutModal' });
23+
}
24+
return window.miqCheckForChanges();
25+
};
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import { Grid } from 'patternfly-react';
4+
import TopLevel from './top-level';
5+
import SecondLevel from './second-level';
6+
import ThirdLevel from './third-level';
7+
import { menuProps, RecursiveMenuProps } from './recursive-props';
8+
9+
const Fallback = props => <ThirdLevel level={2} {...props} />;
10+
11+
const getLevelComponent = level => ({
12+
0: props => <TopLevel level={level} {...props} />,
13+
1: props => <SecondLevel level={level} {...props} />,
14+
})[level] || Fallback;
15+
16+
export const MenuItem = ({ level, ...props }) => getLevelComponent(level)(props);
17+
18+
const MainMenu = ({ menu }) => (
19+
<Grid className="top-navbar">
20+
<div className="nav-pf-vertical nav-pf-vertical-with-sub-menus nav-pf-vertical-collapsible-menus">
21+
<ul className="list-group" id="maintab">
22+
{menu.map(props => <MenuItem key={props.id} level={0} {...props} />)}
23+
</ul>
24+
</div>
25+
</Grid>
26+
);
27+
28+
MainMenu.propTypes = {
29+
menu: PropTypes.arrayOf(PropTypes.shape({
30+
...menuProps,
31+
items: PropTypes.arrayOf(PropTypes.shape({
32+
...menuProps,
33+
items: PropTypes.arrayOf(PropTypes.shape(RecursiveMenuProps())),
34+
})),
35+
})).isRequired,
36+
};
37+
38+
export default MainMenu;

0 commit comments

Comments
 (0)