• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

CBIIT / crdc-datahub-ui / 14666777147

25 Apr 2025 02:19PM UTC coverage: 62.713%. First build
14666777147

Pull #691

github

web-flow
Merge 8956c2f3e into cbeb99a86
Pull Request #691: CRDCDH-2594 Add support for auth permission scoping

3452 of 5899 branches covered (58.52%)

Branch coverage included in aggregate %.

68 of 84 new or added lines in 17 files covered. (80.95%)

4776 of 7221 relevant lines covered (66.14%)

196.49 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

56.22
/src/components/Header/components/NavbarDesktop.tsx
1
import React, { useEffect, useState, useRef } from "react";
2
import { NavLink, Link, useNavigate, useLocation } from "react-router-dom";
3
import { Button, styled } from "@mui/material";
4
import { useAuthContext } from "../../Contexts/AuthContext";
5
import GenericAlert from "../../GenericAlert";
6
import { HeaderLinks, HeaderSubLinks } from "../../../config/HeaderConfig";
7
import APITokenDialog from "../../APITokenDialog";
8
import UploaderToolDialog from "../../UploaderToolDialog";
9
import { Entity, hasPermission, Permissions } from "../../../config/AuthPermissions";
10

11
const Nav = styled("div")({
2✔
12
  top: 0,
13
  left: 0,
14
  width: "100%",
15
  background: "#ffffff",
16
  boxShadow: "-0.1px 6px 9px -6px rgba(0, 0, 0, 0.5)",
17
  zIndex: 1100,
18
  position: "relative",
19
  "& .dropdownContainer": {
20
    margin: "0 auto",
21
    position: "relative",
22
    width: "1400px",
23
  },
24
  "& .loggedInName": {
25
    color: "#007BBD",
26
    textAlign: "right",
27
    fontSize: "14px",
28
    fontFamily: "Poppins",
29
    fontStyle: "normal",
30
    fontWeight: 600,
31
    lineHeight: "normal",
32
    letterSpacing: "0.42px",
33
    textDecoration: "none",
34
    textTransform: "uppercase",
35
    padding: "10px 0",
36
    marginBottom: "4.5px",
37
    marginRight: "40px",
38
  },
39
  "& .invisible": {
40
    visibility: "hidden",
41
  },
42
});
43

44
const NavContainer = styled("div")({
2✔
45
  margin: "0 auto",
46
  maxWidth: "1400px",
47
  textAlign: "left",
48
  position: "relative",
49
  display: "flex",
50
  justifyContent: "space-between",
51
  alignItems: "end",
52
  "#navbar-dropdown-name-container": {
53
    margin: 0,
54
  },
55
});
56

57
const UlContainer = styled("ul")({
2✔
58
  listStyle: "none",
59
  margin: 0,
60
  paddingTop: "17px",
61
  paddingLeft: "11px",
62
  display: "flex",
63
  width: "100%",
64
});
65

