diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index b931442e7d..cd00135004 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -20,6 +20,7 @@ "@material-ui/lab": "4.0.0-alpha.45", "@types/react-lazylog": "^4.5.0", "circleci-api": "^4.0.0", + "fast-deep-equal": "^3.1.1", "moment": "^2.25.3", "react": "16.13.1", "react-dom": "16.13.1", diff --git a/plugins/circleci/src/components/App.tsx b/plugins/circleci/src/components/App.tsx index 701d35ea16..3324753b88 100644 --- a/plugins/circleci/src/components/App.tsx +++ b/plugins/circleci/src/components/App.tsx @@ -1,3 +1,18 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ import React from 'react'; import { Switch, Route } from 'react-router'; import { BuildsPage } from '../pages/BuildsPage'; diff --git a/plugins/circleci/src/state/AppState.tsx b/plugins/circleci/src/state/AppState.tsx index 8c2e46d68b..ecea9e5221 100644 --- a/plugins/circleci/src/state/AppState.tsx +++ b/plugins/circleci/src/state/AppState.tsx @@ -17,6 +17,7 @@ import React, { FC, useReducer, Dispatch, Reducer } from 'react'; import { circleCIApiRef } from '../api'; import { State, Action, SettingsState } from './types'; export { SettingsState }; +import equal from 'fast-deep-equal'; export const AppContext = React.createContext<[State, Dispatch]>( [] as any, @@ -41,6 +42,7 @@ const reducer: Reducer = (state, action) => { settings: { ...state.settings, ...action.payload }, }; case 'setBuilds': + if (equal(action.payload, state.builds)) return state; return { ...state, builds: action.payload, diff --git a/plugins/circleci/src/state/types.ts b/plugins/circleci/src/state/types.ts index 8f4849748f..de801ad213 100644 --- a/plugins/circleci/src/state/types.ts +++ b/plugins/circleci/src/state/types.ts @@ -1,3 +1,18 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ import { BuildSummary, BuildWithSteps } from '../api'; export type SettingsState = { diff --git a/plugins/circleci/src/state/useAsyncPolling.ts b/plugins/circleci/src/state/useAsyncPolling.ts new file mode 100644 index 0000000000..b82d023934 --- /dev/null +++ b/plugins/circleci/src/state/useAsyncPolling.ts @@ -0,0 +1,37 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useRef } from 'react'; + +export const useAsyncPolling = ( + pollingFn: () => Promise, + interval: number, +) => { + const isPolling = useRef(false); + const startPolling = async () => { + if (isPolling.current === true) return; + isPolling.current = true; + + while (isPolling.current === true) { + await pollingFn(); + await new Promise((resolve) => setTimeout(resolve, interval)); + } + }; + + const stopPolling = () => { + isPolling.current = false; + }; + return { isPolling, startPolling, stopPolling }; +}; diff --git a/plugins/circleci/src/state/useBuildWithSteps.ts b/plugins/circleci/src/state/useBuildWithSteps.ts index 22f5309963..cb0a01c8c1 100644 --- a/plugins/circleci/src/state/useBuildWithSteps.ts +++ b/plugins/circleci/src/state/useBuildWithSteps.ts @@ -14,21 +14,25 @@ * limitations under the License. */ import { errorApiRef, useApi } from '@backstage/core'; -import { useContext, useRef } from 'react'; +import { useContext } from 'react'; import { circleCIApiRef, GitType } from '../api/index'; import { AppContext } from '.'; import { useSettings } from './useSettings'; +import { useAsyncPolling } from './useAsyncPolling'; + + const INTERVAL_AMOUNT = 3000; export function useBuildWithSteps(buildId: number) { const [settings] = useSettings(); const [{ buildsWithSteps }, dispatch] = useContext(AppContext); - const intervalId = useRef(null); - const isPolling = intervalId !== null; const api = useApi(circleCIApiRef); const errorApi = useApi(errorApiRef); + const {isPolling, startPolling, stopPolling} = useAsyncPolling(() => getBuildWithSteps(), INTERVAL_AMOUNT); + + const getBuildWithSteps = async () => { try { const options = { @@ -61,19 +65,6 @@ export function useBuildWithSteps(buildId: number) { } }; - const startPolling = () => { - stopPolling(); - intervalId.current = (setInterval( - () => getBuildWithSteps(), - INTERVAL_AMOUNT, - ) as any) as number; - }; - - const stopPolling = () => { - const currentIntervalId = intervalId.current; - if (currentIntervalId) clearInterval(currentIntervalId); - }; - const build = buildsWithSteps[buildId]; return [ diff --git a/plugins/circleci/src/state/useBuilds.tsx b/plugins/circleci/src/state/useBuilds.tsx index 72e8830042..89f85744c8 100644 --- a/plugins/circleci/src/state/useBuilds.tsx +++ b/plugins/circleci/src/state/useBuilds.tsx @@ -15,17 +15,19 @@ */ import { errorApiRef, useApi } from '@backstage/core'; import { GitType } from 'circleci-api'; -import { useContext, useRef } from 'react'; +import { useContext } from 'react'; import { circleCIApiRef } from '../api/index'; import { AppContext } from '.'; +import { useAsyncPolling } from './useAsyncPolling'; const INTERVAL_AMOUNT = 3000; export function useBuilds() { const [{ builds, settings }, dispatch] = useContext(AppContext); - const intervalId = useRef(null); + const api = useApi(circleCIApiRef); const errorApi = useApi(errorApiRef); + const {isPolling, startPolling, stopPolling} = useAsyncPolling(() => getBuilds(), INTERVAL_AMOUNT); const getBuilds = async () => { if (settings.owner === '' || settings.repo === '') return; @@ -38,7 +40,7 @@ export function useBuilds() { type: GitType.GITHUB, }, }); - dispatch({ + if (isPolling) dispatch({ type: 'setBuilds', payload: newBuilds, }); @@ -62,18 +64,7 @@ export function useBuilds() { } }; - const startPolling = () => { - stopPolling(); - intervalId.current = (setInterval( - () => getBuilds(), - INTERVAL_AMOUNT, - ) as unknown) as number; - }; - const stopPolling = () => { - const currentIntervalId = intervalId.current; - if (currentIntervalId) clearInterval(currentIntervalId); - }; return [ builds, diff --git a/plugins/circleci/src/state/useSettings.ts b/plugins/circleci/src/state/useSettings.ts index 68f19ee3b9..34b2f5e402 100644 --- a/plugins/circleci/src/state/useSettings.ts +++ b/plugins/circleci/src/state/useSettings.ts @@ -17,28 +17,9 @@ import { useContext, useEffect } from 'react'; import { AppContext, STORAGE_KEY, SettingsState } from '.'; import { useApi, errorApiRef } from '@backstage/core'; -// type Effect = { -// type: 'rehydrate', -// payload: any -// }; - -// const effects = []; -// pushEffect, popEffect - export function useSettings() { const [{ settings }, dispatch] = useContext(AppContext); - // const interpret = eff => { - // return { - // async rehydrate() {}, - - // }[eff.type](eff.payload) - // } - // useEffect(() => { - // const effectToInterpret = effects[0]; - // removeEffect(0); - // interpret(effectToInterpret) - // },[effects]) const errorApi = useApi(errorApiRef);