Skip to content

Commit

Permalink
Merge pull request #166 from lsst-sqre/tickets/DM-44392
Browse files Browse the repository at this point in the history
DM-44392: Drop use of Reach UI
  • Loading branch information
jonathansick authored Jun 20, 2024
2 parents f54eede + 6ae78ca commit 78a2306
Show file tree
Hide file tree
Showing 20 changed files with 495 additions and 353 deletions.
5 changes: 5 additions & 0 deletions .changeset/hungry-monkeys-laugh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'squareone': minor
---

Usage of Reach UI is now removed and replaced with Radix UI. The user menu now uses `GafaelfawrUserMenu` from `@lsst-sqre/squared` and is based on Radix UI's Navigation Menu component. It is customized here to work with the Gafaelawr API to show a log in button for the logged out state, and to show the user's menu with a default log out button for the logged in state. Previously we also used Reach UI for showing an accessible validation alert in the Times Square page parameters UI. For now we've dropped this functionality.
5 changes: 5 additions & 0 deletions .changeset/loud-books-develop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lsst-sqre/squared': minor
---

Created GafaelfawrUserMenu based on the Radix UI [navigation-menu](https://www.radix-ui.com/primitives/docs/components/navigation-menu) component. That's the right primitive for an accessible menu that uses `<a>` or Next `Link` elements. The existing Gafaelfawr menu is now `GafaelfawrUserDropdown` for reference (it is based on Radix UI's [dropdown menu](https://www.radix-ui.com/primitives/docs/components/dropdown-menu), but is more appropriate as a menu of buttons.
2 changes: 0 additions & 2 deletions apps/squareone/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,6 @@
"@lsst-sqre/rubin-style-dictionary": "workspace:*",
"@lsst-sqre/squared": "workspace:*",
"@microsoft/fetch-event-source": "^2.0.1",
"@reach/alert": "^0.17.0",
"@reach/menu-button": "^0.17.0",
"ajv": "^8.11.0",
"date-fns": "^3.6.0",
"formik": "^2.2.9",
Expand Down
81 changes: 11 additions & 70 deletions apps/squareone/src/components/Header/UserMenu.js
Original file line number Diff line number Diff line change
@@ -1,83 +1,24 @@
/* Menu for a user profile and settings, built on @react/menu-button. */

import PropTypes from 'prop-types';
import styled from 'styled-components';
import getConfig from 'next/config';
import { Menu, MenuList, MenuButton, MenuLink } from '@reach/menu-button';
import '@reach/menu-button/styles.css';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';

import { getLogoutUrl } from '../../lib/utils/url';
import useUserInfo from '../../hooks/useUserInfo';

const StyledMenuButton = styled(MenuButton)`
background-color: transparent;
color: var(--rsd-component-header-nav-text-color);
border: none;
&:hover {
color: var(--rsd-component-header-nav-text-hover-color);
}
`;

const StyledFontAwesomeIcon = styled(FontAwesomeIcon)`
margin-left: 0.25em;
font-size: 0.8em;
opacity: 0.9;
`;

const StyledMenuList = styled(MenuList)`
font-size: 1rem;
background-color: var(--rsd-component-header-nav-menulist-background-color);
width: 12rem;
border-radius: 0.5rem;
// Top margin is related to the triangle; see :before
margin-top: 10px;
color: var(--rsd-component-header-nav-menulist-text-color);
a {
color: var(--rsd-component-header-nav-menulist-text-color);
}
&:before {
// Make a CSS triangle on the top of the menu
content: '';
border: 8px solid transparent;
border-bottom: 8px solid
var(--rsd-component-header-nav-menulist-background-color);
position: absolute;
display: inline-block;
// Top is related to the border size and margin-top of menu list
top: -5px;
right: 9px;
left: auto;
}
[data-reach-menu-item][data-selected] {
background: var(
--rsd-component-header-nav-menulist-selected-background-color
);
}
`;
import { GafaelfawrUserMenu } from '@lsst-sqre/squared';

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

return (
<Menu>
<StyledMenuButton>
{userInfo.username} <StyledFontAwesomeIcon icon="angle-down" />
</StyledMenuButton>
<StyledMenuList>
{coManageRegistryUrl && (
<MenuLink href={coManageRegistryUrl}>Account settings</MenuLink>
)}
<MenuLink href="/auth/tokens">Security tokens</MenuLink>
<MenuLink href={logoutUrl}>Log out</MenuLink>
</StyledMenuList>
</Menu>
<GafaelfawrUserMenu currentUrl={pageUrl}>
{coManageRegistryUrl && (
<GafaelfawrUserMenu.Link href={coManageRegistryUrl}>
Account Settings
</GafaelfawrUserMenu.Link>
)}
<GafaelfawrUserMenu.Link href="/auth/tokens">
Security tokens
</GafaelfawrUserMenu.Link>
</GafaelfawrUserMenu>
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import styled from 'styled-components';
import Alert from '@reach/alert';

export default function ParameterInput({
children,
Expand Down Expand Up @@ -33,7 +32,7 @@ const ParameterName = styled.p`
'Courier New', monospace;
`;

const ErrorMessage = styled(Alert)`
const ErrorMessage = styled.p`
color: red;
margin-top: 0.2em;
margin-bottom: 0.2em;
Expand Down
2 changes: 1 addition & 1 deletion apps/squareone/src/lib/mocks/devstate.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// the POST /api/dev/logout method.

let DEV_STATE = {
loggedIn: false,
loggedIn: true,
username: 'vera',
name: 'Vera Rubin',
uid: 1234,
Expand Down
1 change: 1 addition & 0 deletions packages/squared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@lsst-sqre/global-css": "workspace:*",
"@lsst-sqre/rubin-style-dictionary": "workspace:*",
"@radix-ui/react-dropdown-menu": "^2.0.5",
"@radix-ui/react-navigation-menu": "^1.1.4",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-feather": "^2.0.10",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import type { Meta, StoryObj } from '@storybook/react';
import { rest } from 'msw';
import { SWRConfig } from 'swr';
import { within, userEvent, screen } from '@storybook/testing-library';
import { expect } from '@storybook/jest';

import GafaelfawrUserDropdown from './GafaelfawrUserDropdown';

const meta: Meta<typeof GafaelfawrUserDropdown> = {
title: 'Components/GafaelfawrUserDropdown',
component: GafaelfawrUserDropdown,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout
layout: 'centered',
// The user menu always shows up on a dark background.
backgrounds: {
default: 'dark',
values: [{ name: 'dark', value: '#1f2121' }],
},
},
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs
tags: ['autodocs'],
};

export default meta;
type Story = StoryObj<typeof GafaelfawrUserDropdown>;

const loggedInAuthHandlers = [
rest.get('/auth/api/v1/user-info', (req, res, ctx) => {
return res(
ctx.json({
username: 'someuser',
name: 'Alice Example',
email: 'alice@example.com',
uid: 4123,
gid: 4123,
groups: [
{
name: 'g_special_users',
id: 123181,
},
],
quota: {
api: {},
notebook: {
cpu: 4,
memory: 16,
},
},
})
);
}),
];

const loggedOutAuthHandlers = [
rest.get('/auth/api/v1/user-info', (req, res, ctx) => {
return res(ctx.status(401));
}),
];

export const Default: Story = {
args: {
currentUrl: 'http://localhost:6006/somepage',
},

parameters: {
msw: {
handlers: {
auth: loggedInAuthHandlers,
},
},
},

render: (args) => (
<SWRConfig value={{ provider: () => new Map() }}>
<GafaelfawrUserDropdown {...args}>
<GafaelfawrUserDropdown.Item>
<a href="#">Account Settings</a>
</GafaelfawrUserDropdown.Item>
<GafaelfawrUserDropdown.Item>
<a href="#">Security tokens</a>
</GafaelfawrUserDropdown.Item>
</GafaelfawrUserDropdown>
</SWRConfig>
),
};

export const LoggedOut: Story = {
args: { ...Default.args },

parameters: {
msw: {
handlers: {
auth: loggedOutAuthHandlers,
},
},
},
};

export const OpenedMenu: Story = {
args: { ...Default.args },

parameters: { ...Default.parameters },

play: async ({ canvasElement }) => {
// Delay so msw can load
const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
await delay(1000);

const canvas = within(canvasElement);
await userEvent.click(canvas.getByRole('button'));
// Using screen rather than canvas because Radix renders the dropdown
// outside the scope of the storybook canvas.
await expect(screen.getByText('Log out')).toBeInTheDocument();
await expect(screen.getByText('Log out')).toHaveAttribute(
'href',
'http://localhost:6006/logout?rd=http%3A%2F%2Flocalhost%3A6006%2F'
);
},

render: (args) => (
<SWRConfig value={{ provider: () => new Map() }}>
<GafaelfawrUserDropdown {...args}>
<GafaelfawrUserDropdown.Item>
<a href="#">Account Settings</a>
</GafaelfawrUserDropdown.Item>
<GafaelfawrUserDropdown.Item>
<a href="#">Security tokens</a>
</GafaelfawrUserDropdown.Item>
</GafaelfawrUserDropdown>
</SWRConfig>
),
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React from 'react';

import styled from 'styled-components';

import Menu from './Menu';
import Separator from './Separator';
import MenuItem from './MenuItem';
import { getLoginUrl, getLogoutUrl } from './authUrls';
import useGafaelfawrUser from '../../hooks/useGafaelfawrUser';

export interface GafaelfawrUserDropdownProps {
children: React.ReactNode;
/**
* The URL of the current page. Used to construct the login and logout URLs
* with appropriate redirects.
*/
currentUrl: string;
}

export const GafaelfawrUserDropdown = ({
children,
currentUrl,
}: GafaelfawrUserDropdownProps) => {
const { user, isLoggedIn } = useGafaelfawrUser();
// TODO: it'd be nice to integrate the useCurrentUrl hook into
// this component so the user doesn't have to pass this prop.
const logoutUrl = getLogoutUrl(currentUrl);
const loginUrl = getLoginUrl(currentUrl);
if (isLoggedIn && user) {
return (
<Menu logoutHref={logoutUrl} username={user.username}>
{children}
</Menu>
);
} else {
return <SiteNavLink href={loginUrl}>Log in / Sign up</SiteNavLink>;
}
};

const SiteNavLink = styled.a`
color: var(--rsd-component-header-nav-text-color);
&:hover {
color: var(--rsd-component-header-nav-text-hover-color);
}
`;

// Associate child components with the parent for easier imports.
GafaelfawrUserDropdown.Item = MenuItem;
GafaelfawrUserDropdown.Separator = Separator;

export default GafaelfawrUserDropdown;
Loading

0 comments on commit 78a2306

Please sign in to comment.