66
const LiSection = styled("li")({
2✔
67
  display: "inline-block",
68
  position: "relative",
69
  lineHeight: "50px",
70
  letterSpacing: "1px",
71
  textAlign: "center",
72
  transition: "all 0.3s ease-in-out",
73
  "& a": {
74
    color: "#585C65",
75
    textDecoration: "none",
76
  },
77
  "& .displayName": {
78
    color: "#007BBD",
79
    fontSize: "14px",
80
    lineHeight: "20px",
81
    padding: "10px 0",
82
    textAlign: "right",
83
    width: "fit-content",
84
  },
85
  "&.name-dropdown-li": {
86
    marginLeft: "auto",
87
  },
88
  "&.login-button": {
89
    lineHeight: "48px",
90
  },
91
  "& .navTitle": {
92
    display: "block",
93
    color: "#585C65",
94
    fontFamily: "poppins",
95
    fontSize: "17px",
96
    fontWeight: 600,
97
    lineHeight: "40px",
98
    letterSpacing: "normal",
99
    textDecoration: "none",
100
    margin: "0 5px",
101
    padding: "0 8px",
102
    userSelect: "none",
103
    borderTop: "4px solid transparent",
104
    borderLeft: "4px solid transparent",
105
    borderRight: "4px solid transparent",
106
    "&:hover": {
107
      cursor: "pointer",
108
    },
109
  },
110
  "& .navText": {
111
    borderBottom: "4px solid transparent",
112
    width: "fit-content",
113
    margin: "auto",
114
    "&:hover": {
115
      cursor: "pointer",
116
      color: "#3A75BD",
117
      borderBottom: "4px solid #3A75BD",
118
      "&::after": {
119
        content: '""',
120
        display: "inline-block",
121
        width: "6px",
122
        height: "6px",
123
        borderBottom: "1px solid #298085",
124
        borderLeft: "1px solid #298085",
125
        margin: "0 0 4px 8px",
126
        transform: "rotate(-45deg)",
127
        WebkitTransform: "rotate(-45deg)",
128
      },
129
    },
130
    "&::after": {
131
      content: '""',
132
      display: "inline-block",
133
      width: "6px",
134
      height: "6px",
135
      borderBottom: "1px solid #585C65",
136
      borderLeft: "1px solid #585C65",
137
      margin: "0 0 4px 8px",
138
      transform: "rotate(-45deg)",
139
      WebkitTransform: "rotate(-45deg)",
140
    },
141
  },
142
  "& .clicked": {
143
    color: "#FFFFFF",
144
    background: "#1F4671",
145
    "&::after": {
146
      borderTop: "1px solid #FFFFFF",
147
      borderRight: "1px solid #FFFFFF",
148
      borderBottom: "0",
149
      borderLeft: "0",
150
      margin: "0 0 0 8px",
151
    },
152
    "&:hover": {
153
      borderBottom: "4px solid #1F4671",
154
      color: "#FFFFFF",
155
      "&::after": {
156
        content: '""',
157
        display: "inline-block",
158
        width: "6px",
159
        height: "6px",
160
        borderTop: "1px solid #FFFFFF",
161
        borderRight: "1px solid #FFFFFF",
162
        borderBottom: "0",
163
        borderLeft: "0",
164
        margin: "0 0 0 8px",
165
        transform: "rotate(-45deg)",
166
        WebkitTransform: "rotate(-45deg)",
167
      },
168
    },
169
  },
170
  "& .directLink::after": {
171
    display: "none",
172
  },
173
  "& .directLink:hover::after": {
174
    display: "none",
175
  },
176
  "& .shouldBeUnderlined": {
177
    borderBottom: "4px solid #3A75BD !important",
178
  },
179
  "& .navTitleClicked": {
180
    display: "block",
181
    color: "#FFFFFF",
182
    fontFamily: "poppins",
183
    fontSize: "17px",
184
    fontWeight: 600,
185
    lineHeight: "40px",
186
    letterSpacing: "normal",
187
    textDecoration: "none",
188
    margin: "0 5px",
189
    padding: "0 8px",
190
    userSelect: "none",
191
    background: "#1F4671",
192
    borderTop: "4px solid #5786FF",
193
    borderLeft: "4px solid #5786FF",
194
    borderRight: "4px solid #5786FF",
195
  },
196
  "& .invisible": {
197
    visibility: "hidden",
198
  },
199
});
200

201
const Dropdown = styled("div")({
2✔
202
  top: "60.5px",
203
  left: 0,
204
  width: "100%",
205
  background: "#1F4671",
206
  zIndex: 1100,
207
  position: "absolute",
208
});
209

210
const NameDropdownContainer = styled("div")({
2✔
211
  margin: "0 auto",
212
  textAlign: "left",
213
  position: "relative",
214
  maxWidth: "1400px",
215
  "& .dropdownList": {
216
    background: "#1F4671",
217
    display: "inline-flex",
218
    gridTemplateColumns: "repeat(auto-fit, minmax(250px, 1fr))",
219
    padding: "32px 32px 0 32px",
220
  },
221
  "& .dropdownItem": {
222
    padding: "0 10px 52px 10px",
223
    textAlign: "left",
224
    fontFamily: "'Poppins', sans-serif",
225
    fontStyle: "normal",
226
    fontWeight: 600,
227
    fontSize: "20px",
228
    lineHeight: "110%",
229
    color: "#FFFFFF",
230
    textDecoration: "none",
231
    cursor: "pointer",
232
    "&:hover": {
233
      textDecoration: "underline",
234
    },
235
  },
236
  "& .dropdownItemButton": {
237
    textTransform: "none",
238
    paddingLeft: "20px",
239
    paddingRight: "20px",
240
    "&:hover": {
241
      background: "transparent",
242
    },
243
  },
244
  "#navbar-dropdown-item-name-logout": {
245
    maxWidth: "200px",
246
  },
247
});
248

