From 5724aeec25042927eb0c7bf29391c5245280f2dc Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Wed, 21 Feb 2024 00:59:23 +0000 Subject: [PATCH 01/18] feat: add auth styling and 'extra actions' menu --- .../CoderAuthDistrustedForm.tsx | 60 +++++ .../CoderAuthWrapper/CoderAuthInputForm.tsx | 175 ++++++++++++++ .../CoderAuthLoadingState.tsx | 62 +++++ .../CoderAuthWrapper/CoderAuthWrapper.tsx | 121 +++------- .../src/components/CoderLogo/CoderLogo.tsx | 36 +++ .../src/components/CoderLogo/index.ts | 1 + .../CoderProvider/CoderAuthProvider.tsx | 47 ++-- .../CoderWorkspacesCard.tsx | 4 +- .../ExtraActionsButton.tsx | 219 ++++++++++++++++++ .../CoderWorkspacesCard/RefreshButton.tsx | 109 --------- .../CoderWorkspacesCard/WorkspacesList.tsx | 21 +- .../components/CoderWorkspacesCard/index.ts | 2 +- .../src/testHelpers/mockBackstageData.ts | 4 +- 13 files changed, 620 insertions(+), 241 deletions(-) create mode 100644 plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthDistrustedForm.tsx create mode 100644 plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthInputForm.tsx create mode 100644 plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthLoadingState.tsx create mode 100644 plugins/backstage-plugin-coder/src/components/CoderLogo/CoderLogo.tsx create mode 100644 plugins/backstage-plugin-coder/src/components/CoderLogo/index.ts create mode 100644 plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ExtraActionsButton.tsx delete mode 100644 plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/RefreshButton.tsx diff --git a/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthDistrustedForm.tsx b/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthDistrustedForm.tsx new file mode 100644 index 00000000..1a63a24a --- /dev/null +++ b/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthDistrustedForm.tsx @@ -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 ( +
+
+ +

+ Unable to verify token authenticity. Please check your internet + connection, or try ejecting the token. +

+
+ + + Eject token + +
+ ); +}; diff --git a/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthInputForm.tsx b/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthInputForm.tsx new file mode 100644 index 00000000..7f9a60bf --- /dev/null +++ b/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthInputForm.tsx @@ -0,0 +1,175 @@ +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 => ({ + formContainer: { + maxWidth: '30em', + marginLeft: 'auto', + marginRight: 'auto', + }, + + authInputFieldset: { + display: 'flex', + flexFlow: 'column nowrap', + rowGap: theme.spacing(2), + marginTop: theme.spacing(-0.5), + border: 'none', + margin: 0, + 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) => { + 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 warningBannerId = `${hookId}-warning-banner`; + + return ( +
+
+ +

+ Your Coder session token is {mapAuthStatusToText(status)}. Please + enter a new token from your{' '} + + Coder deployment's token page + (link opens in new tab) + + . +

+
+ +
+ + + + + + Authenticate + +
+ + {(status === 'invalid' || status === 'reauthenticating') && ( +
+
+ {status === 'invalid' && 'Invalid token'} + {status === 'reauthenticating' && <>Reauthenticating…} +
+
+ )} +
+ ); +}; + +function mapAuthStatusToText(status: CoderAuthStatus): string { + switch (status) { + case 'tokenMissing': { + return 'missing'; + } + + case 'initializing': + case 'reauthenticating': { + return status; + } + + default: { + return 'invalid'; + } + } +} diff --git a/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthLoadingState.tsx b/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthLoadingState.tsx new file mode 100644 index 00000000..1ed9749a --- /dev/null +++ b/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthLoadingState.tsx @@ -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 ( +
+ +

+ Loading + {/* Exposing the more semantic ellipses for screen readers, but + rendering the individual dots for sighted viewers so that they can + be animated */} + + {dotRange.map(dotPosition => ( + = dotPosition ? 1 : 0 }} + aria-hidden + > + . + + ))} +

+
+ ); +}; diff --git a/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthWrapper.tsx b/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthWrapper.tsx index 496a087a..7d999fbe 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthWrapper.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthWrapper.tsx @@ -1,72 +1,25 @@ import React, { type FC, type PropsWithChildren } from 'react'; -import { - type CoderAuth, - type CoderAuthStatus, - useCoderAuth, - useCoderAppConfig, -} from '../CoderProvider'; - -import { LinkButton } from '@backstage/core-components'; -import { VisuallyHidden } from '../VisuallyHidden'; -import { Card } from '../Card'; - -type FormProps = Readonly>; - -const CoderAuthForm = ({ registerNewToken, status }: FormProps) => { - const appConfig = useCoderAppConfig(); - +import { useCoderAuth } from '../CoderProvider'; +import { InfoCard } from '@backstage/core-components'; +import { CoderAuthDistrustedForm } from './CoderAuthDistrustedForm'; +import { makeStyles } from '@material-ui/core'; +import { CoderAuthLoadingState } from './CoderAuthLoadingState'; +import { CoderAuthInputForm } from './CoderAuthInputForm'; + +const useStyles = makeStyles(theme => ({ + cardContent: { + paddingTop: theme.spacing(5), + paddingBottom: theme.spacing(5), + }, +})); + +function CoderAuthCard({ children }: PropsWithChildren) { + const styles = useStyles(); return ( -
{ - event.preventDefault(); - const formData = Object.fromEntries(new FormData(event.currentTarget)); - const newToken = - typeof formData.authToken === 'string' ? formData.authToken : ''; - - registerNewToken(newToken); - }} - > -

PLACEHOLDER STYLING

-

Status: {status}

- -

- Your Coder session token is {mapAuthStatusToText(status)}. Please enter - a new token from our{' '} - - Token page - (link opens in new tab) - - . -

- - - - - Authenticate - -
+ +
{children}
+
); -}; - -type LayoutComponentProps = PropsWithChildren; -function CoderAuthCard({ children }: LayoutComponentProps) { - return {children}; } type WrapperProps = Readonly< @@ -77,17 +30,15 @@ type WrapperProps = Readonly< export const CoderAuthWrapper = ({ children, type }: WrapperProps) => { const auth = useCoderAuth(); - if (auth.isAuthed) { + if (auth.isAuthenticated) { return <>{children}; } - let Wrapper: FC>; switch (type) { case 'card': { Wrapper = CoderAuthCard; break; } - default: { throw new Error( `Unknown CoderAuthWrapper display type ${type} encountered`, @@ -95,33 +46,15 @@ export const CoderAuthWrapper = ({ children, type }: WrapperProps) => { } } + const isInitializing = auth.status === 'initializing'; + const ableToVerify = + auth.status !== 'distrusted' && auth.status !== 'noInternetConnection'; + return ( - {auth.status === 'initializing' ? ( -

Loading…

- ) : ( - - )} + {isInitializing && } + {!ableToVerify && } + {!isInitializing && ableToVerify && }
); }; - -function mapAuthStatusToText(status: CoderAuthStatus): string { - switch (status) { - case 'tokenMissing': { - return 'missing'; - } - - case 'initializing': - case 'reauthenticating': { - return status; - } - - default: { - return 'invalid'; - } - } -} diff --git a/plugins/backstage-plugin-coder/src/components/CoderLogo/CoderLogo.tsx b/plugins/backstage-plugin-coder/src/components/CoderLogo/CoderLogo.tsx new file mode 100644 index 00000000..7649a48f --- /dev/null +++ b/plugins/backstage-plugin-coder/src/components/CoderLogo/CoderLogo.tsx @@ -0,0 +1,36 @@ +import { makeStyles } from '@material-ui/core'; +import React, { type HTMLAttributes } from 'react'; + +type CoderLogoProps = Readonly< + Omit, 'children'> +>; + +const useStyles = makeStyles(theme => ({ + root: { + fill: theme.palette.text.primary, + opacity: 1, + }, +})); + +export const CoderLogo = ({ + className = '', + ...delegatedProps +}: CoderLogoProps) => { + const styles = useStyles(); + return ( + + + + + + + + + ); +}; diff --git a/plugins/backstage-plugin-coder/src/components/CoderLogo/index.ts b/plugins/backstage-plugin-coder/src/components/CoderLogo/index.ts new file mode 100644 index 00000000..3819c2f9 --- /dev/null +++ b/plugins/backstage-plugin-coder/src/components/CoderLogo/index.ts @@ -0,0 +1 @@ +export * from './CoderLogo'; diff --git a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx index 9a43c5ca..3dbf0f8a 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx @@ -54,7 +54,8 @@ type AuthState = Readonly< export type CoderAuthStatus = AuthState['status']; export type CoderAuth = Readonly< AuthState & { - isAuthed: boolean; + isAuthenticated: boolean; + tokenLoadedOnMount: boolean; registerNewToken: (newToken: string) => void; ejectToken: () => void; } @@ -98,20 +99,24 @@ type CoderAuthProviderProps = Readonly>; export const CoderAuthProvider = ({ children }: CoderAuthProviderProps) => { const { baseUrl } = useBackstageEndpoints(); const [isInsideGracePeriod, setIsInsideGracePeriod] = useState(true); - const [authToken, setAuthToken] = useState( - () => window.localStorage.getItem(TOKEN_STORAGE_KEY) ?? '', - ); + + // Need to split hairs, because the query object can be disabled. Only want to + // expose the initializing state if the app mounts with a token already in + // localStorage + const [authToken, setAuthToken] = useState(readAuthToken); + const [readonlyInitialAuthToken] = useState(authToken); const authValidityQuery = useQuery({ ...authValidation({ baseUrl, authToken }), refetchOnWindowFocus: query => query.state.data !== false, }); - const authState = generateAuthState( + const authState = generateAuthState({ authToken, authValidityQuery, isInsideGracePeriod, - ); + initialAuthToken: readonlyInitialAuthToken, + }); // Mid-render state sync to avoid unnecessary re-renders that useEffect would // introduce, especially since we don't know how costly re-renders could be in @@ -161,7 +166,8 @@ export const CoderAuthProvider = ({ children }: CoderAuthProviderProps) => { { if (newToken !== '') { setAuthToken(newToken); @@ -179,19 +185,28 @@ export const CoderAuthProvider = ({ children }: CoderAuthProviderProps) => { ); }; +type GenerateAuthStateInputs = Readonly<{ + authToken: string; + initialAuthToken: string; + authValidityQuery: UseQueryResult; + isInsideGracePeriod: boolean; +}>; + /** * This function isn't too big, but it is accounting for a lot of possible * configurations that authValidityQuery can be in while background fetches and - * refetches are happening. Can't get away with checking the .status alone + * re-fetches are happening. Can't get away with checking the .status alone * * @see {@link https://tkdodo.eu/blog/status-checks-in-react-query} */ -function generateAuthState( - authToken: string, - authValidityQuery: UseQueryResult, - isInsideAuthGracePeriod: boolean, -): AuthState { +function generateAuthState({ + authToken, + initialAuthToken, + authValidityQuery, + isInsideGracePeriod, +}: GenerateAuthStateInputs): AuthState { const isInitializing = + initialAuthToken !== '' && authValidityQuery.isLoading && authValidityQuery.isFetching && !authValidityQuery.isFetchedAfterMount; @@ -226,7 +241,7 @@ function generateAuthState( }; } - if (isInsideAuthGracePeriod) { + if (isInsideGracePeriod) { return { status: 'distrustedWithGracePeriod', token: authToken, @@ -288,3 +303,7 @@ function generateAuthState( error: authValidityQuery.error, }; } + +function readAuthToken(): string { + return window.localStorage.getItem(TOKEN_STORAGE_KEY) ?? ''; +} diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/CoderWorkspacesCard.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/CoderWorkspacesCard.tsx index 9e54d711..64bff808 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/CoderWorkspacesCard.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/CoderWorkspacesCard.tsx @@ -5,8 +5,8 @@ import { type WorkspacesCardProps, Root } from './Root'; import { HeaderRow } from './HeaderRow'; import { SearchBox } from './SearchBox'; import { WorkspacesList } from './WorkspacesList'; -import { RefreshButton } from './RefreshButton'; import { CreateWorkspaceLink } from './CreateWorkspaceLink'; +import { ExtraActionsButton } from './ExtraActionsButton'; const useStyles = makeStyles(theme => ({ searchWrapper: { @@ -26,8 +26,8 @@ export const CoderWorkspacesCard = ( headerLevel="h2" actions={ <> - + } /> diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ExtraActionsButton.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ExtraActionsButton.tsx new file mode 100644 index 00000000..ebb1308e --- /dev/null +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ExtraActionsButton.tsx @@ -0,0 +1,219 @@ +import React, { + type ButtonHTMLAttributes, + type ForwardedRef, + forwardRef, + useEffect, + useRef, + useState, +} from 'react'; + +import { useId } from '../../hooks/hookPolyfills'; +import { useCoderAuth } from '../CoderProvider'; +import { useWorkspacesCardContext } from './Root'; +import { VisuallyHidden } from '../VisuallyHidden'; + +import Menu, { type MenuProps } from '@material-ui/core/Menu'; +import { type MenuListProps } from '@material-ui/core/MenuList'; +import MenuItem from '@material-ui/core/MenuItem'; +import MoreItemsIcon from '@material-ui/icons/MoreVert'; +import Tooltip, { type TooltipProps } from '@material-ui/core/Tooltip'; +import { makeStyles } from '@material-ui/core'; + +const REFRESH_THROTTLE_MS = 1_000; + +const useStyles = makeStyles(theme => { + const padding = theme.spacing(0.5); + return { + root: { + padding, + margin: 0, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + color: theme.palette.text.primary, + width: theme.spacing(4) + padding, + height: theme.spacing(4) + padding, + border: 'none', + borderRadius: '9999px', + backgroundColor: 'inherit', + lineHeight: 1, + + // Buttons don't traditionally have the pointer style, but it's being + // changed to match the cursor style for CreateWorkspaceButtonLink + cursor: 'pointer', + + '&:hover': { + backgroundColor: theme.palette.action.hover, + }, + }, + + menuList: { + '& > li:first-child:focus': { + backgroundColor: theme.palette.action.hover, + }, + }, + }; +}); + +type ExtraActionsMenuProps = Readonly< + Omit< + MenuProps, + | 'id' + | 'open' + | 'anchorEl' + | 'MenuListProps' + | 'children' + | 'onClose' + | 'getContentAnchorEl' + > & { + MenuListProps: Omit; + } +>; + +type ExtraActionsButtonProps = Readonly< + Omit, 'id' | 'aria-controls'> & { + onClose?: MenuProps['onClose']; + menuProps?: ExtraActionsMenuProps; + toolTipProps?: Omit; + tooltipText?: string; + tooltipRef?: ForwardedRef; + } +>; + +export const ExtraActionsButton = forwardRef< + HTMLButtonElement, + ExtraActionsButtonProps +>((props, ref) => { + const { + menuProps, + toolTipProps, + tooltipRef, + children, + className, + onClick: outerOnClick, + onClose: outerOnClose, + tooltipText = 'See additional workspace actions', + ...delegatedButtonProps + } = props; + + const { + className: menuListClassName, + ref: menuListRef, + MenuListProps = {}, + ...delegatedMenuProps + } = menuProps ?? {}; + + const hookId = useId(); + const [loadedAnchor, setLoadedAnchor] = useState(); + const refreshWorkspaces = useRefreshWorkspaces(); + const { ejectToken } = useCoderAuth(); + const styles = useStyles(); + + const closeMenu = () => setLoadedAnchor(undefined); + const isOpen = loadedAnchor !== undefined; + const menuId = `${hookId}-menu`; + const buttonId = `${hookId}-button`; + const keyboardInstructionsId = `${hookId}-instructions`; + + return ( + <> + + + + + + + {/* Warning: all direct children of Menu must be MenuItem components, or + else the auto-focus behavior will break. Even a custom component that + returns out nothing but a MenuItem will break it. (Guessing that MUI + uses something like cloneElement under the hood, and that they're + interacting with the raw JSX metadata objects before they're turned + into new UI.) */} + { + closeMenu(); + outerOnClose?.(event, reason); + }} + {...delegatedMenuProps} + > + { + refreshWorkspaces(); + closeMenu(); + }} + > + Refresh workspaces list + + + { + ejectToken(); + closeMenu(); + }} + > + Eject token + + + + ); +}); + +function useRefreshWorkspaces() { + const { workspacesQuery } = useWorkspacesCardContext(); + const refreshThrottleIdRef = useRef(); + + useEffect(() => { + const clearThrottleOnUnmount = () => { + window.clearTimeout(refreshThrottleIdRef.current); + }; + + return clearThrottleOnUnmount; + }, []); + + const refreshWorkspaces = () => { + if (refreshThrottleIdRef.current !== undefined) { + return; + } + + workspacesQuery.refetch(); + refreshThrottleIdRef.current = window.setTimeout(() => { + refreshThrottleIdRef.current = undefined; + }, REFRESH_THROTTLE_MS); + }; + + return refreshWorkspaces; +} diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/RefreshButton.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/RefreshButton.tsx deleted file mode 100644 index ee6e550b..00000000 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/RefreshButton.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import React, { - type ButtonHTMLAttributes, - type ForwardedRef, - forwardRef, - useEffect, - useRef, -} from 'react'; -import { makeStyles } from '@material-ui/core'; - -import { useWorkspacesCardContext } from './Root'; -import { VisuallyHidden } from '../VisuallyHidden'; - -import Tooltip, { type TooltipProps } from '@material-ui/core/Tooltip'; -import RefreshIcon from '@material-ui/icons/Cached'; - -const REFRESH_THROTTLE_MS = 1_000; - -const useStyles = makeStyles(theme => { - const padding = theme.spacing(0.5); - - return { - root: { - padding, - margin: 0, - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - color: theme.palette.text.primary, - width: theme.spacing(4) + padding, - height: theme.spacing(4) + padding, - border: 'none', - borderRadius: '9999px', - backgroundColor: 'inherit', - lineHeight: 1, - - // Buttons don't traditionally have the pointer style, but it's being - // changed to match the cursor style for CreateWorkspaceButtonLink - cursor: 'pointer', - - '&:hover': { - backgroundColor: theme.palette.action.hover, - }, - }, - }; -}); - -type RefreshButtonProps = Readonly< - ButtonHTMLAttributes & { - tooltipText?: string; - toolTipProps?: Omit; - tooltipRef?: ForwardedRef; - } ->; - -export const RefreshButton = forwardRef( - (props: RefreshButtonProps, ref?: ForwardedRef) => { - const { - children, - className, - tooltipRef, - onClick: outerOnClick, - toolTipProps = {}, - type = 'button', - tooltipText = 'Refresh workspaces list', - ...delegatedProps - } = props; - - const { workspacesQuery } = useWorkspacesCardContext(); - const refreshThrottleIdRef = useRef(); - const styles = useStyles(); - - useEffect(() => { - const clearThrottleOnUnmount = () => { - window.clearTimeout(refreshThrottleIdRef.current); - }; - - return clearThrottleOnUnmount; - }, []); - - const refreshWorkspaces = () => { - if (refreshThrottleIdRef.current !== undefined) { - return; - } - - refreshThrottleIdRef.current = window.setTimeout(() => { - workspacesQuery.refetch(); - refreshThrottleIdRef.current = undefined; - }, REFRESH_THROTTLE_MS); - }; - - return ( - - - - ); - }, -); diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesList.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesList.tsx index ca71e9ba..f85ff5c4 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesList.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesList.tsx @@ -9,6 +9,7 @@ import { type Theme, makeStyles } from '@material-ui/core'; import type { Workspace } from '../../typesConstants'; import { useWorkspacesCardContext } from './Root'; import { WorkspacesListItem } from './WorkspacesListItem'; +import { CoderLogo } from '../CoderLogo'; const usePlaceholderStyles = makeStyles(theme => ({ root: { @@ -18,11 +19,6 @@ const usePlaceholderStyles = makeStyles(theme => ({ alignItems: 'center', }, - coderLogo: { - fill: theme.palette.text.primary, - opacity: 1, - }, - text: { fontWeight: 400, fontSize: '1.125rem', @@ -40,20 +36,7 @@ const Placeholder = ({ children }: PlaceholderProps) => { return (
- - - - - - - - - +

{children}

); diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/index.ts b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/index.ts index 0ff4361a..55b94206 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/index.ts +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/index.ts @@ -1,7 +1,7 @@ export * from './CoderWorkspacesCard'; export * from './CreateWorkspaceLink'; export * from './HeaderRow'; -export * from './RefreshButton'; +export * from './ExtraActionsButton'; export * from './Root'; export * from './SearchBox'; export * from './WorkspacesList'; diff --git a/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts b/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts index 4f94ae38..6b8dc473 100644 --- a/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts +++ b/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts @@ -110,7 +110,7 @@ export const mockAppConfig = { const authedState = { token: mockCoderAuthToken, error: undefined, - isAuthed: true, + isAuthenticated: true, registerNewToken: jest.fn(), ejectToken: jest.fn(), } as const satisfies Partial; @@ -118,7 +118,7 @@ const authedState = { const notAuthedState = { token: undefined, error: undefined, - isAuthed: false, + isAuthenticated: false, registerNewToken: jest.fn(), ejectToken: jest.fn(), } as const satisfies Partial; From a62bc1dc1559481adc8ea460ecd53179c34f449a Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Wed, 21 Feb 2024 15:34:48 +0000 Subject: [PATCH 02/18] fix: make sure auth field has valid label --- .../CoderAuthWrapper/CoderAuthInputForm.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthInputForm.tsx b/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthInputForm.tsx index 7f9a60bf..a619820a 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthInputForm.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthInputForm.tsx @@ -97,6 +97,7 @@ export const CoderAuthInputForm = () => { }; const legendId = `${hookId}-legend`; + const authTokenInputId = `${hookId}-auth-token`; const warningBannerId = `${hookId}-warning-banner`; return ( @@ -123,13 +124,19 @@ export const CoderAuthInputForm = () => { Date: Wed, 21 Feb 2024 15:38:41 +0000 Subject: [PATCH 03/18] docs: rewrite comment for clarity --- .../components/CoderAuthWrapper/CoderAuthInputForm.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthInputForm.tsx b/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthInputForm.tsx index a619820a..5a882977 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthInputForm.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthInputForm.tsx @@ -124,10 +124,10 @@ export const CoderAuthInputForm = () => { Date: Thu, 22 Feb 2024 17:48:46 +0000 Subject: [PATCH 04/18] refactor: rename reauthenticating status to authenticating --- .../components/CoderAuthWrapper/CoderAuthInputForm.tsx | 6 +++--- .../src/components/CoderProvider/CoderAuthProvider.tsx | 8 ++++---- .../src/testHelpers/mockBackstageData.ts | 6 ++++-- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthInputForm.tsx b/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthInputForm.tsx index 5a882977..3ab0287c 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthInputForm.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthInputForm.tsx @@ -152,11 +152,11 @@ export const CoderAuthInputForm = () => { - {(status === 'invalid' || status === 'reauthenticating') && ( + {(status === 'invalid' || status === 'authenticating') && (
{status === 'invalid' && 'Invalid token'} - {status === 'reauthenticating' && <>Reauthenticating…} + {status === 'authenticating' && <>Authenticating…}
)} @@ -171,7 +171,7 @@ function mapAuthStatusToText(status: CoderAuthStatus): string { } case 'initializing': - case 'reauthenticating': { + case 'authenticating': { return status; } diff --git a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx index 3dbf0f8a..ae62fc4a 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx @@ -42,7 +42,7 @@ type AuthState = Readonly< // Distrusted represents a token that could be valid, but we are unable to // verify it within an allowed window. invalid is definitely, 100% invalid status: - | 'reauthenticating' + | 'authenticating' | 'invalid' | 'distrusted' | 'noInternetConnection'; @@ -259,15 +259,15 @@ function generateAuthState({ // Have to include isLoading here because the auth query uses the // isPreviousData property to mask the fact that we're shifting to different // query keys and cache pockets each time the token value changes - const isReauthenticating = + const isAuthenticating = authValidityQuery.isLoading || (authValidityQuery.isRefetching && ((authValidityQuery.isError && authValidityQuery.data !== true) || (authValidityQuery.isSuccess && authValidityQuery.data === false))); - if (isReauthenticating) { + if (isAuthenticating) { return { - status: 'reauthenticating', + status: 'authenticating', token: undefined, error: authValidityQuery.error, }; diff --git a/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts b/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts index 6b8dc473..0fe310ef 100644 --- a/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts +++ b/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts @@ -110,6 +110,7 @@ export const mockAppConfig = { const authedState = { token: mockCoderAuthToken, error: undefined, + tokenLoadedOnMount: true, isAuthenticated: true, registerNewToken: jest.fn(), ejectToken: jest.fn(), @@ -118,6 +119,7 @@ const authedState = { const notAuthedState = { token: undefined, error: undefined, + tokenLoadedOnMount: false, isAuthenticated: false, registerNewToken: jest.fn(), ejectToken: jest.fn(), @@ -139,9 +141,9 @@ export const mockAuthStates = { status: 'invalid', }, - reauthenticating: { + authenticating: { ...notAuthedState, - status: 'reauthenticating', + status: 'authenticating', }, distrusted: { From b9e97657858b94c8feac7d98a87a422676245a6e Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 22 Feb 2024 17:58:03 +0000 Subject: [PATCH 05/18] fix: make sure pressing Enter doesn't cause button edge case behavior --- .../CoderWorkspacesCard/ExtraActionsButton.tsx | 1 + .../src/components/CoderWorkspacesCard/Root.tsx | 15 +++++++-------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ExtraActionsButton.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ExtraActionsButton.tsx index ebb1308e..b7fb79a5 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ExtraActionsButton.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ExtraActionsButton.tsx @@ -123,6 +123,7 @@ export const ExtraActionsButton = forwardRef< id={buttonId} aria-controls={isOpen ? menuId : undefined} className={`${styles.root} ${className ?? ''}`} + // type="button" onClick={event => { setLoadedAnchor(event.currentTarget); outerOnClick?.(event); diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/Root.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/Root.tsx index ea15c1dc..a151b10a 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/Root.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/Root.tsx @@ -109,14 +109,13 @@ export const Root = forwardRef( aria-labelledby={headerId} {...delegatedProps} > -
element for semantics, but never want to go - // through a full native HTML form submission when the user does - // things like hitting the Enter key - onSubmit={event => event.preventDefault()} - > - {children} -
+ {/* Want to expose the overall container as a form for good + semantics and screen reader support, but since there isn't an + explicit submission process (queries happen automatically), + using a base div with a role override to side-step keyboard + input and button child edge cases seems like the easiest + approach */} +
{children}
From b5a256e9b1a883b56b19f5ada6bafddbeb18a912 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 22 Feb 2024 18:04:30 +0000 Subject: [PATCH 06/18] fix: clean up form semantics to protect against more edge cases --- .../src/components/CoderWorkspacesCard/ExtraActionsButton.tsx | 2 +- .../src/components/CoderWorkspacesCard/Root.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ExtraActionsButton.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ExtraActionsButton.tsx index b7fb79a5..07f6d9db 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ExtraActionsButton.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/ExtraActionsButton.tsx @@ -123,7 +123,7 @@ export const ExtraActionsButton = forwardRef< id={buttonId} aria-controls={isOpen ? menuId : undefined} className={`${styles.root} ${className ?? ''}`} - // type="button" + type="button" onClick={event => { setLoadedAnchor(event.currentTarget); outerOnClick?.(event); diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/Root.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/Root.tsx index a151b10a..452a663f 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/Root.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/Root.tsx @@ -112,8 +112,8 @@ export const Root = forwardRef( {/* Want to expose the overall container as a form for good semantics and screen reader support, but since there isn't an explicit submission process (queries happen automatically), - using a base div with a role override to side-step keyboard - input and button child edge cases seems like the easiest + using a base div with a role override to side-step edge cases + around keyboard input and button children seems like the easiest approach */}
{children}
From 6fd74b4c32764123d33f31f942a78f19c143161a Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 22 Feb 2024 18:19:19 +0000 Subject: [PATCH 07/18] refactor: rewrite code to have more explicit exhaustiveness checks --- .../CoderAuthWrapper/CoderAuthWrapper.tsx | 57 ++++++++++++++----- 1 file changed, 44 insertions(+), 13 deletions(-) diff --git a/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthWrapper.tsx b/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthWrapper.tsx index 7d999fbe..c7247c67 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthWrapper.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthWrapper.tsx @@ -30,9 +30,7 @@ type WrapperProps = Readonly< export const CoderAuthWrapper = ({ children, type }: WrapperProps) => { const auth = useCoderAuth(); - if (auth.isAuthenticated) { - return <>{children}; - } + let Wrapper: FC>; switch (type) { case 'card': { @@ -40,21 +38,54 @@ export const CoderAuthWrapper = ({ children, type }: WrapperProps) => { break; } default: { - throw new Error( - `Unknown CoderAuthWrapper display type ${type} encountered`, - ); + assertExhaustion(type); } } - const isInitializing = auth.status === 'initializing'; - const ableToVerify = - auth.status !== 'distrusted' && auth.status !== 'noInternetConnection'; - return ( - {isInitializing && } - {!ableToVerify && } - {!isInitializing && ableToVerify && } + {/* Slightly awkward syntax with the IIFE, but need something switch-like + to make sure that all status cases are handled exhaustively */} + {(() => { + switch (auth.status) { + case 'authenticated': + case 'distrustedWithGracePeriod': { + return <>{children}; + } + + case 'initializing': { + return ; + } + + case 'distrusted': + case 'noInternetConnection': { + return ; + } + + case 'authenticating': + case 'invalid': + case 'tokenMissing': { + return ; + } + + default: { + return assertExhaustion(auth); + } + } + })()} ); }; + +function assertExhaustion(...inputs: readonly never[]): never { + let inputsToLog: unknown; + try { + inputsToLog = JSON.stringify(inputs); + } catch { + inputsToLog = inputs; + } + + throw new Error( + `Not all possibilities for inputs (${inputsToLog}) have been exhausted`, + ); +} From ad729e13a062210a9b699814fbbedc48d9c5f9e5 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 22 Feb 2024 18:22:45 +0000 Subject: [PATCH 08/18] refactor: update style declarations to be less confusing --- .../src/components/CoderAuthWrapper/CoderAuthInputForm.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthInputForm.tsx b/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthInputForm.tsx index 3ab0287c..2b0fdeee 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthInputForm.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthInputForm.tsx @@ -32,9 +32,8 @@ const useStyles = makeStyles(theme => ({ display: 'flex', flexFlow: 'column nowrap', rowGap: theme.spacing(2), - marginTop: theme.spacing(-0.5), + margin: `${theme.spacing(-0.5)} 0 0 0`, border: 'none', - margin: 0, padding: 0, }, From 75d410fc53fd141cef3faca35bda4fba5e2abeae Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 22 Feb 2024 18:34:53 +0000 Subject: [PATCH 09/18] fix: make sure wrapper is only used when needed --- .../CoderAuthWrapper/CoderAuthWrapper.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthWrapper.tsx b/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthWrapper.tsx index c7247c67..02b6ff21 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthWrapper.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthWrapper.tsx @@ -30,6 +30,9 @@ type WrapperProps = Readonly< export const CoderAuthWrapper = ({ children, type }: WrapperProps) => { const auth = useCoderAuth(); + if (auth.isAuthenticated) { + return <>{children}; + } let Wrapper: FC>; switch (type) { @@ -48,11 +51,6 @@ export const CoderAuthWrapper = ({ children, type }: WrapperProps) => { to make sure that all status cases are handled exhaustively */} {(() => { switch (auth.status) { - case 'authenticated': - case 'distrustedWithGracePeriod': { - return <>{children}; - } - case 'initializing': { return ; } @@ -68,6 +66,13 @@ export const CoderAuthWrapper = ({ children, type }: WrapperProps) => { return ; } + case 'authenticated': + case 'distrustedWithGracePeriod': { + throw new Error( + 'This code should be unreachable because of the auth check near the start of the component', + ); + } + default: { return assertExhaustion(auth); } From 96b9f7fcac8ee321c7dc2767c6c949d55e462770 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 22 Feb 2024 18:43:22 +0000 Subject: [PATCH 10/18] fix: make it more clear that clear button is interactive --- .../src/components/CoderWorkspacesCard/SearchBox.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/SearchBox.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/SearchBox.tsx index 1a20f4d8..a1af15a0 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/SearchBox.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/SearchBox.tsx @@ -90,6 +90,7 @@ const useStyles = makeStyles(theme => ({ color: theme.palette.text.primary, opacity: isInputEmpty ? '40%' : '100%', outline: 'none', + cursor: 'pointer', '&:focus': { boxShadow: '0 0 0 1px hsl(213deg, 94%, 68%)', From 2d0939e99228e930294bd90aca09116ebe91c4a6 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 22 Feb 2024 18:51:28 +0000 Subject: [PATCH 11/18] fix: update spelling to follow conventional standards --- .../src/components/CoderWorkspacesCard/WorkspacesList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesList.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesList.tsx index f85ff5c4..67742a06 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesList.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesList.tsx @@ -116,7 +116,7 @@ export const WorkspacesList = ({ {workspacesQuery.fetchStatus === 'fetching' ? ( <>Loading… ) : ( - <>Use the searchbar to find matching Coder workspaces + <>Use the search bar to find matching Coder workspaces )} )} From 95e483ae6946476640c427c5d32b4587996571a3 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 22 Feb 2024 19:05:48 +0000 Subject: [PATCH 12/18] fix: add horizontal padding for search bar reminder --- .../src/components/CoderWorkspacesCard/WorkspacesList.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesList.tsx b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesList.tsx index 67742a06..173beac6 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesList.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesList.tsx @@ -20,6 +20,8 @@ const usePlaceholderStyles = makeStyles(theme => ({ }, text: { + textAlign: 'center', + padding: `0 ${theme.spacing(2.5)}px`, fontWeight: 400, fontSize: '1.125rem', color: theme.palette.text.secondary, From 705e267633374e25781e522eff8c991f278ccdb8 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 22 Feb 2024 19:32:06 +0000 Subject: [PATCH 13/18] fix: prevent infinite loops when proxied deployment is unavailable --- .../CoderProvider/CoderAuthProvider.tsx | 16 ++++++++--- .../CoderProvider/CoderProvider.tsx | 28 +++++++++---------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx index ae62fc4a..ebbe72f3 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx @@ -148,15 +148,23 @@ export const CoderAuthProvider = ({ children }: CoderAuthProviderProps) => { // outside React because we let the user connect their own queryClient const queryClient = useQueryClient(); useEffect(() => { + let isRefetchingTokenQuery = false; const queryCache = queryClient.getQueryCache(); - const unsubscribe = queryCache.subscribe(event => { + + const unsubscribe = queryCache.subscribe(async event => { const queryError = event.query.state.error; const shouldRevalidate = - queryError instanceof BackstageHttpError && !queryError.ok; + !isRefetchingTokenQuery && + queryError instanceof BackstageHttpError && + queryError.status === 401; - if (shouldRevalidate) { - queryClient.refetchQueries({ queryKey: authQueryKey }); + if (!shouldRevalidate) { + return; } + + isRefetchingTokenQuery = true; + await queryClient.refetchQueries({ queryKey: authQueryKey }); + isRefetchingTokenQuery = false; }); return unsubscribe; diff --git a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.tsx b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.tsx index f76be309..4c8d0898 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.tsx @@ -6,31 +6,29 @@ import { CoderAppConfigProvider } from './CoderAppConfigProvider'; import { CoderErrorBoundary } from '../CoderErrorBoundary'; import { BackstageHttpError } from '../../api'; +const MAX_FETCH_FAILURES = 3; + export type CoderProviderProps = ComponentProps & ComponentProps & { queryClient?: QueryClient; }; const shouldRetryRequest = (failureCount: number, error: unknown): boolean => { - const tooManyFailures = failureCount >= 3; - - // Have to duplicate a logic a little bit to improve type narrowing + const isBelowThreshold = failureCount < MAX_FETCH_FAILURES; if (!(error instanceof BackstageHttpError)) { - return tooManyFailures; + return isBelowThreshold; } - // This should trigger when there is an issue with the Backstage auth setup; - // just immediately give up on retries - if (error.status === 401) { - return false; - } + const isAuthenticationError = error.status === 401; + const isLikelyProxyConfigurationError = + error.status === 504 || + (error.status === 200 && error.contentType !== 'application/json'); - // This is rare, but a likely a sign that the proxy isn't set up properly - if (error.status === 200 && error.contentType !== 'application/json') { - return false; - } - - return tooManyFailures; + return ( + !isAuthenticationError && + !isLikelyProxyConfigurationError && + isBelowThreshold + ); }; const defaultClient = new QueryClient({ From 9d9d45d3bccc56c05a002acd4e6116180dd00199 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 22 Feb 2024 19:56:07 +0000 Subject: [PATCH 14/18] chore: add 'deploymentUnavailable' status to auth --- .../CoderAuthWrapper/CoderAuthWrapper.tsx | 3 ++- .../CoderProvider/CoderAuthProvider.tsx | 18 +++++++++++++++++- .../src/testHelpers/mockBackstageData.ts | 5 +++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthWrapper.tsx b/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthWrapper.tsx index 02b6ff21..0bfdff65 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthWrapper.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthWrapper.tsx @@ -56,7 +56,8 @@ export const CoderAuthWrapper = ({ children, type }: WrapperProps) => { } case 'distrusted': - case 'noInternetConnection': { + case 'noInternetConnection': + case 'deploymentUnavailable': { return ; } diff --git a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx index ebbe72f3..3192198e 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.tsx @@ -45,7 +45,8 @@ type AuthState = Readonly< | 'authenticating' | 'invalid' | 'distrusted' - | 'noInternetConnection'; + | 'noInternetConnection' + | 'deploymentUnavailable'; token: undefined; error: unknown; } @@ -237,6 +238,21 @@ function generateAuthState({ }; } + if (authValidityQuery.error instanceof BackstageHttpError) { + const deploymentLikelyUnavailable = + authValidityQuery.error.status === 504 || + (authValidityQuery.error.status === 200 && + authValidityQuery.error.contentType !== 'application/json'); + + if (deploymentLikelyUnavailable) { + return { + status: 'deploymentUnavailable', + token: undefined, + error: authValidityQuery.error, + }; + } + } + const isTokenValidFromPrevFetch = authValidityQuery.data === true; if (isTokenValidFromPrevFetch) { const canTrustAuthThisRender = diff --git a/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts b/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts index 0fe310ef..04e1e7c0 100644 --- a/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts +++ b/plugins/backstage-plugin-coder/src/testHelpers/mockBackstageData.ts @@ -165,6 +165,11 @@ export const mockAuthStates = { ...notAuthedState, status: 'tokenMissing', }, + + deploymentUnavailable: { + ...notAuthedState, + status: 'deploymentUnavailable', + }, } as const satisfies Record; export function getMockConfigApi() { From 676c5389670964f257762d9d53a4a987222d1aa2 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 22 Feb 2024 20:20:41 +0000 Subject: [PATCH 15/18] fix: update test helpers to use newer testing library versions --- plugins/backstage-plugin-coder/package.json | 4 + .../CoderProvider/CoderProvider.test.tsx | 2 +- .../src/hooks/hookPolyfills.test.ts | 2 +- .../src/testHelpers/setup.tsx | 13 +- yarn.lock | 244 +++++++++++++++--- 5 files changed, 230 insertions(+), 35 deletions(-) diff --git a/plugins/backstage-plugin-coder/package.json b/plugins/backstage-plugin-coder/package.json index 98155f76..be023bee 100644 --- a/plugins/backstage-plugin-coder/package.json +++ b/plugins/backstage-plugin-coder/package.json @@ -26,11 +26,15 @@ "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", "@material-ui/lab": "4.0.0-alpha.61", "@tanstack/react-query": "4.36.1", + "@testing-library/react": "^14.2.1", + "@testing-library/react-hooks": "^8.0.1", "react-use": "^17.2.4", "valibot": "^0.28.1" }, diff --git a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.test.tsx b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.test.tsx index cce89258..735449ff 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.test.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.test.tsx @@ -1,5 +1,5 @@ import React, { PropsWithChildren } from 'react'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { act, waitFor } from '@testing-library/react'; import { TestApiProvider } from '@backstage/test-utils'; diff --git a/plugins/backstage-plugin-coder/src/hooks/hookPolyfills.test.ts b/plugins/backstage-plugin-coder/src/hooks/hookPolyfills.test.ts index aeebb6d1..e58a3922 100644 --- a/plugins/backstage-plugin-coder/src/hooks/hookPolyfills.test.ts +++ b/plugins/backstage-plugin-coder/src/hooks/hookPolyfills.test.ts @@ -1,4 +1,4 @@ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { useId } from './hookPolyfills'; describe(`${useId.name}`, () => { diff --git a/plugins/backstage-plugin-coder/src/testHelpers/setup.tsx b/plugins/backstage-plugin-coder/src/testHelpers/setup.tsx index 49dabdb5..f03c2e5f 100644 --- a/plugins/backstage-plugin-coder/src/testHelpers/setup.tsx +++ b/plugins/backstage-plugin-coder/src/testHelpers/setup.tsx @@ -8,12 +8,15 @@ import { } from '@testing-library/react-hooks'; /* eslint-enable @backstage/no-undeclared-imports */ -import React, { ReactElement, type PropsWithChildren } from 'react'; +import React, { ReactElement } from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { scmIntegrationsApiRef } from '@backstage/integration-react'; -import { EntityProvider } from '@backstage/plugin-catalog-react'; import { configApiRef, errorApiRef } from '@backstage/core-plugin-api'; +import { + type EntityProviderProps, + EntityProvider, +} from '@backstage/plugin-catalog-react'; import { type CoderProviderProps, @@ -116,7 +119,7 @@ export const CoderProviderWithMockAuth = ({ ); }; -type ChildProps = Readonly>; +type ChildProps = EntityProviderProps; type RenderResultWithErrorApi = ReturnType & { errorApi: MockErrorApi; }; @@ -221,7 +224,9 @@ export const renderHookAsCoderEntity = < queryClient={mockQueryClient} authStatus={authStatus} > - {children} + + <>{children} + ), diff --git a/yarn.lock b/yarn.lock index 746c7686..e3ad6804 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2208,6 +2208,16 @@ cross-fetch "^4.0.0" uri-template "^2.0.0" +"@backstage/catalog-client@^1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@backstage/catalog-client/-/catalog-client-1.6.0.tgz#d4ba505f84a58f03177d0998becc6eb8ed54f40e" + integrity sha512-O6yoBX/BcKy89AwXmXVxNPlk0mX7jbgqYUCeIxGZr7n10A9oJx1iRj1XMub+V67yuqdfILPmh8WW+jd0N98+JA== + dependencies: + "@backstage/catalog-model" "^1.4.4" + "@backstage/errors" "^1.2.3" + cross-fetch "^4.0.0" + uri-template "^2.0.0" + "@backstage/catalog-model@^1.4.3": version "1.4.3" resolved "https://registry.yarnpkg.com/@backstage/catalog-model/-/catalog-model-1.4.3.tgz#64abf34071d1cad6372f905b92e1d831e480750c" @@ -2218,6 +2228,16 @@ ajv "^8.10.0" lodash "^4.17.21" +"@backstage/catalog-model@^1.4.4": + version "1.4.4" + resolved "https://registry.yarnpkg.com/@backstage/catalog-model/-/catalog-model-1.4.4.tgz#53ebbe754c72a0e01bb7ea025af0358dc459db9c" + integrity sha512-JiCeAgUdRMQTjO0+34QeKDxYh/UQrXtDUvVic5z11uf8WuX3L9N7LiPOqJG+3t9TAyc5side21nDD7REdHoVFA== + dependencies: + "@backstage/errors" "^1.2.3" + "@backstage/types" "^1.1.1" + ajv "^8.10.0" + lodash "^4.17.21" + "@backstage/cli-common@^0.1.13": version "0.1.13" resolved "https://registry.yarnpkg.com/@backstage/cli-common/-/cli-common-0.1.13.tgz#cbeda6a359ca4437fc782f0ac51bb957e8d49e73" @@ -2458,6 +2478,51 @@ zen-observable "^0.10.0" zod "^3.22.4" +"@backstage/core-components@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@backstage/core-components/-/core-components-0.14.0.tgz#f9208617f569badd4dbf3bf270179e1d6dd41e26" + integrity sha512-uIoQJFOghQX9kNk/RjWKYzqc/euq6p6HLYU01ptrCwY81dIChXUU/XulxuT0l1LQq8oAzQPbg6v9l4nU7EJ1yg== + dependencies: + "@backstage/config" "^1.1.1" + "@backstage/core-plugin-api" "^1.9.0" + "@backstage/errors" "^1.2.3" + "@backstage/theme" "^0.5.1" + "@backstage/version-bridge" "^1.0.7" + "@date-io/core" "^1.3.13" + "@material-table/core" "^3.1.0" + "@material-ui/core" "^4.12.2" + "@material-ui/icons" "^4.9.1" + "@material-ui/lab" "4.0.0-alpha.61" + "@react-hookz/web" "^24.0.0" + "@types/react" "^16.13.1 || ^17.0.0 || ^18.0.0" + "@types/react-sparklines" "^1.7.0" + "@types/react-text-truncate" "^0.14.0" + ansi-regex "^6.0.1" + classnames "^2.2.6" + d3-selection "^3.0.0" + d3-shape "^3.0.0" + d3-zoom "^3.0.0" + dagre "^0.8.5" + linkify-react "4.1.3" + linkifyjs "4.1.3" + lodash "^4.17.21" + pluralize "^8.0.0" + qs "^6.9.4" + rc-progress "3.5.1" + react-helmet "6.1.0" + react-hook-form "^7.12.2" + react-idle-timer "5.6.2" + react-markdown "^8.0.0" + react-sparklines "^1.7.0" + react-syntax-highlighter "^15.4.5" + react-text-truncate "^0.19.0" + react-use "^17.3.2" + react-virtualized-auto-sizer "^1.0.11" + react-window "^1.8.6" + remark-gfm "^3.0.1" + zen-observable "^0.10.0" + zod "^3.22.4" + "@backstage/core-plugin-api@^1.8.2": version "1.8.2" resolved "https://registry.yarnpkg.com/@backstage/core-plugin-api/-/core-plugin-api-1.8.2.tgz#1e6f54f0ef1669ffeff56490fbde92c766312230" @@ -2469,6 +2534,18 @@ "@types/react" "^16.13.1 || ^17.0.0" history "^5.0.0" +"@backstage/core-plugin-api@^1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@backstage/core-plugin-api/-/core-plugin-api-1.9.0.tgz#49cda87ab82b968c9c7439da99549a4c34c4f720" + integrity sha512-k+w9TfJCFv/5YyiATuZfnlg/8KkJEL0fo9MHGFcOTOeqX0rcb0eecEWmb2kiA4NfPzLmEeNSSc4Nv8zdRQwCQA== + dependencies: + "@backstage/config" "^1.1.1" + "@backstage/errors" "^1.2.3" + "@backstage/types" "^1.1.1" + "@backstage/version-bridge" "^1.0.7" + "@types/react" "^16.13.1 || ^17.0.0 || ^18.0.0" + history "^5.0.0" + "@backstage/dev-utils@^1.0.26": version "1.0.26" resolved "https://registry.yarnpkg.com/@backstage/dev-utils/-/dev-utils-1.0.26.tgz#b3ca44a6900cd575cc0f2546b10f9865fa436176" @@ -2526,6 +2603,21 @@ zod "^3.22.4" zod-to-json-schema "^3.21.4" +"@backstage/frontend-plugin-api@^0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@backstage/frontend-plugin-api/-/frontend-plugin-api-0.6.0.tgz#849ced607fbf503daed29f4c1ea1e4381e3e0c01" + integrity sha512-09M3ftyZGljxTiCURGSHyPaO/ACBAQEL7iH0Kfq20i3c5ReyUjL/eZ/pgk/MGX7AhPheR98XTeHPD9OACfj+JQ== + dependencies: + "@backstage/core-components" "^0.14.0" + "@backstage/core-plugin-api" "^1.9.0" + "@backstage/types" "^1.1.1" + "@backstage/version-bridge" "^1.0.7" + "@material-ui/core" "^4.12.4" + "@types/react" "^16.13.1 || ^17.0.0 || ^18.0.0" + lodash "^4.17.21" + zod "^3.22.4" + zod-to-json-schema "^3.21.4" + "@backstage/integration-aws-node@^0.1.8": version "0.1.8" resolved "https://registry.yarnpkg.com/@backstage/integration-aws-node/-/integration-aws-node-0.1.8.tgz#c0582a63e2348a42bbe172bdcd4609f024cc0051" @@ -2551,6 +2643,18 @@ "@material-ui/icons" "^4.9.1" "@types/react" "^16.13.1 || ^17.0.0" +"@backstage/integration-react@^1.1.24": + version "1.1.24" + resolved "https://registry.yarnpkg.com/@backstage/integration-react/-/integration-react-1.1.24.tgz#2ae41ca6ad73cf5064bbe988229f0c942ba39198" + integrity sha512-C7aIYFCU14drZx9k0knDZeY4uq4oN5gbI4OVYJtQFVdZlgWwUuycxtw8ar9XAEzIl+UgPcpIpIWsbvOLBb8Qaw== + dependencies: + "@backstage/config" "^1.1.1" + "@backstage/core-plugin-api" "^1.9.0" + "@backstage/integration" "^1.9.0" + "@material-ui/core" "^4.12.2" + "@material-ui/icons" "^4.9.1" + "@types/react" "^16.13.1 || ^17.0.0" + "@backstage/integration@^1.8.0": version "1.8.0" resolved "https://registry.yarnpkg.com/@backstage/integration/-/integration-1.8.0.tgz#affc54e1c12c5a4e68a92de4e42c6cf001bdf6ec" @@ -2565,6 +2669,21 @@ lodash "^4.17.21" luxon "^3.0.0" +"@backstage/integration@^1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@backstage/integration/-/integration-1.9.0.tgz#c60b33a7ec9b3970ccd4e8d54662b686b7ad27bf" + integrity sha512-lqZcjcfLeDyHxDdmTKxiko3GX+vQCyhoNM/lgPFLJFih9TiE3V+hTc9isEfkpQqRE9dCEy1w7rgUrNHXlz0pTA== + dependencies: + "@azure/identity" "^4.0.0" + "@backstage/config" "^1.1.1" + "@backstage/errors" "^1.2.3" + "@octokit/auth-app" "^4.0.0" + "@octokit/rest" "^19.0.3" + cross-fetch "^4.0.0" + git-url-parse "^14.0.0" + lodash "^4.17.21" + luxon "^3.0.0" + "@backstage/plugin-api-docs@^0.10.3": version "0.10.3" resolved "https://registry.yarnpkg.com/@backstage/plugin-api-docs/-/plugin-api-docs-0.10.3.tgz#e2dedad4d8630a1bf8297521d2d3b0bf872718e5" @@ -2874,6 +2993,15 @@ "@backstage/plugin-permission-common" "^0.7.12" "@backstage/plugin-search-common" "^1.2.10" +"@backstage/plugin-catalog-common@^1.0.21": + version "1.0.21" + resolved "https://registry.yarnpkg.com/@backstage/plugin-catalog-common/-/plugin-catalog-common-1.0.21.tgz#1dba78e151079cab0137158b71427276799d4104" + integrity sha512-7VA76TRzeVkfyefDVR01lAfTQnaHw2ZtlvOjIo+tSlteivZ+wEzJVq9af/ekHYlOGuDsYzDzGgc/P/eRwY67Ag== + dependencies: + "@backstage/catalog-model" "^1.4.4" + "@backstage/plugin-permission-common" "^0.7.12" + "@backstage/plugin-search-common" "^1.2.10" + "@backstage/plugin-catalog-graph@^0.3.3": version "0.3.3" resolved "https://registry.yarnpkg.com/@backstage/plugin-catalog-graph/-/plugin-catalog-graph-0.3.3.tgz#fc35db8f9c3ea67560a5ffaff71f9cfe459ed611" @@ -2938,6 +3066,36 @@ "@backstage/plugin-permission-node" "^0.7.20" "@backstage/types" "^1.1.1" +"@backstage/plugin-catalog-react@^1.10.0": + version "1.10.0" + resolved "https://registry.yarnpkg.com/@backstage/plugin-catalog-react/-/plugin-catalog-react-1.10.0.tgz#5c0bab60bd2bf854f4778c111e1f06e2db8ae881" + integrity sha512-xeejxqVp20NCtQIlWrOfvI/scWOefu7PsfQ0Eovqn0dULDVKAJTSgULpdm/AwgJ4E3F46voGw4FE/k5Rlf8Glg== + dependencies: + "@backstage/catalog-client" "^1.6.0" + "@backstage/catalog-model" "^1.4.4" + "@backstage/core-components" "^0.14.0" + "@backstage/core-plugin-api" "^1.9.0" + "@backstage/errors" "^1.2.3" + "@backstage/frontend-plugin-api" "^0.6.0" + "@backstage/integration-react" "^1.1.24" + "@backstage/plugin-catalog-common" "^1.0.21" + "@backstage/plugin-permission-common" "^0.7.12" + "@backstage/plugin-permission-react" "^0.4.20" + "@backstage/types" "^1.1.1" + "@backstage/version-bridge" "^1.0.7" + "@material-ui/core" "^4.12.2" + "@material-ui/icons" "^4.9.1" + "@material-ui/lab" "4.0.0-alpha.61" + "@react-hookz/web" "^24.0.0" + "@types/react" "^16.13.1 || ^17.0.0 || ^18.0.0" + classnames "^2.2.6" + lodash "^4.17.21" + material-ui-popup-state "^1.9.3" + qs "^6.9.4" + react-use "^17.2.4" + yaml "^2.0.0" + zen-observable "^0.10.0" + "@backstage/plugin-catalog-react@^1.9.3": version "1.9.3" resolved "https://registry.yarnpkg.com/@backstage/plugin-catalog-react/-/plugin-catalog-react-1.9.3.tgz#d5910989bc62e1827be00bc4e9650985f2ea338e" @@ -3088,6 +3246,17 @@ "@types/react" "^16.13.1 || ^17.0.0" swr "^2.0.0" +"@backstage/plugin-permission-react@^0.4.20": + version "0.4.20" + resolved "https://registry.yarnpkg.com/@backstage/plugin-permission-react/-/plugin-permission-react-0.4.20.tgz#508bb6bfadaa89a32e891c06bc68b168f10b88bf" + integrity sha512-kP1lmtEtN5XFgPJhnHO5xb++60XyMUmbSjfrT6p+77my3w0qvg8NwGwtg7fingrYJ3pcFGvEgcmL4j7JUfgH7g== + dependencies: + "@backstage/config" "^1.1.1" + "@backstage/core-plugin-api" "^1.9.0" + "@backstage/plugin-permission-common" "^0.7.12" + "@types/react" "^16.13.1 || ^17.0.0 || ^18.0.0" + swr "^2.0.0" + "@backstage/plugin-proxy-backend@^0.4.7": version "0.4.7" resolved "https://registry.yarnpkg.com/@backstage/plugin-proxy-backend/-/plugin-proxy-backend-0.4.7.tgz#4bc5f7f9118ce253063bc6132170340c7a1a4795" @@ -3688,6 +3857,15 @@ "@emotion/styled" "^11.10.5" "@mui/material" "^5.12.2" +"@backstage/theme@^0.5.1": + version "0.5.1" + resolved "https://registry.yarnpkg.com/@backstage/theme/-/theme-0.5.1.tgz#3134874f464990a043127c3fdbc634ea770a911b" + integrity sha512-dVX4xVx9TkNUkubgZqmRjIFTjJeOPRVM9U1aG8S2TRVSUzv9pNK0jDHDN2kyfdSUb9prrC9iEi3+g2lvCwjgKQ== + dependencies: + "@emotion/react" "^11.10.5" + "@emotion/styled" "^11.10.5" + "@mui/material" "^5.12.2" + "@backstage/types@^1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@backstage/types/-/types-1.1.1.tgz#c9ccb30357005e7fb5fa2ac140198059976eb076" @@ -6429,6 +6607,13 @@ dependencies: "@react-hookz/deep-equal" "^1.0.4" +"@react-hookz/web@^24.0.0": + version "24.0.4" + resolved "https://registry.yarnpkg.com/@react-hookz/web/-/web-24.0.4.tgz#7a13d4c2cc65861b926ef6c4452fba00408c8778" + integrity sha512-DcIM6JiZklDyHF6CRD1FTXzuggAkQ+3Ncq2Wln7Kdih8GV6ZIeN9JfS6ZaQxpQUxan8/4n0J2V/R7nMeiSrb2Q== + dependencies: + "@react-hookz/deep-equal" "^1.0.4" + "@remix-run/router@1.15.0": version "1.15.0" resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.15.0.tgz#461a952c2872dd82c8b2e9b74c4dfaff569123e2" @@ -7928,20 +8113,6 @@ resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.0.0.tgz#637bee36f0cabf96a1d436887c90f138a7e9378b" integrity sha512-SYXOBTjJb05rXa2vl55TTwO40A6wKu0R5i1qQwhJYNDIqaIGF7D0HsLw+pJAyi2OvntlEIVusx3xtbbgSUi6zg== -"@testing-library/dom@^8.0.0": - version "8.20.1" - resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.20.1.tgz#2e52a32e46fc88369eef7eef634ac2a192decd9f" - integrity sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/runtime" "^7.12.5" - "@types/aria-query" "^5.0.1" - aria-query "5.1.3" - chalk "^4.1.0" - dom-accessibility-api "^0.5.9" - lz-string "^1.5.0" - pretty-format "^27.0.2" - "@testing-library/dom@^9.0.0": version "9.3.4" resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-9.3.4.tgz#50696ec28376926fec0a1bf87d9dbac5e27f60ce" @@ -7985,16 +8156,15 @@ lodash "^4.17.15" redent "^3.0.0" -"@testing-library/react@^12.1.3": - version "12.1.5" - resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-12.1.5.tgz#bb248f72f02a5ac9d949dea07279095fa577963b" - integrity sha512-OfTXCJUFgjd/digLUuPxa0+/3ZxsQmE7ub9kcbW/wi96Bh3o/p5vrETcBGfP17NWPGqeYYl5LTRpwyGoMC4ysg== +"@testing-library/react-hooks@^8.0.1": + version "8.0.1" + resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-8.0.1.tgz#0924bbd5b55e0c0c0502d1754657ada66947ca12" + integrity sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g== dependencies: "@babel/runtime" "^7.12.5" - "@testing-library/dom" "^8.0.0" - "@types/react-dom" "<18.0.0" + react-error-boundary "^3.1.0" -"@testing-library/react@^14.0.0": +"@testing-library/react@^14.0.0", "@testing-library/react@^14.2.1": version "14.2.1" resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-14.2.1.tgz#bf69aa3f71c36133349976a4a2da3687561d8310" integrity sha512-sGdjws32ai5TLerhvzThYFbpnF9XtL65Cjf+gB0Dhr29BGqK+mAeN7SURSdu+eqgET4ANcWoC7FQpkaiGvBr+A== @@ -8564,13 +8734,6 @@ dependencies: "@types/react" "*" -"@types/react-dom@<18.0.0": - version "17.0.25" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.25.tgz#e0e5b3571e1069625b3a3da2b279379aa33a0cb5" - integrity sha512-urx7A7UxkZQmThYA4So0NelOVjx3V4rNFVJwp0WZlbIK5eM4rNJDiN3R/E9ix0MBh6kAEojk/9YL+Te6D9zHNA== - dependencies: - "@types/react" "^17" - "@types/react-redux@^7.1.20": version "7.1.33" resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.33.tgz#53c5564f03f1ded90904e3c90f77e4bd4dc20b15" @@ -8611,7 +8774,7 @@ "@types/scheduler" "*" csstype "^3.0.2" -"@types/react@^16.13.1 || ^17.0.0", "@types/react@^17": +"@types/react@^16.13.1 || ^17.0.0": version "17.0.75" resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.75.tgz#cffbc76840a12fcadaf5a3cf14878bb06efcf73d" integrity sha512-MSA+NzEzXnQKrqpO63CYqNstFjsESgvJAdAyyJ1n6ZQq/GLgf6nOfIKwk+Twuz0L1N6xPe+qz5xRCJrbhMaLsw== @@ -8620,6 +8783,15 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/react@^16.13.1 || ^17.0.0 || ^18.0.0": + version "18.2.57" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.57.tgz#147b516d8bdb2900219acbfc6f939bdeecca7691" + integrity sha512-ZvQsktJgSYrQiMirAN60y4O/LRevIV8hUzSOSNB6gfR3/o3wCBFQx3sPwIYtuDMeiVgsSS3UzCV26tEzgnfvQw== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + "@types/request@^2.47.1", "@types/request@^2.48.8": version "2.48.12" resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.12.tgz#0f590f615a10f87da18e9790ac94c29ec4c5ef30" @@ -13767,6 +13939,13 @@ git-url-parse@^13.0.0: dependencies: git-up "^7.0.0" +git-url-parse@^14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/git-url-parse/-/git-url-parse-14.0.0.tgz#18ce834726d5fbca0c25a4555101aa277017418f" + integrity sha512-NnLweV+2A4nCvn4U/m2AoYu0pPKlsmhK9cknG7IMwsjFY1S2jxM+mAhsDxyxfCIGfGaD+dozsyX4b6vkYc83yQ== + dependencies: + git-up "^7.0.0" + gitconfiglocal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz#41d045f3851a5ea88f03f24ca1c6178114464b9b" @@ -19986,6 +20165,13 @@ react-double-scrollbar@0.0.15: resolved "https://registry.yarnpkg.com/react-double-scrollbar/-/react-double-scrollbar-0.0.15.tgz#e915ab8cb3b959877075f49436debfdb04288fe4" integrity sha512-dLz3/WBIpgFnzFY0Kb4aIYBMT2BWomHuW2DH6/9jXfS6/zxRRBUFQ04My4HIB7Ma7QoRBpcy8NtkPeFgcGBpgg== +react-error-boundary@^3.1.0: + version "3.1.4" + resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.4.tgz#255db92b23197108757a888b01e5b729919abde0" + integrity sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA== + dependencies: + "@babel/runtime" "^7.12.5" + react-error-overlay@^6.0.11: version "6.0.11" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb" From c037d8dec6c3dcdb800b944a23c59a05917b2f28 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 22 Feb 2024 20:59:35 +0000 Subject: [PATCH 16/18] fix: polyfill AbortSignal.timeout for tests --- .../CoderProvider/CoderProvider.test.tsx | 4 +-- .../src/testHelpers/setup.tsx | 28 ++++++++++++++----- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.test.tsx b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.test.tsx index 735449ff..5c57cdb9 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.test.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.test.tsx @@ -47,7 +47,7 @@ describe(`${CoderProvider.name}`, () => { // just to stabilize the memory reference for the value, and make sure that // memoization caches don't get invalidated too often. This test is just a // safety net to catch what happens if someone forgets - test('Context value will change by reference on re-render if defined inline in a parent', () => { + test('Context value will change by reference on re-render if defined inline inside a parent', () => { const ParentComponent = ({ children }: PropsWithChildren) => { const configThatChangesEachRender = { ...mockAppConfig }; @@ -101,7 +101,7 @@ describe(`${CoderProvider.name}`, () => { }); }; - it('Should clear out the auth token when the user logs out', async () => { + it('Should let the user eject their auth token', async () => { const { result } = renderUseCoderAuth(); act(() => result.current.registerNewToken(mockCoderAuthToken)); diff --git a/plugins/backstage-plugin-coder/src/testHelpers/setup.tsx b/plugins/backstage-plugin-coder/src/testHelpers/setup.tsx index f03c2e5f..dfbad686 100644 --- a/plugins/backstage-plugin-coder/src/testHelpers/setup.tsx +++ b/plugins/backstage-plugin-coder/src/testHelpers/setup.tsx @@ -1,11 +1,11 @@ /* eslint-disable @backstage/no-undeclared-imports -- For test helpers only */ -import { render } from '@testing-library/react'; import { MockErrorApi, TestApiProvider } from '@backstage/test-utils'; import { - RenderHookOptions, - RenderHookResult, + type RenderHookOptions, + type RenderHookResult, + render, renderHook, -} from '@testing-library/react-hooks'; +} from '@testing-library/react'; /* eslint-enable @backstage/no-undeclared-imports */ import React, { ReactElement } from 'react'; @@ -32,9 +32,23 @@ import { getMockConfigApi, mockAuthStates, } from './mockBackstageData'; - import { CoderErrorBoundary } from '../plugin'; +const initialAbortSignalTimeout = AbortSignal.timeout; +beforeAll(() => { + if (!AbortSignal.timeout) { + AbortSignal.timeout = ms => { + const controller = new AbortController(); + setTimeout(() => controller.abort(new DOMException('TimeoutError')), ms); + return controller.signal; + }; + } +}); + +afterAll(() => { + AbortSignal.timeout = initialAbortSignalTimeout; +}); + const afterEachCleanupFunctions: (() => void)[] = []; export function cleanUpAfterEachHelpers() { @@ -197,12 +211,12 @@ type RenderHookAsCoderEntityOptions> = Omit< }; export const renderHookAsCoderEntity = < - TProps extends NonNullable = NonNullable, TReturn = unknown, + TProps extends NonNullable = NonNullable, >( hook: (props: TProps) => TReturn, options?: RenderHookAsCoderEntityOptions, -): RenderHookResult => { +): RenderHookResult => { const { authStatus, ...delegatedOptions } = options ?? {}; const mockErrorApi = getMockErrorApi(); const mockSourceControl = getMockSourceControl(); From c1423492ed8b8a3e6d14bbebaa7ca9cc2d21b4f8 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 22 Feb 2024 23:11:15 +0000 Subject: [PATCH 17/18] fix: resolve all test flakes --- .../CoderProvider/CoderProvider.test.tsx | 14 ++--- .../src/hooks/useBackstageEndpoints.test.ts | 4 +- .../src/hooks/useCoderEntityConfig.test.ts | 4 +- .../src/hooks/useCoderWorkspaces.test.ts | 6 +-- .../src/testHelpers/setup.tsx | 53 +++++++++++-------- 5 files changed, 45 insertions(+), 36 deletions(-) diff --git a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.test.tsx b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.test.tsx index 5c57cdb9..2a240a75 100644 --- a/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.test.tsx +++ b/plugins/backstage-plugin-coder/src/components/CoderProvider/CoderProvider.test.tsx @@ -2,7 +2,7 @@ import React, { PropsWithChildren } from 'react'; import { renderHook } from '@testing-library/react'; import { act, waitFor } from '@testing-library/react'; -import { TestApiProvider } from '@backstage/test-utils'; +import { TestApiProvider, wrapInTestApp } from '@backstage/test-utils'; import { configApiRef, errorApiRef } from '@backstage/core-plugin-api'; import { CoderProvider } from './CoderProvider'; @@ -28,13 +28,13 @@ describe(`${CoderProvider.name}`, () => { }); } - test(`Context hook exposes the same config that the provider has`, () => { - const { result } = renderUseAppConfig(); + test(`Context hook exposes the same config that the provider has`, async () => { + const { result } = await renderUseAppConfig(); expect(result.current).toBe(mockAppConfig); }); - test('Context value remains stable across re-renders if appConfig is defined outside', () => { - const { result, rerender } = renderUseAppConfig(); + test('Context value remains stable across re-renders if appConfig is defined outside', async () => { + const { result, rerender } = await renderUseAppConfig(); expect(result.current).toBe(mockAppConfig); for (let i = 0; i < 10; i++) { @@ -51,7 +51,7 @@ describe(`${CoderProvider.name}`, () => { const ParentComponent = ({ children }: PropsWithChildren) => { const configThatChangesEachRender = { ...mockAppConfig }; - return ( + return wrapInTestApp( { {children} - + , ); }; diff --git a/plugins/backstage-plugin-coder/src/hooks/useBackstageEndpoints.test.ts b/plugins/backstage-plugin-coder/src/hooks/useBackstageEndpoints.test.ts index 1d45d6a4..d245e5d3 100644 --- a/plugins/backstage-plugin-coder/src/hooks/useBackstageEndpoints.test.ts +++ b/plugins/backstage-plugin-coder/src/hooks/useBackstageEndpoints.test.ts @@ -12,8 +12,8 @@ import { } from '../testHelpers/mockBackstageData'; describe(`${useBackstageEndpoints.name}`, () => { - it('Should provide pre-formatted URLs for interacting with Backstage endpoints', () => { - const { result } = renderHookAsCoderEntity(useBackstageEndpoints); + it('Should provide pre-formatted URLs for interacting with Backstage endpoints', async () => { + const { result } = await renderHookAsCoderEntity(useBackstageEndpoints); expect(result.current).toEqual( expect.objectContaining({ diff --git a/plugins/backstage-plugin-coder/src/hooks/useCoderEntityConfig.test.ts b/plugins/backstage-plugin-coder/src/hooks/useCoderEntityConfig.test.ts index 293f4b02..ffc52e57 100644 --- a/plugins/backstage-plugin-coder/src/hooks/useCoderEntityConfig.test.ts +++ b/plugins/backstage-plugin-coder/src/hooks/useCoderEntityConfig.test.ts @@ -108,8 +108,8 @@ describe(`${compileCoderConfig.name}`, () => { }); describe(`${useCoderEntityConfig.name}`, () => { - it('Reads relevant data from CoderProvider, entity, and source control API', () => { - const { result } = renderHookAsCoderEntity(useCoderEntityConfig); + it('Reads relevant data from CoderProvider, entity, and source control API', async () => { + const { result } = await renderHookAsCoderEntity(useCoderEntityConfig); expect(result.current).toEqual( expect.objectContaining>({ diff --git a/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspaces.test.ts b/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspaces.test.ts index e6a1806e..eb4674e1 100644 --- a/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspaces.test.ts +++ b/plugins/backstage-plugin-coder/src/hooks/useCoderWorkspaces.test.ts @@ -14,7 +14,7 @@ afterAll(() => { describe(`${useCoderWorkspaces.name}`, () => { it('Will make a request when provided correct inputs', async () => { - const { result } = renderHookAsCoderEntity(() => { + const { result } = await renderHookAsCoderEntity(() => { return useCoderWorkspaces('owner:me'); }); @@ -22,7 +22,7 @@ describe(`${useCoderWorkspaces.name}`, () => { }); it('Will not be enabled if auth token is missing', async () => { - const { result } = renderHookAsCoderEntity( + const { result } = await renderHookAsCoderEntity( () => useCoderWorkspaces('owner:me'), { authStatus: 'invalid' }, ); @@ -47,7 +47,7 @@ describe(`${useCoderWorkspaces.name}`, () => { }); it('Will only return workspaces for a given repo when a repoConfig is provided', async () => { - const { result } = renderHookAsCoderEntity(() => { + const { result } = await renderHookAsCoderEntity(() => { return useCoderWorkspaces('owner:me', { repoConfig: mockCoderEntityConfig, }); diff --git a/plugins/backstage-plugin-coder/src/testHelpers/setup.tsx b/plugins/backstage-plugin-coder/src/testHelpers/setup.tsx index dfbad686..e8018694 100644 --- a/plugins/backstage-plugin-coder/src/testHelpers/setup.tsx +++ b/plugins/backstage-plugin-coder/src/testHelpers/setup.tsx @@ -1,10 +1,15 @@ /* eslint-disable @backstage/no-undeclared-imports -- For test helpers only */ -import { MockErrorApi, TestApiProvider } from '@backstage/test-utils'; +import { + MockErrorApi, + TestApiProvider, + wrapInTestApp, +} from '@backstage/test-utils'; import { type RenderHookOptions, type RenderHookResult, render, renderHook, + waitFor, } from '@testing-library/react'; /* eslint-enable @backstage/no-undeclared-imports */ @@ -210,39 +215,43 @@ type RenderHookAsCoderEntityOptions> = Omit< authStatus?: CoderAuthStatus; }; -export const renderHookAsCoderEntity = < +export const renderHookAsCoderEntity = async < TReturn = unknown, TProps extends NonNullable = NonNullable, >( hook: (props: TProps) => TReturn, options?: RenderHookAsCoderEntityOptions, -): RenderHookResult => { +): Promise> => { const { authStatus, ...delegatedOptions } = options ?? {}; const mockErrorApi = getMockErrorApi(); const mockSourceControl = getMockSourceControl(); const mockConfigApi = getMockConfigApi(); const mockQueryClient = getMockQueryClient(); - return renderHook(hook, { + const renderHookValue = renderHook(hook, { ...delegatedOptions, - wrapper: ({ children }) => ( - - + wrapInTestApp( + - - <>{children} - - - - ), + + + <>{children} + + + , + ), }); + + await waitFor(() => expect(renderHookValue.result.current).not.toBe(null)); + return renderHookValue; }; From be495b95f52b42de0bad02e16e00b5e94b3cca6a Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 22 Feb 2024 23:24:42 +0000 Subject: [PATCH 18/18] chore: remove unneeded package dependencies --- plugins/backstage-plugin-coder/package.json | 4 +-- yarn.lock | 37 ++------------------- 2 files changed, 3 insertions(+), 38 deletions(-) diff --git a/plugins/backstage-plugin-coder/package.json b/plugins/backstage-plugin-coder/package.json index be023bee..e451bc88 100644 --- a/plugins/backstage-plugin-coder/package.json +++ b/plugins/backstage-plugin-coder/package.json @@ -33,8 +33,6 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", "@tanstack/react-query": "4.36.1", - "@testing-library/react": "^14.2.1", - "@testing-library/react-hooks": "^8.0.1", "react-use": "^17.2.4", "valibot": "^0.28.1" }, @@ -47,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" }, diff --git a/yarn.lock b/yarn.lock index e3ad6804..d3e8e486 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8156,14 +8156,6 @@ lodash "^4.17.15" redent "^3.0.0" -"@testing-library/react-hooks@^8.0.1": - version "8.0.1" - resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-8.0.1.tgz#0924bbd5b55e0c0c0502d1754657ada66947ca12" - integrity sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g== - dependencies: - "@babel/runtime" "^7.12.5" - react-error-boundary "^3.1.0" - "@testing-library/react@^14.0.0", "@testing-library/react@^14.2.1": version "14.2.1" resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-14.2.1.tgz#bf69aa3f71c36133349976a4a2da3687561d8310" @@ -8727,7 +8719,7 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== -"@types/react-dom@*", "@types/react-dom@^18.0.0": +"@types/react-dom@*", "@types/react-dom@^18", "@types/react-dom@^18.0.0": version "18.2.19" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.19.tgz#b84b7c30c635a6c26c6a6dfbb599b2da9788be58" integrity sha512-aZvQL6uUbIJpjZk4U8JZGbau9KDeAwMfmhyWorxgBkqDIEf6ROjRozcmPIicqsUwPUjbkDfHKgGee1Lq65APcA== @@ -8765,25 +8757,7 @@ dependencies: "@types/react" "*" -"@types/react@*": - version "18.2.55" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.55.tgz#38141821b7084404b5013742bc4ae08e44da7a67" - integrity sha512-Y2Tz5P4yz23brwm2d7jNon39qoAtMMmalOQv6+fEFt1mT+FcM3D841wDpoUvFXhaYenuROCy3FZYqdTjM7qVyA== - dependencies: - "@types/prop-types" "*" - "@types/scheduler" "*" - csstype "^3.0.2" - -"@types/react@^16.13.1 || ^17.0.0": - version "17.0.75" - resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.75.tgz#cffbc76840a12fcadaf5a3cf14878bb06efcf73d" - integrity sha512-MSA+NzEzXnQKrqpO63CYqNstFjsESgvJAdAyyJ1n6ZQq/GLgf6nOfIKwk+Twuz0L1N6xPe+qz5xRCJrbhMaLsw== - dependencies: - "@types/prop-types" "*" - "@types/scheduler" "*" - csstype "^3.0.2" - -"@types/react@^16.13.1 || ^17.0.0 || ^18.0.0": +"@types/react@*", "@types/react@^16.13.1 || ^17.0.0", "@types/react@^16.13.1 || ^17.0.0 || ^18.0.0", "@types/react@^18": version "18.2.57" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.57.tgz#147b516d8bdb2900219acbfc6f939bdeecca7691" integrity sha512-ZvQsktJgSYrQiMirAN60y4O/LRevIV8hUzSOSNB6gfR3/o3wCBFQx3sPwIYtuDMeiVgsSS3UzCV26tEzgnfvQw== @@ -20165,13 +20139,6 @@ react-double-scrollbar@0.0.15: resolved "https://registry.yarnpkg.com/react-double-scrollbar/-/react-double-scrollbar-0.0.15.tgz#e915ab8cb3b959877075f49436debfdb04288fe4" integrity sha512-dLz3/WBIpgFnzFY0Kb4aIYBMT2BWomHuW2DH6/9jXfS6/zxRRBUFQ04My4HIB7Ma7QoRBpcy8NtkPeFgcGBpgg== -react-error-boundary@^3.1.0: - version "3.1.4" - resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.4.tgz#255db92b23197108757a888b01e5b729919abde0" - integrity sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA== - dependencies: - "@babel/runtime" "^7.12.5" - react-error-overlay@^6.0.11: version "6.0.11" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb"