diff --git a/plugins/circleci/src/api/index.ts b/plugins/circleci/src/api/index.ts index 89f9623e90..d98f511f35 100644 --- a/plugins/circleci/src/api/index.ts +++ b/plugins/circleci/src/api/index.ts @@ -24,10 +24,11 @@ import { BuildWithSteps, BuildStepAction, BuildSummary, + GitType, } from 'circleci-api'; import { ApiRef } from '@backstage/core'; -export { BuildWithSteps, BuildStepAction, BuildSummary }; +export { BuildWithSteps, BuildStepAction, BuildSummary, GitType }; export const circleCIApiRef = new ApiRef({ id: 'plugin.circleci.service', diff --git a/plugins/circleci/src/components/Store/Store.tsx b/plugins/circleci/src/components/Store/Store.tsx index b83ba8f086..3085480b42 100644 --- a/plugins/circleci/src/components/Store/Store.tsx +++ b/plugins/circleci/src/components/Store/Store.tsx @@ -15,7 +15,7 @@ */ import React, { FC, useReducer, Dispatch } from 'react'; import { circleCIApiRef } from '../../api'; -import { BuildSummary } from 'circleci-api'; +import { BuildSummary, BuildWithSteps } from 'circleci-api'; export const AppContext = React.createContext<[AppState, Dispatch]>( [] as any, @@ -43,9 +43,10 @@ export const STORAGE_KEY = `${circleCIApiRef.id}.settings`; export type AppState = { settings: SettingsState; builds: BuildsState; + buildsWithSteps: BuildsWithStepsState; }; -const initialState = { +const initialState: AppState = { settings: { owner: '', repo: '', @@ -56,6 +57,12 @@ const initialState = { pollingIntervalId: null, pollingState: PollingState.Idle, }, + buildsWithSteps: { + builds: {}, + pollingIntervalId: null, + pollingState: PollingState.Idle, + getBuildError: null, + }, }; type SettingsAction = { @@ -77,7 +84,24 @@ type BuildsAction = payload: number | null; }; -type Action = SettingsAction | BuildsAction; +type BuildsWithStepsAction = + | { + type: 'setBuildWithSteps'; + payload: BuildWithSteps; + } + | { + type: 'setPollingIntervalIdForBuildsWithSteps'; + payload: number | null; + }; + +export type BuildsWithStepsState = { + builds: Record; + pollingIntervalId: number | null; + pollingState: PollingState; + getBuildError: Error | null; +}; + +type Action = SettingsAction | BuildsAction | BuildsWithStepsAction; const reducer = (state: AppState, action: Action): AppState => { switch (action.type) { @@ -91,11 +115,37 @@ const reducer = (state: AppState, action: Action): AppState => { ...state, builds: { ...state.builds, builds: action.payload }, }; + case 'setBuildWithSteps': { + if (state.buildsWithSteps.pollingState !== PollingState.Polling) { + return state; + } + return { + ...state, + buildsWithSteps: { + ...state.buildsWithSteps, + builds: { + ...state.buildsWithSteps.builds, + [action.payload.build_num!]: action.payload, + }, + }, + }; + } case 'setPollingIntervalId': return { ...state, builds: { ...state.builds, + pollingIntervalId: action.payload, + pollingState: + action.payload === null ? PollingState.Idle : PollingState.Polling, + }, + }; + case 'setPollingIntervalIdForBuildsWithSteps': + return { + ...state, + buildsWithSteps: { + ...state.buildsWithSteps, + pollingIntervalId: action.payload, pollingState: action.payload === null ? PollingState.Idle : PollingState.Polling, diff --git a/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx b/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx index 15598cd8f4..f77b4ff5e7 100644 --- a/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx +++ b/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx @@ -14,17 +14,16 @@ * limitations under the License. */ import React, { FC } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; import { useParams } from 'react-router-dom'; -import { Content, InfoCard, useApi } from '@backstage/core'; -import { circleCIApiRef, BuildWithSteps, BuildStepAction } from '../../api'; +import { Content, InfoCard } from '@backstage/core'; +import { BuildWithSteps, BuildStepAction } from '../../api'; import { Grid, Box } from '@material-ui/core'; import { makeStyles } from '@material-ui/core/styles'; import { PluginHeader } from '../../components/PluginHeader'; import { ActionOutput } from './lib/ActionOutput/ActionOutput'; import { Layout } from '../../components/Layout'; -import { Dispatch, iRootState } from '../../state/store'; import { withStore } from '../../components/Store'; +import { useBuildWithSteps } from './hooks'; const BuildName: FC<{ build: BuildWithSteps | null }> = ({ build }) => ( <> @@ -91,17 +90,8 @@ const pickClassName = ( const DetailedViewPage: FC<{}> = () => { const { buildId = '' } = useParams(); const classes = useStyles(); - const dispatch: Dispatch = useDispatch(); - const api = useApi(circleCIApiRef); - React.useEffect(() => { - dispatch.buildWithSteps.startPolling({ api, buildId: Number(buildId) }); - return () => { - dispatch.buildWithSteps.stopPolling(); - }; - }, []); - const { builds } = useSelector((state: iRootState) => state.buildWithSteps); - const build = builds[parseInt(buildId, 10)]; + const [build] = useBuildWithSteps(parseInt(buildId, 10)); return ( diff --git a/plugins/circleci/src/pages/DetailedViewPage/hooks.ts b/plugins/circleci/src/pages/DetailedViewPage/hooks.ts new file mode 100644 index 0000000000..78ac6d8c7a --- /dev/null +++ b/plugins/circleci/src/pages/DetailedViewPage/hooks.ts @@ -0,0 +1,101 @@ +/* + * 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 { errorApiRef, useApi } from '@backstage/core'; +import { useContext, useEffect } from 'react'; +import { circleCIApiRef, GitType } from '../../api/index'; +import { AppContext } from '../../components/Store'; +import { useSettings } from '../SettingsPage/settings'; + +const INTERVAL_AMOUNT = 3000; + +export function useBuildWithSteps(buildId: number) { + const [settings] = useSettings(); + const [{ buildsWithSteps }, dispatch] = useContext(AppContext); + const api = useApi(circleCIApiRef); + const errorApi = useApi(errorApiRef); + + const getBuildWithSteps = async () => { + try { + const options = { + token: settings.token, + vcs: { + owner: settings.owner, + repo: settings.repo, + type: GitType.GITHUB, + }, + }; + const build = await api.getBuild(buildId, options); + dispatch({ type: 'setBuildWithSteps', payload: build }); + } catch (e) { + errorApi.post(e); + } + }; + + const restartBuild = async () => { + try { + await api.retry(buildId, { + token: settings.token, + vcs: { + owner: settings.owner, + repo: settings.repo, + type: GitType.GITHUB, + }, + }); + } catch (e) { + errorApi.post(e); + } + }; + + const startPolling = () => { + stopPolling(); + const intervalId = (setInterval( + () => getBuildWithSteps(), + INTERVAL_AMOUNT, + ) as any) as number; + dispatch({ + type: 'setPollingIntervalIdForBuildsWithSteps', + payload: intervalId, + }); + }; + + const stopPolling = () => { + const currentIntervalId = buildsWithSteps.pollingIntervalId; + if (currentIntervalId) clearInterval(currentIntervalId); + dispatch({ + type: 'setPollingIntervalIdForBuildsWithSteps', + payload: null, + }); + }; + + useEffect(() => { + startPolling(); + return () => { + stopPolling(); + }; + }, [buildId, settings]); + + const build = buildsWithSteps.builds[buildId]; + + return [ + build, + { + restartBuild, + startPolling, + stopPolling, + getBuildWithSteps, + }, + ] as const; +} diff --git a/plugins/circleci/src/state/models/buildWithSteps.ts b/plugins/circleci/src/state/models/buildWithSteps.ts deleted file mode 100644 index 796348f205..0000000000 --- a/plugins/circleci/src/state/models/buildWithSteps.ts +++ /dev/null @@ -1,101 +0,0 @@ -/* - * 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 { Dispatch, iRootState } from '../store'; -import { GitType, BuildWithSteps } from 'circleci-api'; -import { CircleCIApi } from '../../api'; - -export type BuildState = { - builds: Record; - pollingIntervalId: number | null; - pollingState: PollingState; - getBuildError: Error | null; -}; - -const INTERVAL_AMOUNT = 1500; - -export enum PollingState { - Polling, - Idle, -} -export const buildWithSteps = { - state: { - builds: {}, - pollingIntervalId: null, - pollingState: PollingState.Idle, - getBuildError: null, - } as BuildState, - reducers: { - setBuild(state: BuildState, payload: BuildWithSteps) { - if (state.pollingState !== PollingState.Polling) { - return state; - } - return { - ...state, - builds: { ...state.builds, [payload.build_num!]: payload }, - }; - }, - setBuildError(state: BuildState, payload: Error | null) { - return { ...state, getBuildError: payload }; - }, - setPollingIntervalId(state: BuildState, payload: number | null) { - return { - ...state, - pollingIntervalId: payload, - pollingState: - payload === null ? PollingState.Idle : PollingState.Polling, - }; - }, - }, - effects: (dispatch: Dispatch) => ({ - async getBuild( - { api, buildId }: { api: CircleCIApi; buildId: number }, - state: iRootState, - ) { - try { - dispatch.buildWithSteps.setBuildError(null); - const options = { - token: state.settings.token, - vcs: { - owner: state.settings.owner, - repo: state.settings.repo, - type: GitType.GITHUB, - }, - }; - const build = await api.getBuild(buildId, options); - dispatch.buildWithSteps.setBuild(build); - } catch (e) { - dispatch.buildWithSteps.setBuildError(e); - } - }, - startPolling( - { api, buildId }: { api: CircleCIApi; buildId: number }, - state: iRootState, - ) { - if (state.buildWithSteps.pollingIntervalId) return; - - const intervalId = (setInterval( - () => dispatch.buildWithSteps.getBuild({ buildId, api }), - INTERVAL_AMOUNT, - ) as any) as number; - dispatch.buildWithSteps.setPollingIntervalId(intervalId); - }, - stopPolling(_: any, state: iRootState) { - const currentIntervalId = state.buildWithSteps.pollingIntervalId; - if (currentIntervalId) clearInterval(currentIntervalId); - dispatch.buildWithSteps.setPollingIntervalId(null); - }, - }), -}; diff --git a/plugins/circleci/src/state/models/builds.ts b/plugins/circleci/src/state/models/builds.ts deleted file mode 100644 index 6f8a30e868..0000000000 --- a/plugins/circleci/src/state/models/builds.ts +++ /dev/null @@ -1,115 +0,0 @@ -/* - * 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 { Dispatch, iRootState } from '../store'; -import { BuildSummary, GitType } from 'circleci-api'; -import { CircleCIApi } from '../../api'; - -export type BuildsState = { - builds: BuildSummary[]; - pollingIntervalId: number | null; - pollingState: PollingState; - restartBuildError: Error | null; - getBuildsError: Error | null; -}; - -const INTERVAL_AMOUNT = 1500; - -export enum PollingState { - Polling, - Idle, -} -export const builds = { - state: { - builds: [] as BuildSummary[], - pollingIntervalId: null, - pollingState: PollingState.Idle, - restartBuildError: null, - getBuildsError: null, - }, - reducers: { - setBuilds(state: BuildsState, payload: BuildSummary[]) { - if (state.pollingState !== PollingState.Polling) { - return state; - } - return { ...state, builds: payload }; - }, - setPollingIntervalId(state: BuildsState, payload: number | null) { - return { - ...state, - pollingIntervalId: payload, - pollingState: - payload === null ? PollingState.Idle : PollingState.Polling, - }; - }, - setRestartBuildError(state: BuildsState, payload: Error | null) { - return { ...state, restartBuildError: payload }; - }, - setGetBuildsError(state: BuildsState, payload: Error | null) { - return { ...state, getBuildsError: payload }; - }, - }, - effects: (dispatch: Dispatch) => ({ - async getBuilds(api: CircleCIApi, state: iRootState) { - try { - dispatch.builds.setGetBuildsError(null); - const newBuilds = await api.getBuilds({ - token: state.settings.token, - vcs: { - owner: state.settings.owner, - repo: state.settings.repo, - type: GitType.GITHUB, - }, - }); - dispatch.builds.setBuilds(newBuilds); - } catch (e) { - dispatch.builds.setGetBuildsError(null); - } - }, - async restartBuild( - { api, buildId }: { api: CircleCIApi; buildId: number }, - state: iRootState, - ) { - try { - dispatch.builds.setRestartBuildError(null); - await api.retry(buildId, { - token: state.settings.token, - vcs: { - owner: state.settings.owner, - repo: state.settings.repo, - type: GitType.GITHUB, - }, - }); - } catch (e) { - dispatch.builds.setRestartBuildError(e); - } - }, - - startPolling(api: CircleCIApi, state: iRootState) { - if (state.builds.pollingIntervalId) return; - - const intervalId = (setInterval( - () => dispatch.builds.getBuilds(api), - INTERVAL_AMOUNT, - ) as any) as number; - dispatch.builds.setPollingIntervalId(intervalId); - }, - stopPolling(_: any, state: iRootState) { - const currentIntervalId = state.builds.pollingIntervalId; - if (currentIntervalId) clearInterval(currentIntervalId); - dispatch.builds.setPollingIntervalId(null); - }, - }), -}; diff --git a/plugins/circleci/src/state/models/index.ts b/plugins/circleci/src/state/models/index.ts deleted file mode 100644 index 214fd62e6a..0000000000 --- a/plugins/circleci/src/state/models/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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 { builds } from './builds'; -import { buildWithSteps } from './buildWithSteps'; - -// no need to extend from Models -export interface RootModel { - builds: typeof builds; - buildWithSteps: typeof buildWithSteps; -} - -export const models = { - builds, - buildWithSteps, -}; diff --git a/plugins/circleci/src/state/store.ts b/plugins/circleci/src/state/store.ts deleted file mode 100644 index 503a2e9b73..0000000000 --- a/plugins/circleci/src/state/store.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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 { init, RematchRootState, RematchDispatch } from '@rematch/core'; -import { models, RootModel } from './models'; - -export type Dispatch = RematchDispatch; -export type iRootState = RematchRootState; -export default init({ models });