Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DM-48644: Create a PrimaryNavigation component #179

Merged
merged 16 commits into from
Feb 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/early-pears-fold.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'squareone': minor
---

Add a configurable Apps menu to the header navigation. This menu is for linking for non-aspect applications within the RSP, such as Times Square.
6 changes: 6 additions & 0 deletions .changeset/friendly-beans-juggle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@lsst-sqre/squared': minor
'squareone': minor
---

Moved auth URLs into Squared as a library. The `getLoginUrl` and `getLogout` URL functions compute the full URLs to the RSP's login and logout endpoints and include the `?rd` query strings to return the user to current and home URL respectively.
5 changes: 5 additions & 0 deletions .changeset/ninety-paws-hammer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lsst-sqre/tsconfig': patch
---

Add DOM to lib options. This makes types available for DOM APIs, which we do use in react libraries like Squared.
5 changes: 5 additions & 0 deletions .changeset/old-phones-cheat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'squareone': patch
---

Reimplement `HeaderNav` using the `PrimaryNavigation` component from Squared. Although the menu looks the same visually, it is now entirely powered by the Radix `NavigationMenu` primitive so that any menu item can be a trigger for a menu rather than a link to another page. The Login / user menu is reimplemented as a menu item rather than with the special GafaelfawrUserMenu component.
5 changes: 5 additions & 0 deletions .changeset/warm-plums-doubt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lsst-sqre/squared': minor
---

Add a new PrimaryNavigation component. This component uses the Radix [NavigationMenu](https://www.radix-ui.com/primitives/docs/components/navigation-menu) primitive and is intended to be a comprehensive solution for the primary navigation in the header of Squareone. The earlier `GafaelfawrUserMenu` component in Squared also uses `NavigationMenu`, but as a single item. With `PrimaryNavigation`, the functionality of `GafaelfawrUserMenu` can be composed into an instance of `PrimaryNavigation`. Like `GafaelfawrMenu`, `PrimaryNavigation` is set up so that menus only appear after clicking on a trigger, rather than on hover. As well, `PrimaryNavigation` ensures the menu is proximate to the trigger (an improvement on the default `NavigationMenu` functionality that centers the menu below the whole navigation element.
30 changes: 30 additions & 0 deletions apps/squareone/squareone.config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,36 @@
"title": "Sentry debug",
"description": "Setting this option to true will print useful information to the console while you're setting up Sentry.",
"default": false
},
"enableAppsMenu": {
"type": "boolean",
"title": "Enable the Apps menu",
"description": "Setting this option to true will enable the Apps menu in the header.",
"default": false
},
"appLinks": {
"type": "array",
"title": "Application links",
"description": "Links to be displayed in the Apps menu",
"items": {
"type": "object",
"required": ["label", "href", "internal"],
"properties": {
"label": {
"type": "string",
"description": "Display label for the link"
},
"href": {
"type": "string",
"description": "URL or path for the link"
},
"internal": {
"type": "boolean",
"description": "Whether this is an internal route or external URL"
}
}
},
"default": []
}
}
}
8 changes: 8 additions & 0 deletions apps/squareone/squareone.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ docsBaseUrl: 'https://rsp.lsst.io'
semaphoreUrl: 'https://data-dev.lsst.cloud/semaphore'
timesSquareUrl: 'http://localhost:3000/times-square/api'
coManageRegistryUrl: 'https://id.lsst.cloud'
enableAppsMenu: true
appLinks:
- label: 'Times Square'
href: '/times-square/'
internal: true
- label: 'Argo CD'
href: '/argo-cd/'
internal: false
apiAspectPageMdx: |
# Rubin Science Platform APIs

Expand Down
58 changes: 58 additions & 0 deletions apps/squareone/src/components/Header/AppsMenu.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useRouter } from 'next/router';
import getConfig from 'next/config';

import { ChevronDown } from 'react-feather';
import { PrimaryNavigation } from '@lsst-sqre/squared';

export default function AppsMenu({}) {
const { publicRuntimeConfig } = getConfig();
const appLinks = publicRuntimeConfig.appLinks || [];

return (
<>
<PrimaryNavigation.Trigger>
Apps <ChevronDown />
</PrimaryNavigation.Trigger>
<PrimaryNavigation.Content>
{appLinks.map((link, index) => (
<PrimaryNavigation.ContentItem key={`${link.href}-${index}`}>
<Link href={link.href} internal={link.internal}>
{link.label}
</Link>
</PrimaryNavigation.ContentItem>
))}
</PrimaryNavigation.Content>
</>
);
}