249
const StyledLoginLink = styled(Link)({
2✔
250
  color: "#007BBD !important",
251
  textAlign: "right",
252
  fontSize: "14px",
253
  fontFamily: "Poppins",
254
  fontStyle: "normal",
255
  fontWeight: 600,
256
  lineHeight: "normal",
257
  letterSpacing: "0.42px",
258
  textDecoration: "none",
259
  textTransform: "uppercase",
260
  padding: "10px 0",
261
  marginBottom: "4.5px",
262
  marginRight: "32px",
263
});
264

265
const useOutsideAlerter = (ref1: React.RefObject<HTMLDivElement>) => {
2✔
266
  useEffect(() => {
12✔
267
    function handleClickOutside(event) {
268
      if (
×
269
        !event.target ||
×
270
        (event.target.getAttribute("class") !== "dropdownList" &&
271
          ref1.current &&
272
          !ref1.current.contains(event.target))
273
      ) {
274
        const toggle = document.getElementsByClassName("navText clicked");
×
275
        if (toggle[0] && !event.target.getAttribute("class")?.includes("navText clicked")) {
×
276
          const temp: HTMLElement = toggle[0] as HTMLElement;
×
277
          temp.click();
×
278
        }
279
      }
280
    }
281

282
    document.addEventListener("mousedown", handleClickOutside);
12✔
283
    return () => {
12✔
284
      document.removeEventListener("mousedown", handleClickOutside);
12✔
285
    };
286
  }, [ref1]);
287
};
288

