Skip to content

feat: add auth fallback logic for when official Coder components are not mounted #128

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

Merged
merged 58 commits into from
May 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
9db4173
wip: commit progress on fallback UI
Parkreiner May 14, 2024
50d9008
chore: move dep to peer dependencies
Parkreiner May 14, 2024
c57d4c9
wip: commit more progress
Parkreiner May 15, 2024
e567260
wip: more progress
Parkreiner May 15, 2024
61ee528
refactor: consolidate card logic
Parkreiner May 15, 2024
9b16cc2
fix: update component tracking hooks
Parkreiner May 15, 2024
344593d
fix: add a11y landmark to auth fallback
Parkreiner May 15, 2024
2041f17
wip: commit more style progress
Parkreiner May 15, 2024
43cfd52
wip: commit more progress
Parkreiner May 15, 2024
560d009
wip: more progress
Parkreiner May 17, 2024
98faa12
wip: cleanup current approach
Parkreiner May 17, 2024
a73b1b1
wip: commit progress on observer approach
Parkreiner May 17, 2024
0011dd2
wip: fix infinite loop for mutation logic
Parkreiner May 17, 2024
b8bc1ef
fix: prevent padding patches from firing too often
Parkreiner May 17, 2024
2112995
fix: improve scoping of style overrides
Parkreiner May 17, 2024
d04274c
chore: finish intial version of fallback stylling
Parkreiner May 17, 2024
ae85984
fix: tidy up types
Parkreiner May 17, 2024
6e18204
wip: create initial version of dialog form
Parkreiner May 17, 2024
f097f9b
wip: commit progress on modal
Parkreiner May 20, 2024
e1a70dd
chore: finish styling for modal wrapper
Parkreiner May 21, 2024
c4101f6
fix: update padding for FormDialog
Parkreiner May 21, 2024
f8bb852
wip: start extracting out auth form
Parkreiner May 21, 2024
98c96af
fix: add missing barrel export file
Parkreiner May 21, 2024
a404ddb
fix: make sure that auth form isn't dismissed early
Parkreiner May 21, 2024
af2f383
fix: update auth imports
Parkreiner May 21, 2024
5b04628
fix: update spacing for auth modal
Parkreiner May 22, 2024
8ae3d14
refactor: clean up auth provider for clarity
Parkreiner May 22, 2024
1b1f8c4
docs: rewrite comment for clarity
Parkreiner May 22, 2024
1ea39e0
fix: improve granularity between official Coder components and user c…
Parkreiner May 22, 2024
9236ca4
fix: update all internal consumers of useCoderAuth
Parkreiner May 22, 2024
b90ebd0
wip: commit initial version of useCoderQuery helper hook
Parkreiner May 22, 2024
12a7b3e
refactor: rename hooks to avoid confusion
Parkreiner May 22, 2024
c146788
fix: update exports for plugin
Parkreiner May 22, 2024
e7e7755
docs: fill in incomplete sentence
Parkreiner May 22, 2024
8493080
wip: commit initial version of useMutation wrapper
Parkreiner May 22, 2024
8087e19
refactor: extract retry factor into global constant
Parkreiner May 22, 2024
20fa328
merge
Parkreiner May 22, 2024
ca257ae
fix: add explicit return type to useCoderMutation
Parkreiner May 22, 2024
a92ec05
wip: start extracting auth logic into better reusable components
Parkreiner May 22, 2024
028c6b7
fix: update card to have better styling for body
Parkreiner May 22, 2024
aea9345
wip: commit progress on style refactoring
Parkreiner May 22, 2024
ceb65e1
fix: update vertical padding for card wrapper
Parkreiner May 22, 2024
8d1343f
chore: delete CoderAuthWrapper component
Parkreiner May 22, 2024
91e0702
fix: update styling for auth fallback
Parkreiner May 22, 2024
b644eec
chore: shrink size of PR
Parkreiner May 22, 2024
b127195
fix: update imports
Parkreiner May 22, 2024
7118896
docs: add comment about description setup
Parkreiner May 22, 2024
b364e0c
fix: remove risk of runtime render errors in auth form
Parkreiner May 23, 2024
39f9a06
fix: update imports
Parkreiner May 23, 2024
1148eae
fix: update font sizes to use relative units
Parkreiner May 23, 2024
5aca4f7
fix: update peer dependencies for react-dom
Parkreiner May 23, 2024
b04482e
refactor: clean up auth revalidation logic
Parkreiner May 23, 2024
063006c
wip: start updating tests for new code changes
Parkreiner May 23, 2024
1702755
fix: adding missing test case for auth card
Parkreiner May 23, 2024
1d928bb
wip: commit progress on auth form test updates
Parkreiner May 23, 2024
34f027f
fix: removal vetigal properties
Parkreiner May 23, 2024
e0a4760
fix: get all CoderAuthForm tests passing
Parkreiner May 23, 2024
15a4e22
fix: update import for auth hook in test
Parkreiner May 23, 2024
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
3 changes: 2 additions & 1 deletion plugins/backstage-plugin-coder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@
"valibot": "^0.28.1"
},
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0 || ^18.0.0"
"react": "^16.13.1 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needed to bring in React DOM as a peer dependency so I could start using React portals

},
"devDependencies": {
"@backstage/cli": "^0.25.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* @file A slightly different take on Backstage's official InfoCard component,
* with better support for accessibility.
*
* Does not support all of InfoCard's properties just yet.
*/
import React, { type HTMLAttributes, type ReactNode, forwardRef } from 'react';
import { makeStyles } from '@material-ui/core';

export type A11yInfoCardProps = Readonly<
HTMLAttributes<HTMLDivElement> & {
headerContent?: ReactNode;
}
>;

const useStyles = makeStyles(theme => ({
root: {
color: theme.palette.type,
backgroundColor: theme.palette.background.paper,
padding: theme.spacing(2),
borderRadius: theme.shape.borderRadius,
boxShadow: theme.shadows[1],
},

headerContent: {
// Ideally wouldn't be using hard-coded font sizes, but couldn't figure out
// how to use the theme.typography property, especially since not all
// sub-properties have font sizes defined
fontSize: '1.5rem',
color: theme.palette.text.primary,
fontWeight: 700,
borderBottom: `1px solid ${theme.palette.divider}`,

// Margins and padding are a bit wonky to support full-bleed layouts
marginLeft: `-${theme.spacing(2)}px`,
marginRight: `-${theme.spacing(2)}px`,
padding: `0 ${theme.spacing(2)}px ${theme.spacing(2)}px`,
},
}));

// Card should be treated as equivalent to Backstage's official InfoCard
// component; had to make custom version so that it could forward properties for
// accessibility/screen reader support
export const A11yInfoCard = forwardRef<HTMLDivElement, A11yInfoCardProps>(
(props, ref) => {
const { className, children, headerContent, ...delegatedProps } = props;
const styles = useStyles();

return (
<div
ref={ref}
className={`${styles.root} ${className ?? ''}`}
{...delegatedProps}
>
{headerContent !== undefined && (
<div className={styles.headerContent}>{headerContent}</div>
)}

{children}
</div>
);
},
);
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './A11yInfoCard';
27 changes: 0 additions & 27 deletions plugins/backstage-plugin-coder/src/components/Card/Card.tsx

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import React from 'react';
import { CoderLogo } from '../CoderLogo';
import { LinkButton } from '@backstage/core-components';
import { makeStyles } from '@material-ui/core';
import { useCoderAuth } from '../CoderProvider';
import { UnlinkAccountButton } from './UnlinkAccountButton';

const useStyles = makeStyles(theme => ({
root: {
Expand Down Expand Up @@ -31,8 +30,6 @@ const useStyles = makeStyles(theme => ({

export const CoderAuthDistrustedForm = () => {
const styles = useStyles();
const { ejectToken } = useCoderAuth();

return (
<div className={styles.root}>
<div>
Expand All @@ -43,18 +40,7 @@ export const CoderAuthDistrustedForm = () => {
</p>
</div>

<LinkButton
disableRipple
to=""
component="button"
type="submit"
color="primary"
variant="contained"
className={styles.button}
onClick={ejectToken}
>
Eject token
</LinkButton>
<UnlinkAccountButton className={styles.button} />
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,14 @@ import {
mockAuthStates,
mockCoderAuthToken,
} from '../../testHelpers/mockBackstageData';
import { CoderAuthWrapper } from './CoderAuthWrapper';
import { CoderAuthForm } from './CoderAuthForm';
import { renderInTestApp } from '@backstage/test-utils';

type RenderInputs = Readonly<{
authStatus: CoderAuthStatus;
childButtonText?: string;
}>;

async function renderAuthWrapper({
authStatus,
childButtonText = 'Default button text',
}: RenderInputs) {
async function renderAuthWrapper({ authStatus }: RenderInputs) {
const ejectToken = jest.fn();
const registerNewToken = jest.fn();

Expand All @@ -40,67 +36,36 @@ async function renderAuthWrapper({
*/
const renderOutput = await renderInTestApp(
<CoderProviderWithMockAuth appConfig={mockAppConfig} auth={auth}>
<CoderAuthWrapper type="card">
<button>{childButtonText}</button>
</CoderAuthWrapper>
<CoderAuthForm />
</CoderProviderWithMockAuth>,
);

return { ...renderOutput, ejectToken, registerNewToken };
}

describe(`${CoderAuthWrapper.name}`, () => {
describe('Displaying main content', () => {
it('Displays the main children when the user is authenticated', async () => {
const buttonText = 'I have secret Coder content!';
renderAuthWrapper({
authStatus: 'authenticated',
childButtonText: buttonText,
});

const button = await screen.findByRole('button', { name: buttonText });

// This assertion isn't necessary because findByRole will throw an error
// if the button can't be found within the expected period of time. Doing
// this purely to make the Backstage linter happy
expect(button).toBeInTheDocument();
});
});

describe(`${CoderAuthForm.name}`, () => {
describe('Loading UI', () => {
it('Is displayed while the auth is initializing', async () => {
const buttonText = "You shouldn't be able to see me!";
renderAuthWrapper({
authStatus: 'initializing',
childButtonText: buttonText,
});

await screen.findByText(/Loading/);
const button = screen.queryByRole('button', { name: buttonText });
expect(button).not.toBeInTheDocument();
renderAuthWrapper({ authStatus: 'initializing' });
const loadingIndicator = await screen.findByText(/Loading/);
expect(loadingIndicator).toBeInTheDocument();
});
});

describe('Token distrusted form', () => {
it("Is displayed when the user's auth status cannot be verified", async () => {
const buttonText = 'Not sure if you should be able to see me';
const distrustedTextMatcher = /Unable to verify token authenticity/;
const distrustedStatuses: readonly CoderAuthStatus[] = [
'distrusted',
'noInternetConnection',
'deploymentUnavailable',
];

for (const status of distrustedStatuses) {
const { unmount } = await renderAuthWrapper({
authStatus: status,
childButtonText: buttonText,
});

await screen.findByText(distrustedTextMatcher);
const button = screen.queryByRole('button', { name: buttonText });
expect(button).not.toBeInTheDocument();
for (const authStatus of distrustedStatuses) {
const { unmount } = await renderAuthWrapper({ authStatus });
const message = await screen.findByText(distrustedTextMatcher);

expect(message).toBeInTheDocument();
unmount();
}
});
Expand All @@ -112,58 +77,28 @@ describe(`${CoderAuthWrapper.name}`, () => {

const user = userEvent.setup();
const ejectButton = await screen.findByRole('button', {
name: 'Eject token',
name: /Unlink Coder account/,
});

await user.click(ejectButton);
expect(ejectToken).toHaveBeenCalled();
});

it('Will appear if auth status changes during re-renders', async () => {
const buttonText = "Now you see me, now you don't";
const { rerender } = await renderAuthWrapper({
authStatus: 'authenticated',
childButtonText: buttonText,
});

// Capture button after it first appears on the screen
const button = await screen.findByRole('button', { name: buttonText });

rerender(
<CoderProviderWithMockAuth
appConfig={mockAppConfig}
authStatus="distrusted"
>
<CoderAuthWrapper type="card">
<button>{buttonText}</button>
</CoderAuthWrapper>
</CoderProviderWithMockAuth>,
);

// Assert that the button is now gone
expect(button).not.toBeInTheDocument();
});
});

describe('Token submission form', () => {
it("Is displayed when the token either doesn't exist or is definitely not valid", async () => {
const buttonText = "You're not allowed to gaze upon my visage";
const tokenFormMatcher = /Please enter a new token/;
const statusesForInvalidUser: readonly CoderAuthStatus[] = [
'invalid',
'tokenMissing',
];

for (const status of statusesForInvalidUser) {
const { unmount } = await renderAuthWrapper({
authStatus: status,
childButtonText: buttonText,
for (const authStatus of statusesForInvalidUser) {
const { unmount } = await renderAuthWrapper({ authStatus });
const form = screen.getByRole('form', {
name: /Authenticate with Coder/,
});

await screen.findByText(tokenFormMatcher);
const button = screen.queryByRole('button', { name: buttonText });
expect(button).not.toBeInTheDocument();

expect(form).toBeInTheDocument();
unmount();
}
});
Expand All @@ -178,7 +113,8 @@ describe(`${CoderAuthWrapper.name}`, () => {
* 1. The auth input is of type password, which does not have a role
* compatible with Testing Library; can't use getByRole to select it
* 2. MUI adds a star to its labels that are required, meaning that any
* attempts at trying to match the string "Auth token" will fail
* attempts at trying to match string literal "Auth token" will fail;
* have to use a regex selector
*/
const inputField = screen.getByLabelText(/Auth token/);
const submitButton = screen.getByRole('button', { name: 'Authenticate' });
Expand Down
Loading