const Link = ({ href, internal, ...props }) => {
const router = useRouter();
const isActive = href === router.pathname;

// We're implementing our own Link component with an onClick handler because
// if we compose Next.js's Link component we find that Radix Navigation Menu's
// Link component is adding an onClick handler to the Next link that
// causes an error of the sort:
// "onClick" was passed to <Link> with href of /docs but "legacyBehavior"
// was set.
// However, this current implementation is not ideal because it doesn't
// pass through the NavigationMenu's keyboard navigation.
if (internal) {
return (
<PrimaryNavigation.Link active={isActive}>
<span style={{ cursor: 'pointer' }} onClick={() => router.push(href)}>
{props.children}
</span>
</PrimaryNavigation.Link>
);
}

// External links are handled by the PrimaryNavigation.Link component, which
// becomes an <a> tag without any Next onClick handlers.
return (
<PrimaryNavigation.Link active={isActive} href={href}>
{props.children}
</PrimaryNavigation.Link>
);
};
94 changes: 61 additions & 33 deletions apps/squareone/src/components/Header/HeaderNav.js
Original file line number Diff line number Diff line change
@@ -1,66 +1,57 @@
import PropTypes from 'prop-types';
import styled from 'styled-components';
import Link from 'next/link';
import getConfig from 'next/config';
import NextLink from 'next/link';
import { useRouter } from 'next/router';
import { PrimaryNavigation } from '@lsst-sqre/squared';

import useCurrentUrl from '../../hooks/useCurrentUrl';
import Login from './Login';

const StyledNav = styled.nav`
margin: 0 0 30px 0;
padding: 0;
display: flex;
justify-self: end;
width: 100%;
font-size: 1.2rem;
`;

const NavItem = styled.div`
margin: 0 1em;

color: var(--rsd-component-header-nav-text-color);
a {
color: var(--rsd-component-header-nav-text-color);
}

a:hover {
color: var(--rsd-component-header-nav-text-hover-color);
}
`;

const LoginNavItem = styled(NavItem)`
margin: 0 1em 0 auto;
`;
import AppsMenu from './AppsMenu';

