diff --git a/packages/backend/src/plugins/auth.ts b/packages/backend/src/plugins/auth.ts index c2e349f640..7cf9610dc2 100644 --- a/packages/backend/src/plugins/auth.ts +++ b/packages/backend/src/plugins/auth.ts @@ -17,6 +17,6 @@ import { createRouter } from '@backstage/plugin-auth-backend'; import { PluginEnvironment } from '../types'; -export default async function ({ logger }: PluginEnvironment) { +export default async function createPlugin({ logger }: PluginEnvironment) { return await createRouter({ logger }); } diff --git a/packages/backend/src/plugins/catalog.ts b/packages/backend/src/plugins/catalog.ts index 9f5315fa08..91cbd8c254 100644 --- a/packages/backend/src/plugins/catalog.ts +++ b/packages/backend/src/plugins/catalog.ts @@ -27,7 +27,10 @@ import { import { PluginEnvironment } from '../types'; import { EntityPolicies } from '@backstage/catalog-model'; -export default async function ({ logger, database }: PluginEnvironment) { +export default async function createPlugin({ + logger, + database, +}: PluginEnvironment) { const policy = new EntityPolicies(); const ingestion = new IngestionModels( new LocationReaders(), diff --git a/packages/backend/src/plugins/identity.ts b/packages/backend/src/plugins/identity.ts index 26276afd01..63a326965c 100644 --- a/packages/backend/src/plugins/identity.ts +++ b/packages/backend/src/plugins/identity.ts @@ -17,6 +17,6 @@ import { createRouter } from '@backstage/plugin-identity-backend'; import { PluginEnvironment } from '../types'; -export default async function ({ logger }: PluginEnvironment) { +export default async function createPlugin({ logger }: PluginEnvironment) { return await createRouter({ logger }); } diff --git a/packages/backend/src/plugins/scaffolder.ts b/packages/backend/src/plugins/scaffolder.ts index 311c9197aa..08e700bc74 100644 --- a/packages/backend/src/plugins/scaffolder.ts +++ b/packages/backend/src/plugins/scaffolder.ts @@ -21,7 +21,7 @@ import { } from '@backstage/plugin-scaffolder-backend'; import type { PluginEnvironment } from '../types'; -export default async function ({ logger }: PluginEnvironment) { +export default async function createPlugin({ logger }: PluginEnvironment) { const storage = new DiskStorage({ logger }); const templater = new CookieCutter(); diff --git a/packages/backend/src/plugins/sentry.ts b/packages/backend/src/plugins/sentry.ts index 34506ee3de..89ee153faf 100644 --- a/packages/backend/src/plugins/sentry.ts +++ b/packages/backend/src/plugins/sentry.ts @@ -17,6 +17,6 @@ import { createRouter } from '@backstage/plugin-sentry-backend'; import { Logger } from 'winston'; -export default async function (logger: Logger) { +export default async function createPlugin(logger: Logger) { return await createRouter(logger); } diff --git a/packages/core-api/src/app/AppThemeProvider.tsx b/packages/core-api/src/app/AppThemeProvider.tsx index 775d8293ba..6bbcaea93a 100644 --- a/packages/core-api/src/app/AppThemeProvider.tsx +++ b/packages/core-api/src/app/AppThemeProvider.tsx @@ -49,10 +49,6 @@ function resolveTheme( } const useShouldPreferDarkTheme = () => { - if (!window.matchMedia) { - return false; - } - const mediaQuery = useMemo( () => window.matchMedia('(prefers-color-scheme: dark)'), [], @@ -74,12 +70,16 @@ const useShouldPreferDarkTheme = () => { export const AppThemeProvider: FC<{}> = ({ children }) => { const appThemeApi = useApi(appThemeApiRef); - const shouldPreferDark = useShouldPreferDarkTheme(); const themeId = useObservable( appThemeApi.activeThemeId$(), appThemeApi.getActiveThemeId(), ); + // Browser feature detection won't change over time, so ignore lint rule + const shouldPreferDark = Boolean(window.matchMedia) + ? useShouldPreferDarkTheme() // eslint-disable-line react-hooks/rules-of-hooks + : false; + const appTheme = resolveTheme( themeId, shouldPreferDark, diff --git a/packages/core/src/components/FeatureDiscovery/FeatureCalloutCircular.tsx b/packages/core/src/components/FeatureDiscovery/FeatureCalloutCircular.tsx index a3b40d5b88..38e422dbcc 100644 --- a/packages/core/src/components/FeatureDiscovery/FeatureCalloutCircular.tsx +++ b/packages/core/src/components/FeatureDiscovery/FeatureCalloutCircular.tsx @@ -141,9 +141,9 @@ export const FeatureCalloutCircular: FC = ({ window.removeEventListener('resize', update); window.removeEventListener('scroll', update); }; - }, []); + }, [update]); - useLayoutEffect(update, [wrapperRef.current]); + useLayoutEffect(update, [wrapperRef.current, update]); if (!show) { return <>{children}; diff --git a/packages/core/src/components/FeatureDiscovery/lib/usePortal.ts b/packages/core/src/components/FeatureDiscovery/lib/usePortal.ts index 6031a25f1c..d5fd2c23c9 100644 --- a/packages/core/src/components/FeatureDiscovery/lib/usePortal.ts +++ b/packages/core/src/components/FeatureDiscovery/lib/usePortal.ts @@ -51,27 +51,30 @@ function addRootElement(rootElem: Element): void { export function usePortal(id: string): HTMLElement { const rootElemRef = useRef(null); - useEffect(function setupElement() { - // Look for existing target dom element to append to - const existingParent = document.querySelector(`#${id}`); - // Parent is either a new root or the existing dom element - const parentElem = existingParent || createRootElement(id); + useEffect( + function setupElement() { + // Look for existing target dom element to append to + const existingParent = document.querySelector(`#${id}`); + // Parent is either a new root or the existing dom element + const parentElem = existingParent || createRootElement(id); - // If there is no existing DOM element, add a new one. - if (!existingParent) { - addRootElement(parentElem); - } - - // Add the detached element to the parent - parentElem.appendChild(rootElemRef.current!); - - return function removeElement() { - rootElemRef.current!.remove(); - if (parentElem.childNodes.length === -1) { - parentElem.remove(); + // If there is no existing DOM element, add a new one. + if (!existingParent) { + addRootElement(parentElem); } - }; - }, []); + + // Add the detached element to the parent + parentElem.appendChild(rootElemRef.current!); + + return function removeElement() { + rootElemRef.current!.remove(); + if (parentElem.childNodes.length === -1) { + parentElem.remove(); + } + }; + }, + [id], + ); /** * It's important we evaluate this lazily: diff --git a/packages/core/src/components/FeatureDiscovery/lib/useShowCallout.ts b/packages/core/src/components/FeatureDiscovery/lib/useShowCallout.ts index 047473a7e2..0bbcf3b8ec 100644 --- a/packages/core/src/components/FeatureDiscovery/lib/useShowCallout.ts +++ b/packages/core/src/components/FeatureDiscovery/lib/useShowCallout.ts @@ -45,7 +45,7 @@ function useCalloutHasBeenSeen( const markSeen = useCallback(() => { setState(featureId, true); - }, [featureId]); + }, [setState, featureId]); return { seen: states[featureId] === true, markSeen }; } diff --git a/packages/core/src/components/ProgressBars/HorizontalProgress.tsx b/packages/core/src/components/ProgressBars/HorizontalProgress.tsx index 7575c5a9b2..d7f813f55f 100644 --- a/packages/core/src/components/ProgressBars/HorizontalProgress.tsx +++ b/packages/core/src/components/ProgressBars/HorizontalProgress.tsx @@ -29,6 +29,7 @@ type Props = { }; const HorizontalProgress: FC = ({ value }) => { + const theme = useTheme(); if (isNaN(value)) { return null; } @@ -36,7 +37,6 @@ const HorizontalProgress: FC = ({ value }) => { if (percent > 100) { percent = 100; } - const theme = useTheme(); const strokeColor = getProgressColor(theme.palette, percent, false, 100); return ( diff --git a/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx b/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx index e21c4dbce9..f580a84d8a 100644 --- a/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx +++ b/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx @@ -18,6 +18,7 @@ import { render } from '@testing-library/react'; import * as React from 'react'; import { wrapInTestApp } from '@backstage/test-utils'; import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core'; +import { catalogApiRef, CatalogApi } from '../../api/types'; const getTestProps = (componentName: string) => { return { @@ -39,7 +40,17 @@ describe('ComponentPage', () => { const props = getTestProps(''); await render( wrapInTestApp( - + , ), diff --git a/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx b/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx index 0ee0e1d09a..62f34f1620 100644 --- a/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx +++ b/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx @@ -53,11 +53,6 @@ const ComponentPage: FC = ({ match, history }) => { const componentName = match.params.name; const errorApi = useApi(errorApiRef); - if (componentName === '') { - history.push('/catalog'); - return null; - } - const catalogApi = useApi(catalogApiRef); const catalogRequest = useAsync(() => catalogApi.getEntityByName(match.params.name), @@ -70,7 +65,12 @@ const ComponentPage: FC = ({ match, history }) => { history.push('/catalog'); }, REDIRECT_DELAY); } - }, [catalogRequest.error]); + }, [catalogRequest.error, errorApi, history]); + + if (componentName === '') { + history.push('/catalog'); + return null; + } const removeComponent = async () => { setConfirmationDialogOpen(false); diff --git a/plugins/circleci/src/components/Settings/Settings.tsx b/plugins/circleci/src/components/Settings/Settings.tsx index f0ed3f2ceb..8897c1eb40 100644 --- a/plugins/circleci/src/components/Settings/Settings.tsx +++ b/plugins/circleci/src/components/Settings/Settings.tsx @@ -52,7 +52,7 @@ const Settings = () => { if (repoFromStore !== repo) { setRepo(repoFromStore); } - }, [ownerFromStore, repoFromStore, tokenFromStore]); + }, [ownerFromStore, repoFromStore, tokenFromStore, token, owner, repo]); const [saved, setSaved] = useState(false); diff --git a/plugins/circleci/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx b/plugins/circleci/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx index 75d09610b5..77e6daf755 100644 --- a/plugins/circleci/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx +++ b/plugins/circleci/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx @@ -111,7 +111,7 @@ const BuildWithStepsView: FC<{}> = () => { useEffect(() => { startPolling(); return () => stopPolling(); - }, [buildId, settings]); + }, [buildId, settings, startPolling, stopPolling]); return ( <> diff --git a/plugins/circleci/src/state/useBuildWithSteps.ts b/plugins/circleci/src/state/useBuildWithSteps.ts index 7aba770851..8fa7fa896d 100644 --- a/plugins/circleci/src/state/useBuildWithSteps.ts +++ b/plugins/circleci/src/state/useBuildWithSteps.ts @@ -46,7 +46,7 @@ export function useBuildWithSteps(buildId: number) { errorApi.post(e); return Promise.reject(e); } - }, [token, owner, repo, buildId]); + }, [token, owner, repo, buildId, api, errorApi]); const restartBuild = async () => { try { diff --git a/plugins/circleci/src/state/useBuilds.ts b/plugins/circleci/src/state/useBuilds.ts index f90311b56b..6d38c5f901 100644 --- a/plugins/circleci/src/state/useBuilds.ts +++ b/plugins/circleci/src/state/useBuilds.ts @@ -101,7 +101,7 @@ export function useBuilds() { return Promise.reject(e); } }, - [repo, token, owner], + [repo, token, owner, api, errorApi], ); const restartBuild = async (buildId: number) => { @@ -121,7 +121,7 @@ export function useBuilds() { useEffect(() => { getBuilds({ limit: 1, offset: 0 }).then(b => setTotal(b?.[0].build_num!)); - }, [repo]); + }, [repo, getBuilds]); const { loading, value, retry } = useAsyncRetry( () => diff --git a/plugins/circleci/src/state/useSettings.ts b/plugins/circleci/src/state/useSettings.ts index c8dc9ce39b..3cc58a65bd 100644 --- a/plugins/circleci/src/state/useSettings.ts +++ b/plugins/circleci/src/state/useSettings.ts @@ -23,27 +23,29 @@ export function useSettings() { const errorApi = useApi(errorApiRef); - const rehydrate = () => { - try { - const stateFromStorage = JSON.parse(sessionStorage.getItem(STORAGE_KEY)!); - if ( - stateFromStorage && - Object.keys(stateFromStorage).some( - k => (settings as any)[k] !== stateFromStorage[k], - ) - ) - dispatch({ - type: 'setCredentials', - payload: stateFromStorage, - }); - } catch (error) { - errorApi.post(error); - } - }; - useEffect(() => { + const rehydrate = () => { + try { + const stateFromStorage = JSON.parse( + sessionStorage.getItem(STORAGE_KEY)!, + ); + if ( + stateFromStorage && + Object.keys(stateFromStorage).some( + k => (settings as any)[k] !== stateFromStorage[k], + ) + ) + dispatch({ + type: 'setCredentials', + payload: stateFromStorage, + }); + } catch (error) { + errorApi.post(error); + } + }; + rehydrate(); - }, []); + }, [dispatch, errorApi, settings]); const persist = (state: Settings) => { sessionStorage.setItem(STORAGE_KEY, JSON.stringify(state)); diff --git a/plugins/lighthouse/src/components/AuditList/index.tsx b/plugins/lighthouse/src/components/AuditList/index.tsx index 85be31cc80..4dbbb05295 100644 --- a/plugins/lighthouse/src/components/AuditList/index.tsx +++ b/plugins/lighthouse/src/components/AuditList/index.tsx @@ -63,7 +63,7 @@ const AuditList: FC<{}> = () => { if (value?.total && value?.limit) return Math.ceil(value?.total / value?.limit); return 0; - }, [value]); + }, [value?.total, value?.limit]); const history = useHistory(); diff --git a/plugins/sentry/src/components/SentryPluginWidget/SentryPluginWidget.tsx b/plugins/sentry/src/components/SentryPluginWidget/SentryPluginWidget.tsx index 5049905966..b101a9092a 100644 --- a/plugins/sentry/src/components/SentryPluginWidget/SentryPluginWidget.tsx +++ b/plugins/sentry/src/components/SentryPluginWidget/SentryPluginWidget.tsx @@ -43,7 +43,7 @@ export const SentryPluginWidget: FC<{ if (error) { errorApi.post(error); } - }, [error]); + }, [error, errorApi]); if (loading) { return ( diff --git a/plugins/tech-radar/src/components/RadarComponent.tsx b/plugins/tech-radar/src/components/RadarComponent.tsx index 7a6c4fedaf..981d1f1369 100644 --- a/plugins/tech-radar/src/components/RadarComponent.tsx +++ b/plugins/tech-radar/src/components/RadarComponent.tsx @@ -21,6 +21,7 @@ import { TechRadarComponentProps, TechRadarLoaderResponse } from '../api'; import getSampleData from '../sampleData'; const useTechRadarLoader = (props: TechRadarComponentProps) => { + const errorApi = useApi(errorApiRef); const [state, setState] = useState<{ loading: boolean; error?: Error; @@ -31,38 +32,33 @@ const useTechRadarLoader = (props: TechRadarComponentProps) => { data: undefined, }); + const { getData } = props; + useEffect(() => { - if (!props.getData) { + if (!getData) { return; } - props - .getData() + getData() .then((payload: TechRadarLoaderResponse) => { setState({ loading: false, error: undefined, data: payload }); }) .catch((err: Error) => { + errorApi.post(err); setState({ loading: false, error: err, data: undefined, }); }); - }, []); + }, [getData, errorApi]); return state; }; -const RadarComponent: FC = (props) => { - const errorApi = useApi(errorApiRef); +const RadarComponent: FC = props => { const { loading, error, data } = useTechRadarLoader(props); - useEffect(() => { - if (error) { - errorApi.post(error); - } - }, [error && error.message]); - return ( <> {loading && }