Skip to content

Commit f2b82a1

Browse files
authored
Merge pull request #3 from coder/mes/workspaces-list-8
feat: add auth styling and 'extra actions' menu
2 parents 23795ae + be495b9 commit f2b82a1

24 files changed

+1005
-368
lines changed

plugins/backstage-plugin-coder/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
"dependencies": {
2727
"@backstage/core-components": "^0.13.10",
2828
"@backstage/core-plugin-api": "^1.8.2",
29+
"@backstage/integration-react": "^1.1.24",
30+
"@backstage/plugin-catalog-react": "^1.10.0",
2931
"@backstage/theme": "^0.5.0",
3032
"@material-ui/core": "^4.12.2",
3133
"@material-ui/icons": "^4.9.1",
@@ -43,7 +45,7 @@
4345
"@backstage/dev-utils": "^1.0.26",
4446
"@backstage/test-utils": "^1.4.7",
4547
"@testing-library/jest-dom": "^5.10.1",
46-
"@testing-library/react": "^12.1.3",
48+
"@testing-library/react": "^14.2.1",
4749
"@testing-library/user-event": "^14.0.0",
4850
"msw": "^1.0.0"
4951
},
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import React from 'react';
2+
import { CoderLogo } from '../CoderLogo';
3+
import { LinkButton } from '@backstage/core-components';
4+
import { makeStyles } from '@material-ui/core';
5+
import { useCoderAuth } from '../CoderProvider';
6+
7+
const useStyles = makeStyles(theme => ({
8+
root: {
9+
display: 'flex',
10+
flexFlow: 'column nowrap',
11+
alignItems: 'center',
12+
maxWidth: '30em',
13+
marginLeft: 'auto',
14+
marginRight: 'auto',
15+
rowGap: theme.spacing(2),
16+
},
17+
18+
button: {
19+
maxWidth: 'fit-content',
20+
marginLeft: 'auto',
21+
marginRight: 'auto',
22+
},
23+
24+
coderLogo: {
25+
display: 'block',
26+
width: 'fit-content',
27+
marginLeft: 'auto',
28+
marginRight: 'auto',
29+
},
30+
}));
31+
32+
export const CoderAuthDistrustedForm = () => {
33+
const styles = useStyles();
34+
const { ejectToken } = useCoderAuth();
35+
36+
return (
37+
<div className={styles.root}>
38+
<div>
39+
<CoderLogo className={styles.coderLogo} />
40+
<p>
41+
Unable to verify token authenticity. Please check your internet
42+
connection, or try ejecting the token.
43+
</p>
44+
</div>
45+
46+
<LinkButton
47+
disableRipple
48+
to=""
49+
component="button"
50+
type="submit"
51+
color="primary"
52+
variant="contained"
53+
className={styles.button}
54+
onClick={ejectToken}
55+
>
56+
Eject token
57+
</LinkButton>
58+
</div>
59+
);
60+
};
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import React, { FormEvent } from 'react';
2+
import { useId } from '../../hooks/hookPolyfills';
3+
import {
4+
type CoderAuthStatus,
5+
useCoderAppConfig,
6+
useCoderAuth,
7+
} from '../CoderProvider';
8+
9+
import { Theme, makeStyles } from '@material-ui/core';
10+
import TextField from '@material-ui/core/TextField';
11+
import { CoderLogo } from '../CoderLogo';
12+
import { Link, LinkButton } from '@backstage/core-components';
13+
import { VisuallyHidden } from '../VisuallyHidden';
14+
15+
type UseStyleInput = Readonly<{ status: CoderAuthStatus }>;
16+
type StyleKeys =
17+
| 'formContainer'
18+
| 'authInputFieldset'
19+
| 'coderLogo'
20+
| 'authButton'
21+
| 'warningBanner'
22+
| 'warningBannerContainer';
23+
24+
const useStyles = makeStyles<Theme, UseStyleInput, StyleKeys>(theme => ({
25+
formContainer: {
26+
maxWidth: '30em',
27+
marginLeft: 'auto',
28+
marginRight: 'auto',
29+
},
30+
31+
authInputFieldset: {
32+
display: 'flex',
33+
flexFlow: 'column nowrap',
34+
rowGap: theme.spacing(2),
35+
margin: `${theme.spacing(-0.5)} 0 0 0`,
36+
border: 'none',
37+
padding: 0,
38+
},
39+
40+
coderLogo: {
41+
display: 'block',
42+
width: 'fit-content',
43+
marginLeft: 'auto',
44+
marginRight: 'auto',
45+
},
46+
47+
authButton: {
48+
display: 'block',
49+
width: 'fit-content',
50+
marginLeft: 'auto',
51+
marginRight: 'auto',
52+
},
53+
54+
warningBannerContainer: {
55+
paddingTop: theme.spacing(4),
56+
paddingLeft: theme.spacing(6),
57+
paddingRight: theme.spacing(6),
58+
},
59+
60+
warningBanner: ({ status }) => {
61+
let color: string;
62+
let backgroundColor: string;
63+
64+
if (status === 'invalid') {
65+
color = theme.palette.error.contrastText;
66+
backgroundColor = theme.palette.banner.error;
67+
} else {
68+
color = theme.palette.text.primary;
69+
backgroundColor = theme.palette.background.default;
70+
}
71+
72+
return {
73+
color,
74+
backgroundColor,
75+
borderRadius: theme.shape.borderRadius,
76+
textAlign: 'center',
77+
paddingTop: theme.spacing(0.5),
78+
paddingBottom: theme.spacing(0.5),
79+
};
80+
},
81+
}));
82+
83+
export const CoderAuthInputForm = () => {
84+
const hookId = useId();
85+
const appConfig = useCoderAppConfig();
86+
const { status, registerNewToken } = useCoderAuth();
87+
const styles = useStyles({ status });
88+
89+
const onSubmit = (event: FormEvent<HTMLFormElement>) => {
90+
event.preventDefault();
91+
const formData = Object.fromEntries(new FormData(event.currentTarget));
92+
const newToken =
93+
typeof formData.authToken === 'string' ? formData.authToken : '';
94+
95+
registerNewToken(newToken);
96+
};
97+
98+
const legendId = `${hookId}-legend`;
99+
const authTokenInputId = `${hookId}-auth-token`;
100+
const warningBannerId = `${hookId}-warning-banner`;
101+
102+
return (
103+
<form className={styles.formContainer} onSubmit={onSubmit}>
104+
<div>
105+
<CoderLogo className={styles.coderLogo} />
106+
<p>
107+
Your Coder session token is {mapAuthStatusToText(status)}. Please
108+
enter a new token from your{' '}
109+
<Link
110+
to={`${appConfig.deployment.accessUrl}/cli-auth`}
111+
target="_blank"
112+
>
113+
Coder deployment's token page
114+
<VisuallyHidden> (link opens in new tab)</VisuallyHidden>
115+
</Link>
116+
.
117+
</p>
118+
</div>
119+
120+
<fieldset className={styles.authInputFieldset} aria-labelledby={legendId}>
121+
<legend hidden id={legendId}>
122+
Auth input
123+
</legend>
124+
125+
<TextField
126+
// Adding the label prop directly to the TextField will place a label
127+
// in the HTML, so sighted users are fine. But for some reason, it
128+
// won't connect the label and input together, which breaks
129+
// accessibility for screen readers. Need to wire up extra IDs, sadly.
130+
label="Auth token"
131+
id={authTokenInputId}
132+
InputLabelProps={{ htmlFor: authTokenInputId }}
133+
required
134+
name="authToken"
135+
type="password"
136+
defaultValue=""
137+
aria-errormessage={warningBannerId}
138+
style={{ width: '100%' }}
139+
/>
140+
141+
<LinkButton
142+
disableRipple
143+
to=""
144+
component="button"
145+
type="submit"
146+
color="primary"
147+
variant="contained"
148+
className={styles.authButton}
149+
>
150+
Authenticate
151+
</LinkButton>
152+
</fieldset>
153+
154+
{(status === 'invalid' || status === 'authenticating') && (
155+
<div className={styles.warningBannerContainer}>
156+
<div id={warningBannerId} className={styles.warningBanner}>
157+
{status === 'invalid' && 'Invalid token'}
158+
{status === 'authenticating' && <>Authenticating&hellip;</>}
159+
</div>
160+
</div>
161+
)}
162+
</form>
163+
);
164+
};
165+
166+
function mapAuthStatusToText(status: CoderAuthStatus): string {
167+
switch (status) {
168+
case 'tokenMissing': {
169+
return 'missing';
170+
}
171+
172+
case 'initializing':
173+
case 'authenticating': {
174+
return status;
175+
}
176+
177+
default: {
178+
return 'invalid';
179+
}
180+
}
181+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import React, { useEffect, useState } from 'react';
2+
import { CoderLogo } from '../CoderLogo';
3+
import { makeStyles } from '@material-ui/core';
4+
import { VisuallyHidden } from '../VisuallyHidden';
5+
6+
const MAX_DOTS = 3;
7+
const dotRange = new Array(MAX_DOTS).fill(null).map((_, i) => i + 1);
8+
9+
const useStyles = makeStyles(theme => ({
10+
root: {
11+
display: 'flex',
12+
flexFlow: 'column nowrap',
13+
alignItems: 'center',
14+
},
15+
16+
text: {
17+
lineHeight: theme.typography.body1.lineHeight,
18+
paddingLeft: theme.spacing(1),
19+
},
20+
21+
coderLogo: {
22+
display: 'block',
23+
width: 'fit-content',
24+
marginLeft: 'auto',
25+
marginRight: 'auto',
26+
},
27+
}));
28+
29+
export const CoderAuthLoadingState = () => {
30+
const [visibleDots, setVisibleDots] = useState(0);
31+
const styles = useStyles();
32+
33+
useEffect(() => {
34+
const intervalId = window.setInterval(() => {
35+
setVisibleDots(current => (current + 1) % (MAX_DOTS + 1));
36+
}, 1_000);
37+
38+
return () => window.clearInterval(intervalId);
39+
}, []);
40+
41+
return (
42+
<div className={styles.root}>
43+
<CoderLogo className={styles.coderLogo} />
44+
<p className={styles.text}>
45+
Loading
46+
{/* Exposing the more semantic ellipses for screen readers, but
47+
rendering the individual dots for sighted viewers so that they can
48+
be animated */}
49+
<VisuallyHidden>&hellip;</VisuallyHidden>
50+
{dotRange.map(dotPosition => (
51+
<span
52+
key={dotPosition}
53+
style={{ opacity: visibleDots >= dotPosition ? 1 : 0 }}
54+
aria-hidden
55+
>
56+
.
57+
</span>
58+
))}
59+
</p>
60+
</div>
61+
);
62+
};

0 commit comments

Comments
 (0)