Skip to content

Commit 6c050f1

Browse files
authored
chore: soften error message styling for invalid/authenticating tokens (#102)
* wip: commit progress on message redesign * wip: commit more style changes * wip: more style progress * chore: finish update for message * chore: add test case for dismissing functionality
1 parent 81502c2 commit 6c050f1

File tree

2 files changed

+144
-53
lines changed

2 files changed

+144
-53
lines changed

plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthInputForm.tsx

Lines changed: 122 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,20 @@
1-
import React, { FormEvent } from 'react';
1+
import React, { type FormEvent, useState } from 'react';
22
import { useId } from '../../hooks/hookPolyfills';
33
import {
44
type CoderAuthStatus,
55
useCoderAppConfig,
66
useCoderAuth,
77
} from '../CoderProvider';
88

9-
import { Theme, makeStyles } from '@material-ui/core';
10-
import TextField from '@material-ui/core/TextField';
119
import { CoderLogo } from '../CoderLogo';
1210
import { Link, LinkButton } from '@backstage/core-components';
1311
import { VisuallyHidden } from '../VisuallyHidden';
12+
import { makeStyles } from '@material-ui/core';
13+
import TextField from '@material-ui/core/TextField';
14+
import ErrorIcon from '@material-ui/icons/ErrorOutline';
15+
import SyncIcon from '@material-ui/icons/Sync';
1416

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 => ({
17+
const useStyles = makeStyles(theme => ({
2518
formContainer: {
2619
maxWidth: '30em',
2720
marginLeft: 'auto',
@@ -50,41 +43,13 @@ const useStyles = makeStyles<Theme, UseStyleInput, StyleKeys>(theme => ({
5043
marginLeft: 'auto',
5144
marginRight: 'auto',
5245
},
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-
},
8146
}));
8247

8348
export const CoderAuthInputForm = () => {
8449
const hookId = useId();
50+
const styles = useStyles();
8551
const appConfig = useCoderAppConfig();
8652
const { status, registerNewToken } = useCoderAuth();
87-
const styles = useStyles({ status });
8853

8954
const onSubmit = (event: FormEvent<HTMLFormElement>) => {
9055
event.preventDefault();
@@ -161,13 +126,122 @@ export const CoderAuthInputForm = () => {
161126
</fieldset>
162127

163128
{(status === 'invalid' || status === 'authenticating') && (
164-
<div className={styles.warningBannerContainer}>
165-
<div id={warningBannerId} className={styles.warningBanner}>
166-
{status === 'invalid' && 'Invalid token'}
167-
{status === 'authenticating' && <>Authenticating&hellip;</>}
168-
</div>
169-
</div>
129+
<InvalidStatusNotifier authStatus={status} bannerId={warningBannerId} />
170130
)}
171131
</form>
172132
);
173133
};
134+
135+
const useInvalidStatusStyles = makeStyles(theme => ({
136+
warningBannerSpacer: {
137+
paddingTop: theme.spacing(2),
138+
},
139+
140+
warningBanner: {
141+
display: 'flex',
142+
flexFlow: 'row nowrap',
143+
alignItems: 'center',
144+
color: theme.palette.text.primary,
145+
backgroundColor: theme.palette.background.default,
146+
borderRadius: theme.shape.borderRadius,
147+
border: `1.5px solid ${theme.palette.background.default}`,
148+
padding: 0,
149+
},
150+
151+
errorContent: {
152+
display: 'flex',
153+
flexFlow: 'row nowrap',
154+
alignItems: 'center',
155+
columnGap: theme.spacing(1),
156+
marginRight: 'auto',
157+
158+
paddingTop: theme.spacing(0.5),
159+
paddingBottom: theme.spacing(0.5),
160+
paddingLeft: theme.spacing(2),
161+
paddingRight: 0,
162+
},
163+
164+
icon: {
165+
fontSize: '16px',
166+
},
167+
168+
syncIcon: {
169+
color: theme.palette.text.primary,
170+
opacity: 0.6,
171+
},
172+
173+
errorIcon: {
174+
color: theme.palette.error.main,
175+
fontSize: '16px',
176+
},
177+
178+
dismissButton: {
179+
border: 'none',
180+
alignSelf: 'stretch',
181+
padding: `0 ${theme.spacing(1.5)}px 0 ${theme.spacing(2)}px`,
182+
color: theme.palette.text.primary,
183+
backgroundColor: 'inherit',
184+
lineHeight: 1,
185+
cursor: 'pointer',
186+
187+
'&:hover': {
188+
backgroundColor: theme.palette.action.hover,
189+
},
190+
},
191+
192+
'@keyframes spin': {
193+
'100%': {
194+
transform: 'rotate(360deg)',
195+
},
196+
},
197+
}));
198+
199+
type InvalidStatusProps = Readonly<{
200+
authStatus: CoderAuthStatus;
201+
bannerId: string;
202+
}>;
203+
204+
function InvalidStatusNotifier({ authStatus, bannerId }: InvalidStatusProps) {
205+
const [showNotification, setShowNotification] = useState(true);
206+
const styles = useInvalidStatusStyles();
207+
208+
if (!showNotification) {
209+
return null;
210+
}
211+
212+
return (
213+
<div className={styles.warningBannerSpacer}>
214+
<div id={bannerId} className={styles.warningBanner}>
215+
<span className={styles.errorContent}>
216+
{authStatus === 'authenticating' && (
217+
<>
218+
<SyncIcon
219+
className={`${styles.icon} ${styles.syncIcon}`}
220+
// Needed to make MUI v4 icons respect sizing values
221+
fontSize="inherit"
222+
/>
223+
Authenticating&hellip;
224+
</>
225+
)}
226+
227+
{authStatus === 'invalid' && (
228+
<>
229+
<ErrorIcon
230+
className={`${styles.icon} ${styles.errorIcon}`}
231+
fontSize="inherit"
232+
/>
233+
Invalid token
234+
</>
235+
)}
236+
</span>
237+
238+
<button
239+
className={styles.dismissButton}
240+
onClick={() => setShowNotification(false)}
241+
>
242+
Dismiss
243+
</button>
244+
</div>
245+
</div>
246+
);
247+
}

plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthWrapper.test.tsx

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react';
2-
import { screen } from '@testing-library/react';
2+
import { screen, waitFor } from '@testing-library/react';
33
import userEvent from '@testing-library/user-event';
44
import { CoderProviderWithMockAuth } from '../../testHelpers/setup';
55
import type { CoderAuth, CoderAuthStatus } from '../CoderProvider';
@@ -12,13 +12,13 @@ import { CoderAuthWrapper } from './CoderAuthWrapper';
1212
import { renderInTestApp } from '@backstage/test-utils';
1313

1414
type RenderInputs = Readonly<{
15-
childButtonText: string;
1615
authStatus: CoderAuthStatus;
16+
childButtonText?: string;
1717
}>;
1818

1919
async function renderAuthWrapper({
2020
authStatus,
21-
childButtonText,
21+
childButtonText = 'Default button text',
2222
}: RenderInputs) {
2323
const ejectToken = jest.fn();
2424
const registerNewToken = jest.fn();
@@ -108,7 +108,6 @@ describe(`${CoderAuthWrapper.name}`, () => {
108108
it('Lets the user eject the current token', async () => {
109109
const { ejectToken } = await renderAuthWrapper({
110110
authStatus: 'distrusted',
111-
childButtonText: "I don't matter",
112111
});
113112

114113
const user = userEvent.setup();
@@ -174,7 +173,6 @@ describe(`${CoderAuthWrapper.name}`, () => {
174173
it('Lets the user submit a new token', async () => {
175174
const { registerNewToken } = await renderAuthWrapper({
176175
authStatus: 'tokenMissing',
177-
childButtonText: "I don't matter",
178176
});
179177

180178
/**
@@ -194,5 +192,24 @@ describe(`${CoderAuthWrapper.name}`, () => {
194192

195193
expect(registerNewToken).toHaveBeenCalledWith(mockCoderAuthToken);
196194
});
195+
196+
it('Lets the user dismiss any notifications for invalid/authenticating states', async () => {
197+
const authStatuses: readonly CoderAuthStatus[] = [
198+
'invalid',
199+
'authenticating',
200+
];
201+
202+
const user = userEvent.setup();
203+
for (const authStatus of authStatuses) {
204+
const { unmount } = await renderAuthWrapper({ authStatus });
205+
const dismissButton = await screen.findByRole('button', {
206+
name: 'Dismiss',
207+
});
208+
209+
await user.click(dismissButton);
210+
await waitFor(() => expect(dismissButton).not.toBeInTheDocument());
211+
unmount();
212+
}
213+
});
197214
});
198215
});

0 commit comments

Comments
 (0)