289
const NavBar = () => {
2✔
290
  const { isLoggedIn, user, logout } = useAuthContext();
12✔
291
  const navigate = useNavigate();
12✔
292
  const location = useLocation();
12✔
293

294
  const [clickedTitle, setClickedTitle] = useState("");
12✔
295
  const [openAPITokenDialog, setOpenAPITokenDialog] = useState<boolean>(false);
12✔
296
  const [uploaderToolOpen, setUploaderToolOpen] = useState<boolean>(false);
12✔
297
  const [showLogoutAlert, setShowLogoutAlert] = useState<boolean>(false);
12✔
298
  const [restorePath, setRestorePath] = useState<string>(null);
12✔
299
  const dropdownSelection = useRef<HTMLDivElement>(null);
12✔
300

301
  const clickableObject = HeaderLinks.filter(
12✔
302
    (item) => item.className === "navMobileItem clickable"
72✔
303
  );
304
  const clickableTitle = clickableObject.map((item) => item.name);
24✔
305
  const displayName = user?.firstName?.toUpperCase() || "N/A";
12✔
306

307
  clickableTitle.push(displayName);
12✔
308

309
  HeaderSubLinks[displayName] = [
12✔
310
    {
311
      name: "User Profile",
312
      link: `/profile/${user?._id}`,
313
      id: "navbar-dropdown-item-user-profile",
314
      className: "navMobileSubItem",
315
    },
316
    {
317
      name: "Uploader CLI Tool",
318
      onClick: () => setUploaderToolOpen(true),
×
319
      id: "navbar-dropdown-item-uploader-tool",
320
      className: "navMobileSubItem action",
321
    },
322
    {
323
      name: "API Token",
324
      onClick: () => setOpenAPITokenDialog(true),
×
325
      id: "navbar-dropdown-item-api-token",
326
      className: "navMobileSubItem action",
327
      permissions: ["data_submission:create"],
328
    },
329
    {
330
      name: "Manage Studies",
331
      link: "/studies",
332
      id: "navbar-dropdown-item-studies-manage",
333
      className: "navMobileSubItem",
334
      permissions: ["study:manage"],
335
    },
336
    {
337
      name: "Manage Programs",
338
      link: "/programs",
339
      id: "navbar-dropdown-item-program-manage",
340
      className: "navMobileSubItem",
341
      permissions: ["program:manage"],
342
    },
343
    {
344
      name: "Manage Institutions",
345
      link: "/institutions",
346
      id: "navbar-dropdown-item-institution-manage",
347
      className: "navMobileSubItem",
348
      permissions: ["institution:manage"],
349
    },
350
    {
351
      name: "Manage Users",
352
      link: "/users",
353
      id: "navbar-dropdown-item-user-manage",
354
      className: "navMobileSubItem",
355
      permissions: ["user:manage"],
356
    },
357
    {
358
      name: "Logout",
359
      onClick: () => handleLogout(),
×
360
      id: "navbar-dropdown-item-logout",
361
      className: "navMobileSubItem action",
362
    },
363
  ];
364

365
  const handleLogout = async () => {
12✔
366
    setClickedTitle("");
×
367
    const logoutStatus = await logout();
×
368
    if (logoutStatus) {
×
369
      navigate("/");
×
370
      setShowLogoutAlert(true);
×
371
      setTimeout(() => setShowLogoutAlert(false), 10000);
×
372
    }
373
  };
374

375
  const handleMenuClick = (e) => {
12✔
376
    if (e.target.textContent === clickedTitle || !clickableTitle.includes(e.target.textContent)) {
×
377
      setClickedTitle("");
×
378
    } else {
379
      setClickedTitle(e.target.textContent);
×
380
    }
381
  };
382

383
  const onKeyPressHandler = (e) => {
12✔
384
    if (e.key === "Enter") {
×
385
      handleMenuClick(e);
×
386
    }
387
  };
388

389
  const shouldBeUnderlined = (item) => {
12✔
390
    const linkName = item.name;
60✔
391
    const correctPath = window.location.pathname;
60✔
392
    if (item.className === "navMobileItem") {
60✔
393
      return correctPath === item.link;
36✔
394
    }
395
    if (HeaderSubLinks[linkName] === undefined) {
24!
396
      return false;
×
397
    }
398
    const linkNames = Object.values(HeaderSubLinks[linkName]).map((e: NavBarSubItem) => e.link);
108✔
399
    return linkNames.includes(correctPath);
24✔
400
  };
401

402
  const checkPermissions = (permissions: AuthPermissions[]) => {
12✔
403
    if (!permissions?.length) {
72✔
404
      return true; // No permissions required
60✔
405
    }
406

407
    return permissions.every((permission) => {
12✔
408
      const [entityRaw, actionRaw] = permission.split(":", 2);
12✔
409

410
      if (!entityRaw || !actionRaw) {
12!
NEW
411
        return false;
×
412
      }
413

414
      const entity = entityRaw as Entity;
12✔
415
      const action = actionRaw as Permissions[Entity]["action"];
12✔
416

417
      return hasPermission(user, entity, action, { onlyKey: true });
12✔
418
    });
419
  };
420

421
  useOutsideAlerter(dropdownSelection);
12✔
422

423
  useEffect(() => {
12✔
424
    if (!isLoggedIn) {
12✔
425
      setClickedTitle("");
10✔
426
    }
427
  }, [isLoggedIn]);
428

429
  useEffect(() => {
12✔
430
    setClickedTitle("");
12✔
431
  }, []);
432

433
  useEffect(() => {
12✔
434
    if (!location?.pathname || location?.pathname === "/") {
12!
435
      setRestorePath(null);
12✔
436
      return;
12✔
437
    }
438

439
    setRestorePath(location?.pathname);
×
440
  }, [location]);
441

442
  return (
12✔
443
    <Nav>
444
      <GenericAlert open={showLogoutAlert}>
445
        <span>You have been logged out.</span>
446
      </GenericAlert>
447
      <NavContainer>
448
        <UlContainer>
449
          {HeaderLinks.map((navItem: NavBarItem) => {
450
            const hasEveryPermission = checkPermissions(navItem.permissions);
72✔
451
            if (!hasEveryPermission) {
72✔
452
              return null;
12✔
453
            }
454

455
            return (
60✔
456
              <LiSection key={navItem.id}>
457
                {navItem.className === "navMobileItem" ? (
30✔
458
                  <div className="navTitle directLink">
459
                    <NavLink
460
                      to={navItem.link}
461
                      target={navItem.link.startsWith("https://") ? "_blank" : "_self"}
18✔
462
                    >
463
                      <div
464
                        id={navItem.id}
465
                        role="button"
466
                        tabIndex={0}
467
                        className={`navText directLink ${
468
                          shouldBeUnderlined(navItem) ? "shouldBeUnderlined" : ""
18!
469
                        }`}
470
                        onKeyDown={onKeyPressHandler}
471
                        onClick={handleMenuClick}
472
                      >
473
                        {navItem.name}
474
                      </div>
475
                    </NavLink>
476
                  </div>
477
                ) : (
478
                  <div className={clickedTitle === navItem.name ? "navTitleClicked" : "navTitle"}>
12!
479
                    <div
480
                      id={navItem.id}
481
                      role="button"
482
                      tabIndex={0}
483
                      className={`${
484
                        clickedTitle === navItem.name ? "navText clicked" : "navText"
12!
485
                      } ${shouldBeUnderlined(navItem) ? "shouldBeUnderlined" : ""}`}
12!
486
                      onKeyDown={onKeyPressHandler}
487
                      onClick={handleMenuClick}
488
                    >
489
                      {navItem.name}
490
                    </div>
491
                  </div>
492
                )}
493
              </LiSection>
494
            );
495
          })}
496
          <LiSection className={`name-dropdown-li${isLoggedIn ? "" : " login-button"}`}>
6✔
497
            {isLoggedIn ? (
6✔
498
              <div
499
                id="navbar-dropdown-name-container"
500
                className={clickedTitle === displayName ? "navTitleClicked" : "navTitle"}
1!
501
              >
502
                <div
503
                  id="navbar-dropdown-name"
504
                  role="button"
505
                  tabIndex={0}
506
                  className={
507
                    clickedTitle === displayName
1!
508
                      ? "navText displayName clicked"
509
                      : "navText displayName"
510
                  }
511
                  onKeyDown={onKeyPressHandler}
512
                  onClick={handleMenuClick}
513
                >
514
                  {displayName}
515
                </div>
516
              </div>
517
            ) : (
518
              <StyledLoginLink
519
                id="header-navbar-login-button"
520
                to="/login"
521
                state={{ redirectState: restorePath }}
522
              >
523
                Login
524
              </StyledLoginLink>
525
            )}
526
          </LiSection>
527
        </UlContainer>
528
      </NavContainer>
529
      <Dropdown ref={dropdownSelection} className={clickedTitle === "" ? "invisible" : ""}>
6!
530
        <NameDropdownContainer>
531
          <div className="dropdownList">
532
            {clickedTitle !== ""
6!
533
              ? HeaderSubLinks[clickedTitle]?.map((dropItem) => {
NEW
534
                  const hasEveryPermission = checkPermissions(dropItem?.permissions);
×
NEW
535
                  if (!hasEveryPermission) {
×
536
                    return null;
×
537
                  }
538

539
                  if (dropItem.link) {
×
540
                    return (
×
541
                      <span className="dropdownItem" key={dropItem.id}>
542
                        <Link
543
                          target={
544
                            dropItem.link.startsWith("https://") || dropItem.link.endsWith(".pdf")
×
545
                              ? "_blank"
546
                              : "_self"
547
                          }
548
                          id={dropItem.id}
549
                          to={dropItem.link}
550
                          className="dropdownItem"
551
                          onClick={() => setClickedTitle("")}
×
552
                        >
553
                          {dropItem.name}
554
                          {dropItem.text && <div className="dropdownItemText">{dropItem.text}</div>}
×
555
                        </Link>
556
                      </span>
557
                    );
558
                  }
559

560
                  if (dropItem.onClick) {
×
561
                    return (
×
562
                      <Button
563
                        id={dropItem.id}
564
                        key={dropItem.id}
565
                        className="dropdownItem dropdownItemButton"
566
                        onClick={dropItem.onClick}
567
                      >
568
                        {dropItem.name}
569
                      </Button>
570
                    );
571
                  }
572

573
                  return null;
×
574
                })
575
              : null}
576
          </div>
577
        </NameDropdownContainer>
578
      </Dropdown>
579
      <APITokenDialog open={openAPITokenDialog} onClose={() => setOpenAPITokenDialog(false)} />
×
580
      <UploaderToolDialog open={uploaderToolOpen} onClose={() => setUploaderToolOpen(false)} />
×
581
    </Nav>
582
  );
583
};
584

585
export default NavBar;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc