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-44392: Drop use of Reach UI #166

Merged
merged 13 commits into from
Jun 20, 2024
Merged
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: '[email protected]',
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
Loading