Skip to content

feat: add auth styling and 'extra actions' menu #3

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 18 commits into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
5724aee
feat: add auth styling and 'extra actions' menu
Parkreiner Feb 21, 2024
a62bc1d
fix: make sure auth field has valid label
Parkreiner Feb 21, 2024
411a017
docs: rewrite comment for clarity
Parkreiner Feb 21, 2024
6eeef91
refactor: rename reauthenticating status to authenticating
Parkreiner Feb 22, 2024
b9e9765
fix: make sure pressing Enter doesn't cause button edge case behavior
Parkreiner Feb 22, 2024
b5a256e
fix: clean up form semantics to protect against more edge cases
Parkreiner Feb 22, 2024
6fd74b4
refactor: rewrite code to have more explicit exhaustiveness checks
Parkreiner Feb 22, 2024
ad729e1
refactor: update style declarations to be less confusing
Parkreiner Feb 22, 2024
75d410f
fix: make sure wrapper is only used when needed
Parkreiner Feb 22, 2024
96b9f7f
fix: make it more clear that clear button is interactive
Parkreiner Feb 22, 2024
2d0939e
fix: update spelling to follow conventional standards
Parkreiner Feb 22, 2024
95e483a
fix: add horizontal padding for search bar reminder
Parkreiner Feb 22, 2024
705e267
fix: prevent infinite loops when proxied deployment is unavailable
Parkreiner Feb 22, 2024
9d9d45d
chore: add 'deploymentUnavailable' status to auth
Parkreiner Feb 22, 2024
676c538
fix: update test helpers to use newer testing library versions
Parkreiner Feb 22, 2024
c037d8d
fix: polyfill AbortSignal.timeout for tests
Parkreiner Feb 22, 2024
c142349
fix: resolve all test flakes
Parkreiner Feb 22, 2024
be495b9
chore: remove unneeded package dependencies
Parkreiner Feb 22, 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
4 changes: 3 additions & 1 deletion plugins/backstage-plugin-coder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
"dependencies": {
"@backstage/core-components": "^0.13.10",
"@backstage/core-plugin-api": "^1.8.2",
"@backstage/integration-react": "^1.1.24",
"@backstage/plugin-catalog-react": "^1.10.0",
"@backstage/theme": "^0.5.0",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
Expand All @@ -43,7 +45,7 @@
"@backstage/dev-utils": "^1.0.26",
"@backstage/test-utils": "^1.4.7",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^12.1.3",
"@testing-library/react": "^14.2.1",
"@testing-library/user-event": "^14.0.0",
"msw": "^1.0.0"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React from 'react';
import { CoderLogo } from '../CoderLogo';
import { LinkButton } from '@backstage/core-components';
import { makeStyles } from '@material-ui/core';
import { useCoderAuth } from '../CoderProvider';

const useStyles = makeStyles(theme => ({
root: {
display: 'flex',
flexFlow: 'column nowrap',
alignItems: 'center',
maxWidth: '30em',
marginLeft: 'auto',
marginRight: 'auto',
rowGap: theme.spacing(2),
},

button: {
maxWidth: 'fit-content',
marginLeft: 'auto',
marginRight: 'auto',
},

coderLogo: {
display: 'block',
width: 'fit-content',
marginLeft: 'auto',
marginRight: 'auto',
},
}));

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

return (
<div className={styles.root}>
<div>
<CoderLogo className={styles.coderLogo} />
<p>
Unable to verify token authenticity. Please check your internet
Copy link
Member

Choose a reason for hiding this comment

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

I like this! Having the user choose whether to discard the token or not makes a lot of sense.

connection, or try ejecting the token.
</p>
</div>

<LinkButton
disableRipple
to=""
component="button"
type="submit"
color="primary"
variant="contained"
className={styles.button}
onClick={ejectToken}
>
Eject token
</LinkButton>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import React, { FormEvent } from 'react';
import { useId } from '../../hooks/hookPolyfills';
import {
type CoderAuthStatus,
useCoderAppConfig,
useCoderAuth,
} from '../CoderProvider';

import { Theme, makeStyles } from '@material-ui/core';
import TextField from '@material-ui/core/TextField';
import { CoderLogo } from '../CoderLogo';
import { Link, LinkButton } from '@backstage/core-components';
import { VisuallyHidden } from '../VisuallyHidden';

type UseStyleInput = Readonly<{ status: CoderAuthStatus }>;
type StyleKeys =
| 'formContainer'
| 'authInputFieldset'
| 'coderLogo'
| 'authButton'
| 'warningBanner'
| 'warningBannerContainer';

const useStyles = makeStyles<Theme, UseStyleInput, StyleKeys>(theme => ({
formContainer: {
maxWidth: '30em',
marginLeft: 'auto',
marginRight: 'auto',
},

authInputFieldset: {
display: 'flex',
flexFlow: 'column nowrap',
rowGap: theme.spacing(2),
margin: `${theme.spacing(-0.5)} 0 0 0`,
border: 'none',
padding: 0,
},

coderLogo: {
display: 'block',
width: 'fit-content',
marginLeft: 'auto',
marginRight: 'auto',
},

authButton: {
display: 'block',
width: 'fit-content',
marginLeft: 'auto',
marginRight: 'auto',
},

warningBannerContainer: {
paddingTop: theme.spacing(4),
paddingLeft: theme.spacing(6),
paddingRight: theme.spacing(6),
},

warningBanner: ({ status }) => {
let color: string;
let backgroundColor: string;

if (status === 'invalid') {
color = theme.palette.error.contrastText;
backgroundColor = theme.palette.banner.error;
} else {
color = theme.palette.text.primary;
backgroundColor = theme.palette.background.default;
}

return {
color,
backgroundColor,
borderRadius: theme.shape.borderRadius,
textAlign: 'center',
paddingTop: theme.spacing(0.5),
paddingBottom: theme.spacing(0.5),
};
},
}));

export const CoderAuthInputForm = () => {
const hookId = useId();
const appConfig = useCoderAppConfig();
const { status, registerNewToken } = useCoderAuth();
const styles = useStyles({ status });

const onSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData = Object.fromEntries(new FormData(event.currentTarget));
const newToken =
typeof formData.authToken === 'string' ? formData.authToken : '';

registerNewToken(newToken);
};

const legendId = `${hookId}-legend`;
const authTokenInputId = `${hookId}-auth-token`;
const warningBannerId = `${hookId}-warning-banner`;

return (
<form className={styles.formContainer} onSubmit={onSubmit}>
<div>
<CoderLogo className={styles.coderLogo} />
<p>
Your Coder session token is {mapAuthStatusToText(status)}. Please
enter a new token from your{' '}
<Link
to={`${appConfig.deployment.accessUrl}/cli-auth`}
target="_blank"
>
Coder deployment's token page
<VisuallyHidden> (link opens in new tab)</VisuallyHidden>
</Link>
.
</p>
</div>

<fieldset className={styles.authInputFieldset} aria-labelledby={legendId}>
<legend hidden id={legendId}>
Auth input
</legend>

<TextField
// Adding the label prop directly to the TextField will place a label
// in the HTML, so sighted users are fine. But for some reason, it
// won't connect the label and input together, which breaks
// accessibility for screen readers. Need to wire up extra IDs, sadly.
label="Auth token"
id={authTokenInputId}
InputLabelProps={{ htmlFor: authTokenInputId }}
required
name="authToken"
type="password"
defaultValue=""
aria-errormessage={warningBannerId}
style={{ width: '100%' }}
/>

<LinkButton
disableRipple
to=""
component="button"
type="submit"
color="primary"
variant="contained"
className={styles.authButton}
>
Authenticate
</LinkButton>
</fieldset>

{(status === 'invalid' || status === 'authenticating') && (
<div className={styles.warningBannerContainer}>
<div id={warningBannerId} className={styles.warningBanner}>
{status === 'invalid' && 'Invalid token'}
{status === 'authenticating' && <>Authenticating&hellip;</>}
</div>
</div>
)}
</form>
);
};

function mapAuthStatusToText(status: CoderAuthStatus): string {
switch (status) {
case 'tokenMissing': {
return 'missing';
}

case 'initializing':
case 'authenticating': {
return status;
}

default: {
return 'invalid';
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React, { useEffect, useState } from 'react';
import { CoderLogo } from '../CoderLogo';
import { makeStyles } from '@material-ui/core';
import { VisuallyHidden } from '../VisuallyHidden';

const MAX_DOTS = 3;
const dotRange = new Array(MAX_DOTS).fill(null).map((_, i) => i + 1);

const useStyles = makeStyles(theme => ({
root: {
display: 'flex',
flexFlow: 'column nowrap',
alignItems: 'center',
},

text: {
lineHeight: theme.typography.body1.lineHeight,
paddingLeft: theme.spacing(1),
},

coderLogo: {
display: 'block',
width: 'fit-content',
marginLeft: 'auto',
marginRight: 'auto',
},
}));

export const CoderAuthLoadingState = () => {
const [visibleDots, setVisibleDots] = useState(0);
const styles = useStyles();

useEffect(() => {
const intervalId = window.setInterval(() => {
setVisibleDots(current => (current + 1) % (MAX_DOTS + 1));
}, 1_000);

return () => window.clearInterval(intervalId);
}, []);

return (
<div className={styles.root}>
<CoderLogo className={styles.coderLogo} />
<p className={styles.text}>
Loading
{/* Exposing the more semantic ellipses for screen readers, but
rendering the individual dots for sighted viewers so that they can
be animated */}
<VisuallyHidden>&hellip;</VisuallyHidden>
{dotRange.map(dotPosition => (
<span
key={dotPosition}
style={{ opacity: visibleDots >= dotPosition ? 1 : 0 }}
aria-hidden
>
.
</span>
))}
</p>
</div>
);
};
Loading