Skip to content

Commit

Permalink
chore: soften error message styling for invalid/authenticating tokens (
Browse files Browse the repository at this point in the history
…#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
  • Loading branch information
Parkreiner authored Mar 29, 2024
1 parent 81502c2 commit 6c050f1
Show file tree
Hide file tree
Showing 2 changed files with 144 additions and 53 deletions.
Original file line number Diff line number Diff line change
@@ -1,27 +1,20 @@
import React, { FormEvent } from 'react';
import React, { type FormEvent, useState } 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';
import { makeStyles } from '@material-ui/core';
import TextField from '@material-ui/core/TextField';
import ErrorIcon from '@material-ui/icons/ErrorOutline';
import SyncIcon from '@material-ui/icons/Sync';

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

const useStyles = makeStyles<Theme, UseStyleInput, StyleKeys>(theme => ({
const useStyles = makeStyles(theme => ({
formContainer: {
maxWidth: '30em',
marginLeft: 'auto',
Expand Down Expand Up @@ -50,41 +43,13 @@ const useStyles = makeStyles<Theme, UseStyleInput, StyleKeys>(theme => ({
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 styles = useStyles();
const appConfig = useCoderAppConfig();
const { status, registerNewToken } = useCoderAuth();
const styles = useStyles({ status });

const onSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
Expand Down Expand Up @@ -161,13 +126,122 @@ export const CoderAuthInputForm = () => {
</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>
<InvalidStatusNotifier authStatus={status} bannerId={warningBannerId} />
)}
</form>
);
};

const useInvalidStatusStyles = makeStyles(theme => ({
warningBannerSpacer: {
paddingTop: theme.spacing(2),
},

warningBanner: {
display: 'flex',
flexFlow: 'row nowrap',
alignItems: 'center',
color: theme.palette.text.primary,
backgroundColor: theme.palette.background.default,
borderRadius: theme.shape.borderRadius,
border: `1.5px solid ${theme.palette.background.default}`,
padding: 0,
},

errorContent: {
display: 'flex',
flexFlow: 'row nowrap',
alignItems: 'center',
columnGap: theme.spacing(1),
marginRight: 'auto',

paddingTop: theme.spacing(0.5),
paddingBottom: theme.spacing(0.5),
paddingLeft: theme.spacing(2),
paddingRight: 0,
},

icon: {
fontSize: '16px',
},

syncIcon: {
color: theme.palette.text.primary,
opacity: 0.6,
},

errorIcon: {
color: theme.palette.error.main,
fontSize: '16px',
},

dismissButton: {
border: 'none',
alignSelf: 'stretch',
padding: `0 ${theme.spacing(1.5)}px 0 ${theme.spacing(2)}px`,
color: theme.palette.text.primary,
backgroundColor: 'inherit',
lineHeight: 1,
cursor: 'pointer',

'&:hover': {
backgroundColor: theme.palette.action.hover,
},
},

'@keyframes spin': {
'100%': {
transform: 'rotate(360deg)',
},
},
}));

type InvalidStatusProps = Readonly<{
authStatus: CoderAuthStatus;
bannerId: string;
}>;

function InvalidStatusNotifier({ authStatus, bannerId }: InvalidStatusProps) {
const [showNotification, setShowNotification] = useState(true);
const styles = useInvalidStatusStyles();

if (!showNotification) {
return null;
}

return (
<div className={styles.warningBannerSpacer}>
<div id={bannerId} className={styles.warningBanner}>
<span className={styles.errorContent}>
{authStatus === 'authenticating' && (
<>
<SyncIcon
className={`${styles.icon} ${styles.syncIcon}`}
// Needed to make MUI v4 icons respect sizing values
fontSize="inherit"
/>
Authenticating&hellip;
</>
)}

{authStatus === 'invalid' && (
<>
<ErrorIcon
className={`${styles.icon} ${styles.errorIcon}`}
fontSize="inherit"
/>
Invalid token
</>
)}
</span>

<button
className={styles.dismissButton}
onClick={() => setShowNotification(false)}
>
Dismiss
</button>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { screen } from '@testing-library/react';
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { CoderProviderWithMockAuth } from '../../testHelpers/setup';
import type { CoderAuth, CoderAuthStatus } from '../CoderProvider';
Expand All @@ -12,13 +12,13 @@ import { CoderAuthWrapper } from './CoderAuthWrapper';
import { renderInTestApp } from '@backstage/test-utils';

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

async function renderAuthWrapper({
authStatus,
childButtonText,
childButtonText = 'Default button text',
}: RenderInputs) {
const ejectToken = jest.fn();
const registerNewToken = jest.fn();
Expand Down Expand Up @@ -108,7 +108,6 @@ describe(`${CoderAuthWrapper.name}`, () => {
it('Lets the user eject the current token', async () => {
const { ejectToken } = await renderAuthWrapper({
authStatus: 'distrusted',
childButtonText: "I don't matter",
});

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

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

expect(registerNewToken).toHaveBeenCalledWith(mockCoderAuthToken);
});

it('Lets the user dismiss any notifications for invalid/authenticating states', async () => {
const authStatuses: readonly CoderAuthStatus[] = [
'invalid',
'authenticating',
];

const user = userEvent.setup();
for (const authStatus of authStatuses) {
const { unmount } = await renderAuthWrapper({ authStatus });
const dismissButton = await screen.findByRole('button', {
name: 'Dismiss',
});

await user.click(dismissButton);
await waitFor(() => expect(dismissButton).not.toBeInTheDocument());
unmount();
}
});
});
});

0 comments on commit 6c050f1

Please sign in to comment.