/*
* Navigation (within the Header).
*/
export default function HeaderNav() {
const currentUrl = useCurrentUrl();
const { publicRuntimeConfig } = getConfig();

return (
<StyledNav>
<NavItem>
<a href="/portal/app">Portal</a>
<PrimaryNavigation.TriggerLink href="/portal/app">
Portal
</PrimaryNavigation.TriggerLink>
</NavItem>

<NavItem>
<a href="/nb/hub">Notebooks</a>
<PrimaryNavigation.TriggerLink href="/nb/hub">
Notebooks
</PrimaryNavigation.TriggerLink>
</NavItem>

<NavItem>
<Link href="/api-aspect">APIs</Link>
<InternalTriggerLink href="/api-aspect">APIs</InternalTriggerLink>
</NavItem>

{publicRuntimeConfig.enableAppsMenu && (
<NavItem>
<AppsMenu />
</NavItem>
)}

<NavItem>
<Link href="/docs">Documentation</Link>
<InternalTriggerLink href="/docs">Documentation</InternalTriggerLink>
</NavItem>

<NavItem>
<Link href="/support">Support</Link>
<InternalTriggerLink href="/support">Support</InternalTriggerLink>
</NavItem>

<NavItem>
<a href="https://community.lsst.org">Community</a>
<PrimaryNavigation.TriggerLink href="https://community.lsst.org">
Community
</PrimaryNavigation.TriggerLink>
</NavItem>

<LoginNavItem>
Expand All @@ -70,4 +61,41 @@ export default function HeaderNav() {
);
}

const StyledNav = styled(PrimaryNavigation)`
margin: 0 0 30px 0;
padding: 0;
/* display: flex; */
justify-self: end;
width: 100%;
font-size: 1.2rem;
`;

const NavItem = styled(PrimaryNavigation.Item)`
margin: 0 1em;

color: var(--rsd-component-header-nav-text-color);
a {
color: var(--rsd-component-header-nav-text-color);
}

a:hover {
color: var(--rsd-component-header-nav-text-hover-color);
}
`;

const LoginNavItem = styled(NavItem)`
margin: 0 1em 0 auto;
`;

const InternalTriggerLink = ({ href, ...props }) => {
const router = useRouter();
const isActive = href === router.pathname;

return (
<PrimaryNavigation.Trigger asChild active={isActive}>
<NextLink href={href} className="NavigationMenuLink" {...props} />
</PrimaryNavigation.Trigger>
);
};

HeaderNav.propTypes = {};
13 changes: 9 additions & 4 deletions apps/squareone/src/components/Header/Login.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,22 @@

import PropTypes from 'prop-types';

import { getLoginUrl } from '../../lib/utils/url';
import useUserInfo from '../../hooks/useUserInfo';
import UserMenu from './UserMenu';
import { PrimaryNavigation, getLoginUrl } from '@lsst-sqre/squared';

export default function Login({ pageUrl }) {
const { isLoggedIn } = useUserInfo();

if (!isLoggedIn) {
return <a href={getLoginUrl(pageUrl)}>Log in</a>;
if (isLoggedIn === true) {
return <UserMenu pageUrl={pageUrl} />;
}
return <UserMenu pageUrl={pageUrl} />;

return (
<PrimaryNavigation.TriggerLink href={getLoginUrl(pageUrl)}>
Log in
</PrimaryNavigation.TriggerLink>
);
}

Login.propTypes = {
Expand Down
47 changes: 35 additions & 12 deletions apps/squareone/src/components/Header/UserMenu.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,47 @@
/* Menu for a user profile and settings, built on @react/menu-button. */
/* Menu for a user profile and settings. */

import PropTypes from 'prop-types';
import getConfig from 'next/config';
import { GafaelfawrUserMenu } from '@lsst-sqre/squared';
import { ChevronDown } from 'react-feather';
import { PrimaryNavigation } from '@lsst-sqre/squared';
import { useGafaelfawrUser } from '@lsst-sqre/squared';
import { getLogoutUrl } from '@lsst-sqre/squared';

export default function UserMenu({ pageUrl }) {
const { publicRuntimeConfig } = getConfig();
const { coManageRegistryUrl } = publicRuntimeConfig;
const { user } = useGafaelfawrUser();
const logoutUrl = getLogoutUrl(pageUrl);

if (!user) {
return <></>;
}

return (
<GafaelfawrUserMenu currentUrl={pageUrl}>
{coManageRegistryUrl && (
<GafaelfawrUserMenu.Link href={coManageRegistryUrl}>
Account settings
</GafaelfawrUserMenu.Link>
)}
<GafaelfawrUserMenu.Link href="/auth/tokens">
Security tokens
</GafaelfawrUserMenu.Link>
</GafaelfawrUserMenu>
<>
<PrimaryNavigation.Trigger>
{user.username} <ChevronDown />
</PrimaryNavigation.Trigger>
<PrimaryNavigation.Content>
{coManageRegistryUrl && (
<PrimaryNavigation.ContentItem>
<PrimaryNavigation.Link href={coManageRegistryUrl}>
Account settings
</PrimaryNavigation.Link>
</PrimaryNavigation.ContentItem>
)}
<PrimaryNavigation.ContentItem>
<PrimaryNavigation.Link href="/auth/tokens">
Security tokens
</PrimaryNavigation.Link>
</PrimaryNavigation.ContentItem>
<PrimaryNavigation.ContentItem>
<PrimaryNavigation.Link href={logoutUrl}>
Log out
</PrimaryNavigation.Link>
</PrimaryNavigation.ContentItem>
</PrimaryNavigation.Content>
</>
);
}

Expand Down
2 changes: 1 addition & 1 deletion apps/squareone/src/components/HomepageHero/HomepageHero.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const ServiceCard = styled.div`
flex: 1 0 auto;
}
.sticky-footer-container {
flex-shink: 0;
flex-shrink: 0;
margin-top: 2rem;
}
`;
Expand Down
2 changes: 1 addition & 1 deletion apps/squareone/src/components/Page/Page.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const StyledLayout = styled.div`
flex: 1 0 auto;
}
.sticky-footer-container {
flex-shink: 0;
flex-shrink: 0;
}
`;

Expand Down
2 changes: 1 addition & 1 deletion packages/squared/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module.exports = {
root: true,
extends: ['@lsst-sqre/eslint-config', 'plugin:storybook/recommended'],
rules: {
'no-html-link-for-pages': 'off',
'@next/next/no-html-link-for-pages': 'off',
'react/no-unescaped-entities': 'off',
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React from 'react';
import styled from 'styled-components';

import useGafaelfawrUser from '../../hooks/useGafaelfawrUser';
import { getLoginUrl, getLogoutUrl } from './authUrls';
import { getLoginUrl, getLogoutUrl } from '../../lib/authUrls';
import Menu, { MenuLink } from './Menu';

export interface GafaelfawrUserMenuProps {
Expand Down
Loading
Loading