From 86350c23a2ceca9ddd071da8c5cdb010405d1c5f Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Tue, 12 May 2020 17:15:05 +0200 Subject: [PATCH 01/11] Break stuff but add useReducer for settings --- .../circleci/src/components/Store/Store.tsx | 73 ++++++++++++++--- .../src/pages/SettingsPage/SettingsPage.tsx | 79 +++++++++++++++---- 2 files changed, 125 insertions(+), 27 deletions(-) diff --git a/plugins/circleci/src/components/Store/Store.tsx b/plugins/circleci/src/components/Store/Store.tsx index 897f19d0ce..b086fe38b1 100644 --- a/plugins/circleci/src/components/Store/Store.tsx +++ b/plugins/circleci/src/components/Store/Store.tsx @@ -13,28 +13,77 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { FC } from 'react'; -import { Provider, useDispatch } from 'react-redux'; +import React, { FC, useReducer, Dispatch } from 'react'; +// import { Provider, useDispatch } from 'react-redux'; -import store, { Dispatch } from '../../state/store'; +// import store from '../../state/store'; +import { circleCIApiRef } from '../../api'; -const RehydrateSettings = () => { - const dispatch: Dispatch = useDispatch(); +// const RehydrateSettings = () => { +// const dispatch: Dispatch = useDispatch(); - React.useEffect(() => { - dispatch.settings.rehydrate(); - }, []); - return null; +// React.useEffect(() => { +// dispatch.settings.rehydrate(); +// }, []); +// return null; +// }; + +export const SettingsContext = React.createContext< + [RootState, Dispatch] +>([] as any); + +type SettingsState = { + owner: string; + repo: string; + token: string; +}; + +export const STORAGE_KEY = `${circleCIApiRef.id}.settings`; + +type RootState = { + settings: SettingsState; +}; + +const initialState = { + settings: { + owner: '', + repo: '', + token: '', + }, +}; + +type Action = { + type: 'setCredentials'; + payload: { + repo: string; + owner: string; + token: string; + }; +}; + +const reducer = (state: RootState, action: Action): RootState => { + switch (action.type) { + case 'setCredentials': + return { + ...state, + settings: { ...state.settings, ...action.payload }, + }; + default: + return state; + } }; export const Store: FC = ({ children }) => { + const [state, dispatch] = useReducer(reducer, initialState); return ( - + + {/* */}
- + {/* */} {children}
-
+ {/*
*/} + ); }; diff --git a/plugins/circleci/src/pages/SettingsPage/SettingsPage.tsx b/plugins/circleci/src/pages/SettingsPage/SettingsPage.tsx index 536535bc85..df402010db 100644 --- a/plugins/circleci/src/pages/SettingsPage/SettingsPage.tsx +++ b/plugins/circleci/src/pages/SettingsPage/SettingsPage.tsx @@ -13,8 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useState } from 'react'; -import { useSelector, useDispatch } from 'react-redux'; +import React, { useState, useContext, useEffect } from 'react'; import { Button, TextField, @@ -28,22 +27,68 @@ import { Alert } from '@material-ui/lab'; import { InfoCard, Content } from '@backstage/core'; import { Layout } from '../../components/Layout'; import { PluginHeader } from '../../components/PluginHeader'; -import { SettingsState } from '../../state/models/settings'; -import { iRootState, Dispatch } from '../../state/store'; -import { withStore } from '../../components/Store'; +// import { SettingsState } from '../../state/models/settings'; +// import { iRootState, Dispatch } from '../../state/store'; +import { + withStore, + SettingsContext, + STORAGE_KEY, +} from '../../components/Store'; const SettingsPage = () => { - const { - token: tokenFromStore, - owner: ownerFromStore, - repo: repoFromStore, - } = useSelector((state: iRootState): SettingsState => state.settings); + // const { + // token: tokenFromStore, + // owner: ownerFromStore, + // repo: repoFromStore, + // } = useSelector((state: iRootState): SettingsState => state.settings); + + const [ + { + settings: { + repo: repoFromStore, + owner: ownerFromStore, + token: tokenFromStore, + }, + settings, + }, + dispatch, + ] = useContext(SettingsContext); + + 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 (e) {} + }; + useEffect(() => { + rehydrate(); + }, []); const [token, setToken] = React.useState(() => tokenFromStore); const [owner, setOwner] = React.useState(() => ownerFromStore); const [repo, setRepo] = React.useState(() => repoFromStore); - const dispatch: Dispatch = useDispatch(); + const persist = () => { + sessionStorage.setItem( + STORAGE_KEY, + JSON.stringify({ + repo, + owner, + token, + }), + ); + }; + + // const dispatch: Dispatch = useDispatch(); React.useEffect(() => { if (tokenFromStore !== token) { @@ -126,10 +171,14 @@ const SettingsPage = () => { color="primary" onClick={() => { setSaved(true); - dispatch.settings.setCredentials({ - owner, - repo, - token, + persist(); + dispatch({ + type: 'setCredentials', + payload: { + repo: repo, + owner: owner, + token: token, + }, }); }} > From 408435eb842dbcbeaa9f042528464a92b390536f Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Tue, 12 May 2020 18:03:39 +0200 Subject: [PATCH 02/11] Move settings effects into a hook --- .../src/pages/SettingsPage/SettingsPage.tsx | 72 ++----------------- .../src/pages/SettingsPage/settings.tsx | 72 +++++++++++++++++++ plugins/circleci/src/state/models/settings.ts | 6 -- 3 files changed, 79 insertions(+), 71 deletions(-) create mode 100644 plugins/circleci/src/pages/SettingsPage/settings.tsx diff --git a/plugins/circleci/src/pages/SettingsPage/SettingsPage.tsx b/plugins/circleci/src/pages/SettingsPage/SettingsPage.tsx index df402010db..6b23207b50 100644 --- a/plugins/circleci/src/pages/SettingsPage/SettingsPage.tsx +++ b/plugins/circleci/src/pages/SettingsPage/SettingsPage.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useState, useContext, useEffect } from 'react'; +import React, { useState } from 'react'; import { Button, TextField, @@ -27,69 +27,19 @@ import { Alert } from '@material-ui/lab'; import { InfoCard, Content } from '@backstage/core'; import { Layout } from '../../components/Layout'; import { PluginHeader } from '../../components/PluginHeader'; -// import { SettingsState } from '../../state/models/settings'; -// import { iRootState, Dispatch } from '../../state/store'; -import { - withStore, - SettingsContext, - STORAGE_KEY, -} from '../../components/Store'; +import { withStore } from '../../components/Store'; +import { useSettings } from './settings'; const SettingsPage = () => { - // const { - // token: tokenFromStore, - // owner: ownerFromStore, - // repo: repoFromStore, - // } = useSelector((state: iRootState): SettingsState => state.settings); - const [ - { - settings: { - repo: repoFromStore, - owner: ownerFromStore, - token: tokenFromStore, - }, - settings, - }, - dispatch, - ] = useContext(SettingsContext); - - 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 (e) {} - }; - useEffect(() => { - rehydrate(); - }, []); + { repo: repoFromStore, owner: ownerFromStore, token: tokenFromStore }, + { saveSettings }, + ] = useSettings(); const [token, setToken] = React.useState(() => tokenFromStore); const [owner, setOwner] = React.useState(() => ownerFromStore); const [repo, setRepo] = React.useState(() => repoFromStore); - const persist = () => { - sessionStorage.setItem( - STORAGE_KEY, - JSON.stringify({ - repo, - owner, - token, - }), - ); - }; - - // const dispatch: Dispatch = useDispatch(); - React.useEffect(() => { if (tokenFromStore !== token) { setToken(tokenFromStore); @@ -171,15 +121,7 @@ const SettingsPage = () => { color="primary" onClick={() => { setSaved(true); - persist(); - dispatch({ - type: 'setCredentials', - payload: { - repo: repo, - owner: owner, - token: token, - }, - }); + saveSettings({ repo, owner, token }); }} > Save credentials diff --git a/plugins/circleci/src/pages/SettingsPage/settings.tsx b/plugins/circleci/src/pages/SettingsPage/settings.tsx new file mode 100644 index 0000000000..3ebd8e9ef0 --- /dev/null +++ b/plugins/circleci/src/pages/SettingsPage/settings.tsx @@ -0,0 +1,72 @@ +/* + * 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 { useContext, useEffect } from 'react'; +import { SettingsContext, STORAGE_KEY } from '../../components/Store'; +import { useApi, errorApiRef } from '@backstage/core'; + +export type SettingsDispatch = { + saveSettings: (settings: SettingsState) => void; +}; + +export type SettingsState = { + token: string; + owner: string; + repo: string; +}; + +export function useSettings(): [SettingsState, SettingsDispatch] { + const [{ settings }, dispatch] = useContext(SettingsContext); + + 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(() => { + rehydrate(); + }, []); + + const persist = (state: SettingsState) => { + sessionStorage.setItem(STORAGE_KEY, JSON.stringify(state)); + }; + + return [ + settings, + { + saveSettings: (state: SettingsState) => { + persist(state); + dispatch({ + type: 'setCredentials', + payload: state, + }); + }, + }, + ]; +} diff --git a/plugins/circleci/src/state/models/settings.ts b/plugins/circleci/src/state/models/settings.ts index 49b261852a..9237b85c0c 100644 --- a/plugins/circleci/src/state/models/settings.ts +++ b/plugins/circleci/src/state/models/settings.ts @@ -18,12 +18,6 @@ import { circleCIApiRef } from '../../api'; const STORAGE_KEY = `${circleCIApiRef.id}.settings`; -export type SettingsState = { - token: string; - owner: string; - repo: string; -}; - export const settings = { state: { token: '', From 061ffc60ac8535bc22e3216b1a9b8e921846f1b0 Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Wed, 13 May 2020 09:14:34 +0200 Subject: [PATCH 03/11] Clean up comments --- .../circleci/src/components/Store/Store.tsx | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/plugins/circleci/src/components/Store/Store.tsx b/plugins/circleci/src/components/Store/Store.tsx index b086fe38b1..6ce6ee5a4a 100644 --- a/plugins/circleci/src/components/Store/Store.tsx +++ b/plugins/circleci/src/components/Store/Store.tsx @@ -14,20 +14,8 @@ * limitations under the License. */ import React, { FC, useReducer, Dispatch } from 'react'; -// import { Provider, useDispatch } from 'react-redux'; - -// import store from '../../state/store'; import { circleCIApiRef } from '../../api'; -// const RehydrateSettings = () => { -// const dispatch: Dispatch = useDispatch(); - -// React.useEffect(() => { -// dispatch.settings.rehydrate(); -// }, []); -// return null; -// }; - export const SettingsContext = React.createContext< [RootState, Dispatch] >([] as any); @@ -77,12 +65,7 @@ export const Store: FC = ({ children }) => { const [state, dispatch] = useReducer(reducer, initialState); return ( - {/* */} -
- {/* */} - {children} -
- {/*
*/} +
{children}
); }; From ba2b19d4c0b3086b378307ebc244d984986f953e Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Wed, 13 May 2020 17:36:02 +0200 Subject: [PATCH 04/11] Add missing file --- .../src/pages/SettingsPage/settings.ts | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 plugins/circleci/src/pages/SettingsPage/settings.ts diff --git a/plugins/circleci/src/pages/SettingsPage/settings.ts b/plugins/circleci/src/pages/SettingsPage/settings.ts new file mode 100644 index 0000000000..7f29b84f67 --- /dev/null +++ b/plugins/circleci/src/pages/SettingsPage/settings.ts @@ -0,0 +1,92 @@ +/* + * 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 { useContext, useEffect } from 'react'; +import { AppContext, STORAGE_KEY } from '../../components/Store'; +import { useApi, errorApiRef } from '@backstage/core'; + +export type SettingsDispatch = { + saveSettings: (settings: SettingsState) => void; +}; + +export type SettingsState = { + token: string; + owner: string; + repo: string; +}; + +// type Effect = { +// type: 'rehydrate', +// payload: any +// }; + +// const effects = []; +// pushEffect, popEffect + +export function useSettings(): [SettingsState, SettingsDispatch] { + 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); + + 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(() => { + rehydrate(); + }, []); + + const persist = (state: SettingsState) => { + sessionStorage.setItem(STORAGE_KEY, JSON.stringify(state)); + }; + + return [ + settings, + { + saveSettings: (state: SettingsState) => { + persist(state); + dispatch({ + type: 'setCredentials', + payload: state, + }); + }, + }, + ]; +} From 7f56215da6c58c7e7ee9ec0df3dc28ea4a7b8b24 Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Wed, 13 May 2020 19:08:41 +0200 Subject: [PATCH 05/11] Add non working settings implemented with hooks --- .../circleci/src/components/Store/Store.tsx | 63 ++++++++-- .../circleci/src/pages/BuildsPage/builds.tsx | 108 ++++++++++++++++++ .../pages/BuildsPage/lib/Builds/Builds.tsx | 33 ++---- .../src/pages/SettingsPage/settings.tsx | 72 ------------ plugins/circleci/src/state/models/index.ts | 3 - plugins/circleci/src/state/models/settings.ts | 69 ----------- 6 files changed, 173 insertions(+), 175 deletions(-) create mode 100644 plugins/circleci/src/pages/BuildsPage/builds.tsx delete mode 100644 plugins/circleci/src/pages/SettingsPage/settings.tsx delete mode 100644 plugins/circleci/src/state/models/settings.ts diff --git a/plugins/circleci/src/components/Store/Store.tsx b/plugins/circleci/src/components/Store/Store.tsx index 6ce6ee5a4a..b83ba8f086 100644 --- a/plugins/circleci/src/components/Store/Store.tsx +++ b/plugins/circleci/src/components/Store/Store.tsx @@ -15,10 +15,11 @@ */ import React, { FC, useReducer, Dispatch } from 'react'; import { circleCIApiRef } from '../../api'; +import { BuildSummary } from 'circleci-api'; -export const SettingsContext = React.createContext< - [RootState, Dispatch] ->([] as any); +export const AppContext = React.createContext<[AppState, Dispatch]>( + [] as any, +); type SettingsState = { owner: string; @@ -26,10 +27,22 @@ type SettingsState = { token: string; }; +export enum PollingState { + Polling, + Idle, +} + +export type BuildsState = { + builds: BuildSummary[]; + pollingIntervalId: number | null; + pollingState: PollingState; +}; + export const STORAGE_KEY = `${circleCIApiRef.id}.settings`; -type RootState = { +export type AppState = { settings: SettingsState; + builds: BuildsState; }; const initialState = { @@ -38,9 +51,14 @@ const initialState = { repo: '', token: '', }, + builds: { + builds: [], + pollingIntervalId: null, + pollingState: PollingState.Idle, + }, }; -type Action = { +type SettingsAction = { type: 'setCredentials'; payload: { repo: string; @@ -49,12 +67,39 @@ type Action = { }; }; -const reducer = (state: RootState, action: Action): RootState => { +type BuildsAction = + | { + type: 'setBuilds'; + payload: BuildSummary[]; + } + | { + type: 'setPollingIntervalId'; + payload: number | null; + }; + +type Action = SettingsAction | BuildsAction; + +const reducer = (state: AppState, action: Action): AppState => { switch (action.type) { case 'setCredentials': return { ...state, - settings: { ...state.settings, ...action.payload }, + settings: { ...state.settings, ...(action.payload as {}) }, + }; + case 'setBuilds': + return { + ...state, + builds: { ...state.builds, builds: action.payload }, + }; + case 'setPollingIntervalId': + return { + ...state, + builds: { + ...state.builds, + pollingIntervalId: action.payload, + pollingState: + action.payload === null ? PollingState.Idle : PollingState.Polling, + }, }; default: return state; @@ -64,9 +109,9 @@ const reducer = (state: RootState, action: Action): RootState => { export const Store: FC = ({ children }) => { const [state, dispatch] = useReducer(reducer, initialState); return ( - +
{children}
-
+ ); }; diff --git a/plugins/circleci/src/pages/BuildsPage/builds.tsx b/plugins/circleci/src/pages/BuildsPage/builds.tsx new file mode 100644 index 0000000000..a40c36bd24 --- /dev/null +++ b/plugins/circleci/src/pages/BuildsPage/builds.tsx @@ -0,0 +1,108 @@ +/* + * 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 { GitType } from 'circleci-api'; +import { useContext, useEffect } from 'react'; +import { CircleCIApi } from '../../api'; +import { circleCIApiRef } from '../../api/index'; +import { AppContext, BuildsState } from '../../components/Store'; + +export type BuildsDispatch = { + startPolling: (api: CircleCIApi) => void; + stopPolling: () => void; + restartBuild: (buildId: number) => Promise; +}; + +const INTERVAL_AMOUNT = 3000; + +export function useBuilds(): [BuildsState, BuildsDispatch] { + const [{ builds, settings }, dispatch] = useContext(AppContext); + const api = useApi(circleCIApiRef); + const errorApi = useApi(errorApiRef); + + const getBuilds = async () => { + console.log(settings); + if (settings.owner === '' || settings.repo === '') return; + try { + const newBuilds = await api.getBuilds({ + token: settings.token, + vcs: { + owner: settings.owner, + repo: settings.repo, + type: GitType.GITHUB, + }, + }); + dispatch({ + type: 'setBuilds', + payload: newBuilds, + }); + } catch (e) { + errorApi.post(e); + } + }; + + const restartBuild = async (buildId: number) => { + 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 = () => { + if (builds.pollingIntervalId) return; + const intervalId = (setInterval( + () => getBuilds(), + INTERVAL_AMOUNT, + ) as any) as number; + dispatch({ + type: 'setPollingIntervalId', + payload: intervalId, + }); + }; + + const stopPolling = () => { + const currentIntervalId = builds.pollingIntervalId; + if (currentIntervalId) clearInterval(currentIntervalId); + dispatch({ + type: 'setPollingIntervalId', + payload: null, + }); + }; + + useEffect(() => { + startPolling(); + return () => { + stopPolling(); + }; + }, []); + + return [ + builds, + { + restartBuild, + startPolling, + stopPolling, + }, + ]; +} diff --git a/plugins/circleci/src/pages/BuildsPage/lib/Builds/Builds.tsx b/plugins/circleci/src/pages/BuildsPage/lib/Builds/Builds.tsx index 90202ef9a2..f52dff30ad 100644 --- a/plugins/circleci/src/pages/BuildsPage/lib/Builds/Builds.tsx +++ b/plugins/circleci/src/pages/BuildsPage/lib/Builds/Builds.tsx @@ -13,12 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { FC, useEffect } from 'react'; -import { useApi } from '@backstage/core'; -import { CITable, CITableBuildInfo } from '../CITable'; -import { useSelector, useDispatch } from 'react-redux'; -import { iRootState, Dispatch } from '../../../../state/store'; -import { circleCIApiRef, BuildSummary } from '../../../../api'; +import React, { FC } from 'react'; +import { CITableBuildInfo, CITable } from '../CITable'; +import { BuildSummary } from '../../../../api'; +import { useBuilds } from '../../builds'; +import { useSettings } from '../../../SettingsPage/settings'; const makeReadableStatus = (status: string | undefined) => { if (!status) return ''; @@ -41,8 +40,7 @@ const makeReadableStatus = (status: string | undefined) => { const transform = ( buildsData: BuildSummary[], - dispatch: Dispatch, - api: typeof circleCIApiRef.T, + restartBuild: { (buildId: number): Promise }, ): CITableBuildInfo[] => { return buildsData.map((buildData) => { const tableBuildInfo: CITableBuildInfo = { @@ -52,7 +50,8 @@ const transform = ( (buildData.retry_of ? ` (retry of #${buildData.retry_of})` : '') : '', onRetryClick: () => - dispatch.builds.restartBuild({ buildId: buildData.build_num, api }), + typeof buildData.build_num !== 'undefined' && + restartBuild(buildData.build_num), source: { branchName: String(buildData.branch), commit: { @@ -68,19 +67,9 @@ const transform = ( }; export const Builds: FC<{}> = () => { - const dispatch: Dispatch = useDispatch(); - const api = useApi(circleCIApiRef); - - useEffect(() => { - dispatch.builds.startPolling(api); - return () => { - dispatch.builds.stopPolling(); - }; - }, []); - - const { builds } = useSelector((state: iRootState) => state.builds); - const { repo, owner } = useSelector((state: iRootState) => state.settings); - const transformedBuilds = transform(builds, dispatch, api); + const [{ builds }, { restartBuild }] = useBuilds(); + const [{ repo, owner }] = useSettings(); + const transformedBuilds = transform(builds, restartBuild); return ( diff --git a/plugins/circleci/src/pages/SettingsPage/settings.tsx b/plugins/circleci/src/pages/SettingsPage/settings.tsx deleted file mode 100644 index 3ebd8e9ef0..0000000000 --- a/plugins/circleci/src/pages/SettingsPage/settings.tsx +++ /dev/null @@ -1,72 +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 { useContext, useEffect } from 'react'; -import { SettingsContext, STORAGE_KEY } from '../../components/Store'; -import { useApi, errorApiRef } from '@backstage/core'; - -export type SettingsDispatch = { - saveSettings: (settings: SettingsState) => void; -}; - -export type SettingsState = { - token: string; - owner: string; - repo: string; -}; - -export function useSettings(): [SettingsState, SettingsDispatch] { - const [{ settings }, dispatch] = useContext(SettingsContext); - - 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(() => { - rehydrate(); - }, []); - - const persist = (state: SettingsState) => { - sessionStorage.setItem(STORAGE_KEY, JSON.stringify(state)); - }; - - return [ - settings, - { - saveSettings: (state: SettingsState) => { - persist(state); - dispatch({ - type: 'setCredentials', - payload: state, - }); - }, - }, - ]; -} diff --git a/plugins/circleci/src/state/models/index.ts b/plugins/circleci/src/state/models/index.ts index 44c87dcc3b..214fd62e6a 100644 --- a/plugins/circleci/src/state/models/index.ts +++ b/plugins/circleci/src/state/models/index.ts @@ -13,19 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { settings } from './settings'; import { builds } from './builds'; import { buildWithSteps } from './buildWithSteps'; // no need to extend from Models export interface RootModel { - settings: typeof settings; builds: typeof builds; buildWithSteps: typeof buildWithSteps; } export const models = { - settings, builds, buildWithSteps, }; diff --git a/plugins/circleci/src/state/models/settings.ts b/plugins/circleci/src/state/models/settings.ts deleted file mode 100644 index 9237b85c0c..0000000000 --- a/plugins/circleci/src/state/models/settings.ts +++ /dev/null @@ -1,69 +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 { circleCIApiRef } from '../../api'; - -const STORAGE_KEY = `${circleCIApiRef.id}.settings`; - -export const settings = { - state: { - token: '', - owner: '', - repo: '', - }, // initial state - reducers: { - setCredentials( - state: SettingsState, - { doPersist, ...credentials }: SettingsState & { doPersist: boolean }, - ) { - return { ...state, ...credentials }; - }, - }, - effects: (dispatch: Dispatch) => ({ - setCredentials({ - doPersist = true, - ...credentials - }: SettingsState & { doPersist: boolean }) { - if (doPersist) dispatch.settings.persist(credentials); - }, - setRehydrateError(state: SettingsState, payload: Error | null) { - return { ...state, rehydrateError: payload }; - }, - persist(credentials: SettingsState) { - sessionStorage.setItem(STORAGE_KEY, JSON.stringify(credentials)); - }, - rehydrate(_: any, state: iRootState) { - try { - dispatch.settings.setRehydrateError(null); - const stateFromStorage = JSON.parse( - sessionStorage.getItem(STORAGE_KEY)!, - ); - if ( - stateFromStorage && - Object.keys(stateFromStorage).some( - (k) => (state as any).settings[k] !== stateFromStorage[k], - ) - ) - dispatch.settings.setCredentials({ - ...stateFromStorage, - doPersist: false, - }); - } catch (e) { - dispatch.settings.setRehydrateError(e); - } - }, - }), -}; From aac4ac0df62b4d884a356b59e04582287cd7c0dd Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Thu, 14 May 2020 10:47:23 +0200 Subject: [PATCH 06/11] Fix builds polling --- plugins/circleci/src/pages/BuildsPage/builds.tsx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/plugins/circleci/src/pages/BuildsPage/builds.tsx b/plugins/circleci/src/pages/BuildsPage/builds.tsx index a40c36bd24..fba1638fb7 100644 --- a/plugins/circleci/src/pages/BuildsPage/builds.tsx +++ b/plugins/circleci/src/pages/BuildsPage/builds.tsx @@ -16,13 +16,10 @@ import { errorApiRef, useApi } from '@backstage/core'; import { GitType } from 'circleci-api'; import { useContext, useEffect } from 'react'; -import { CircleCIApi } from '../../api'; import { circleCIApiRef } from '../../api/index'; import { AppContext, BuildsState } from '../../components/Store'; export type BuildsDispatch = { - startPolling: (api: CircleCIApi) => void; - stopPolling: () => void; restartBuild: (buildId: number) => Promise; }; @@ -34,7 +31,6 @@ export function useBuilds(): [BuildsState, BuildsDispatch] { const errorApi = useApi(errorApiRef); const getBuilds = async () => { - console.log(settings); if (settings.owner === '' || settings.repo === '') return; try { const newBuilds = await api.getBuilds({ @@ -70,7 +66,7 @@ export function useBuilds(): [BuildsState, BuildsDispatch] { }; const startPolling = () => { - if (builds.pollingIntervalId) return; + stopPolling(); const intervalId = (setInterval( () => getBuilds(), INTERVAL_AMOUNT, @@ -95,14 +91,12 @@ export function useBuilds(): [BuildsState, BuildsDispatch] { return () => { stopPolling(); }; - }, []); + }, [settings]); return [ builds, { restartBuild, - startPolling, - stopPolling, }, ]; } From 7b424c0337cdd0ecfc7e8f3ea34804e65c10aa31 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Thu, 14 May 2020 11:38:39 +0200 Subject: [PATCH 07/11] refactor: detailedViewPage moved to hooks Co-authored-by: Nikita Dudnik --- plugins/circleci/src/api/index.ts | 3 +- .../circleci/src/components/Store/Store.tsx | 56 ++++++++- .../DetailedViewPage/DetailedViewPage.tsx | 18 +-- .../src/pages/DetailedViewPage/hooks.ts | 101 +++++++++++++++ .../src/state/models/buildWithSteps.ts | 101 --------------- plugins/circleci/src/state/models/builds.ts | 115 ------------------ plugins/circleci/src/state/models/index.ts | 28 ----- plugins/circleci/src/state/store.ts | 21 ---- 8 files changed, 160 insertions(+), 283 deletions(-) create mode 100644 plugins/circleci/src/pages/DetailedViewPage/hooks.ts delete mode 100644 plugins/circleci/src/state/models/buildWithSteps.ts delete mode 100644 plugins/circleci/src/state/models/builds.ts delete mode 100644 plugins/circleci/src/state/models/index.ts delete mode 100644 plugins/circleci/src/state/store.ts 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 }); From 6a3cc270487f7ce7e8bc816180cc9a09cb42e866 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Thu, 14 May 2020 11:53:25 +0200 Subject: [PATCH 08/11] refactor: remove redux, some rearrangement --- plugins/circleci/package.json | 3 - .../circleci/src/components/Store/Store.tsx | 73 ++----------------- .../circleci/src/components/Store/types.ts | 62 ++++++++++++++++ .../circleci/src/pages/BuildsPage/builds.tsx | 6 +- .../src/pages/SettingsPage/settings.ts | 17 +---- yarn.lock | 29 +------- 6 files changed, 77 insertions(+), 113 deletions(-) create mode 100644 plugins/circleci/src/components/Store/types.ts diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index f13b7a2e74..b931442e7d 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -18,15 +18,12 @@ "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", - "@rematch/core": "^1.4.0", "@types/react-lazylog": "^4.5.0", - "@types/react-redux": "^7.1.8", "circleci-api": "^4.0.0", "moment": "^2.25.3", "react": "16.13.1", "react-dom": "16.13.1", "react-lazylog": "^4.5.2", - "react-redux": "^7.2.0", "react-router": "^5.1.2", "react-router-dom": "^5.1.2", "react-use": "^13.0.0" diff --git a/plugins/circleci/src/components/Store/Store.tsx b/plugins/circleci/src/components/Store/Store.tsx index 3085480b42..6692ef9468 100644 --- a/plugins/circleci/src/components/Store/Store.tsx +++ b/plugins/circleci/src/components/Store/Store.tsx @@ -13,40 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { FC, useReducer, Dispatch } from 'react'; +import React, { FC, useReducer, Dispatch, Reducer } from 'react'; import { circleCIApiRef } from '../../api'; -import { BuildSummary, BuildWithSteps } from 'circleci-api'; +import { State, PollingState, Action, SettingsState } from './types'; +export { SettingsState }; -export const AppContext = React.createContext<[AppState, Dispatch]>( +export const AppContext = React.createContext<[State, Dispatch]>( [] as any, ); - -type SettingsState = { - owner: string; - repo: string; - token: string; -}; - -export enum PollingState { - Polling, - Idle, -} - -export type BuildsState = { - builds: BuildSummary[]; - pollingIntervalId: number | null; - pollingState: PollingState; -}; - export const STORAGE_KEY = `${circleCIApiRef.id}.settings`; -export type AppState = { - settings: SettingsState; - builds: BuildsState; - buildsWithSteps: BuildsWithStepsState; -}; - -const initialState: AppState = { +const initialState: State = { settings: { owner: '', repo: '', @@ -65,45 +42,7 @@ const initialState: AppState = { }, }; -type SettingsAction = { - type: 'setCredentials'; - payload: { - repo: string; - owner: string; - token: string; - }; -}; - -type BuildsAction = - | { - type: 'setBuilds'; - payload: BuildSummary[]; - } - | { - type: 'setPollingIntervalId'; - payload: number | null; - }; - -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 => { +const reducer: Reducer = (state, action) => { switch (action.type) { case 'setCredentials': return { diff --git a/plugins/circleci/src/components/Store/types.ts b/plugins/circleci/src/components/Store/types.ts new file mode 100644 index 0000000000..d01bcaf391 --- /dev/null +++ b/plugins/circleci/src/components/Store/types.ts @@ -0,0 +1,62 @@ +import { BuildSummary, BuildWithSteps } from '../../api'; + +export type SettingsState = { + owner: string; + repo: string; + token: string; +}; + +export enum PollingState { + Polling, + Idle, +} + +export type BuildsState = { + builds: BuildSummary[]; + pollingIntervalId: number | null; + pollingState: PollingState; +}; + +export type State = { + settings: SettingsState; + builds: BuildsState; + buildsWithSteps: BuildsWithStepsState; +}; + +type SettingsAction = { + type: 'setCredentials'; + payload: { + repo: string; + owner: string; + token: string; + }; +}; + +type BuildsAction = + | { + type: 'setBuilds'; + payload: BuildSummary[]; + } + | { + type: 'setPollingIntervalId'; + payload: number | null; + }; + +type BuildsWithStepsAction = + | { + type: 'setBuildWithSteps'; + payload: BuildWithSteps; + } + | { + type: 'setPollingIntervalIdForBuildsWithSteps'; + payload: number | null; + }; + +export type BuildsWithStepsState = { + builds: Record; + pollingIntervalId: number | null; + pollingState: PollingState; + getBuildError: Error | null; +}; + +export type Action = SettingsAction | BuildsAction | BuildsWithStepsAction; diff --git a/plugins/circleci/src/pages/BuildsPage/builds.tsx b/plugins/circleci/src/pages/BuildsPage/builds.tsx index fba1638fb7..9f23e78b31 100644 --- a/plugins/circleci/src/pages/BuildsPage/builds.tsx +++ b/plugins/circleci/src/pages/BuildsPage/builds.tsx @@ -17,7 +17,7 @@ import { errorApiRef, useApi } from '@backstage/core'; import { GitType } from 'circleci-api'; import { useContext, useEffect } from 'react'; import { circleCIApiRef } from '../../api/index'; -import { AppContext, BuildsState } from '../../components/Store'; +import { AppContext } from '../../components/Store'; export type BuildsDispatch = { restartBuild: (buildId: number) => Promise; @@ -25,7 +25,7 @@ export type BuildsDispatch = { const INTERVAL_AMOUNT = 3000; -export function useBuilds(): [BuildsState, BuildsDispatch] { +export function useBuilds() { const [{ builds, settings }, dispatch] = useContext(AppContext); const api = useApi(circleCIApiRef); const errorApi = useApi(errorApiRef); @@ -98,5 +98,5 @@ export function useBuilds(): [BuildsState, BuildsDispatch] { { restartBuild, }, - ]; + ] as const; } diff --git a/plugins/circleci/src/pages/SettingsPage/settings.ts b/plugins/circleci/src/pages/SettingsPage/settings.ts index 7f29b84f67..8c6de913e9 100644 --- a/plugins/circleci/src/pages/SettingsPage/settings.ts +++ b/plugins/circleci/src/pages/SettingsPage/settings.ts @@ -14,19 +14,9 @@ * limitations under the License. */ import { useContext, useEffect } from 'react'; -import { AppContext, STORAGE_KEY } from '../../components/Store'; +import { AppContext, STORAGE_KEY, SettingsState } from '../../components/Store'; import { useApi, errorApiRef } from '@backstage/core'; -export type SettingsDispatch = { - saveSettings: (settings: SettingsState) => void; -}; - -export type SettingsState = { - token: string; - owner: string; - repo: string; -}; - // type Effect = { // type: 'rehydrate', // payload: any @@ -35,7 +25,7 @@ export type SettingsState = { // const effects = []; // pushEffect, popEffect -export function useSettings(): [SettingsState, SettingsDispatch] { +export function useSettings() { const [{ settings }, dispatch] = useContext(AppContext); // const interpret = eff => { @@ -69,6 +59,7 @@ export function useSettings(): [SettingsState, SettingsDispatch] { errorApi.post(error); } }; + useEffect(() => { rehydrate(); }, []); @@ -88,5 +79,5 @@ export function useSettings(): [SettingsState, SettingsDispatch] { }); }, }, - ]; + ] as const; } diff --git a/yarn.lock b/yarn.lock index 665f5dd90b..80ea25a51e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2740,13 +2740,6 @@ prop-types "^15.6.1" react-lifecycles-compat "^3.0.4" -"@rematch/core@^1.4.0": - version "1.4.0" - resolved "https://registry.npmjs.org/@rematch/core/-/core-1.4.0.tgz#686ce814e1cf125029c5e9fba23ef3ab7c3eb2a7" - integrity sha512-1zy9cTYxbvDHP0PwIL1QqkwagCEnqA0uWMmPf8v2BYvLi2OsxIfX1xiV+vCP3sdJAjjZ0b9+IbSmj0DL2MEgLQ== - dependencies: - redux "^4.0.5" - "@rollup/plugin-commonjs@^11.0.2": version "11.0.2" resolved "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-11.0.2.tgz#837cc6950752327cb90177b608f0928a4e60b582" @@ -4090,14 +4083,6 @@ resolved "https://registry.npmjs.org/@types/history/-/history-4.7.5.tgz#527d20ef68571a4af02ed74350164e7a67544860" integrity sha512-wLD/Aq2VggCJXSjxEwrMafIP51Z+13H78nXIX0ABEuIGhmB5sNGbR113MOKo+yfw+RDo1ZU3DM6yfnnRF/+ouw== -"@types/hoist-non-react-statics@^3.3.0": - version "3.3.1" - resolved "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" - integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== - dependencies: - "@types/react" "*" - hoist-non-react-statics "^3.3.0" - "@types/html-minifier@*": version "3.5.3" resolved "https://registry.npmjs.org/@types/html-minifier/-/html-minifier-3.5.3.tgz#5276845138db2cebc54c789e0aaf87621a21e84f" @@ -4343,16 +4328,6 @@ "@types/react" "*" immutable ">=3.8.2" -"@types/react-redux@^7.1.8": - version "7.1.8" - resolved "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.8.tgz#3631feb559f7858d6ad9eea1d6ef41fa64fe7205" - integrity sha512-kpplH7Wg2SYU00sZVT98WBN0ou6QKrYcShRaW+5Vpe5l7bluKWJbWmAL+ieiso07OQzpcP5i1PeY3690640ZWg== - dependencies: - "@types/hoist-non-react-statics" "^3.3.0" - "@types/react" "*" - hoist-non-react-statics "^3.3.0" - redux "^4.0.0" - "@types/react-router-dom@^5.1.3": version "5.1.3" resolved "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.3.tgz#b5d28e7850bd274d944c0fbbe5d57e6b30d71196" @@ -17713,7 +17688,7 @@ react-popper@^1.3.6: typed-styles "^0.0.7" warning "^4.0.2" -react-redux@^7.0.3, react-redux@^7.2.0: +react-redux@^7.0.3: version "7.2.0" resolved "https://registry.npmjs.org/react-redux/-/react-redux-7.2.0.tgz#f970f62192b3981642fec46fd0db18a074fe879d" integrity sha512-EvCAZYGfOLqwV7gh849xy9/pt55rJXPwmYvI4lilPM5rUT/1NxuuN59ipdBksRVSvz0KInbPnp4IfoXJXCqiDA== @@ -18179,7 +18154,7 @@ redeyed@~2.1.0: dependencies: esprima "~4.0.0" -redux@^4.0.0, redux@^4.0.1, redux@^4.0.5: +redux@^4.0.1: version "4.0.5" resolved "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f" integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w== From 34e021ff596ee67226a6f51dd2d202556c45b5e8 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Thu, 14 May 2020 14:32:58 +0200 Subject: [PATCH 09/11] feat: routing --- plugins/circleci/src/components/App.tsx | 20 +++++++++++++++++++ .../circleci/src/components/Store/Store.tsx | 9 ++------- .../src/pages/BuildsPage/BuildsPage.tsx | 3 +-- .../DetailedViewPage/DetailedViewPage.tsx | 3 +-- .../src/pages/SettingsPage/SettingsPage.tsx | 3 +-- plugins/circleci/src/plugin.ts | 8 ++------ 6 files changed, 27 insertions(+), 19 deletions(-) create mode 100644 plugins/circleci/src/components/App.tsx diff --git a/plugins/circleci/src/components/App.tsx b/plugins/circleci/src/components/App.tsx new file mode 100644 index 0000000000..68a02b70be --- /dev/null +++ b/plugins/circleci/src/components/App.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { Switch, Route } from 'react-router'; +import { BuildsPage } from '../pages/BuildsPage'; +import { SettingsPage } from '../pages/SettingsPage'; +import { DetailedViewPage } from '../pages/DetailedViewPage'; +import { Store } from './Store'; + +export const App = () => ( + + + + + + + +); diff --git a/plugins/circleci/src/components/Store/Store.tsx b/plugins/circleci/src/components/Store/Store.tsx index 6692ef9468..f6df1897a0 100644 --- a/plugins/circleci/src/components/Store/Store.tsx +++ b/plugins/circleci/src/components/Store/Store.tsx @@ -97,15 +97,10 @@ const reducer: Reducer = (state, action) => { export const Store: FC = ({ children }) => { const [state, dispatch] = useReducer(reducer, initialState); + return ( -
{children}
+ <>{children}
); }; - -export const withStore = (Component: React.ComponentType) => () => ( - - - -); diff --git a/plugins/circleci/src/pages/BuildsPage/BuildsPage.tsx b/plugins/circleci/src/pages/BuildsPage/BuildsPage.tsx index 700aa1288e..01593ebf4d 100644 --- a/plugins/circleci/src/pages/BuildsPage/BuildsPage.tsx +++ b/plugins/circleci/src/pages/BuildsPage/BuildsPage.tsx @@ -19,7 +19,6 @@ import { Grid } from '@material-ui/core'; import { Builds } from './lib/Builds'; import { Layout } from '../../components/Layout'; import { PluginHeader } from '../../components/PluginHeader'; -import { withStore } from '../../components/Store'; const BuildsPage: FC<{}> = () => ( @@ -34,4 +33,4 @@ const BuildsPage: FC<{}> = () => ( ); -export default withStore(BuildsPage); +export default BuildsPage; diff --git a/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx b/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx index f77b4ff5e7..19509b32d1 100644 --- a/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx +++ b/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx @@ -22,7 +22,6 @@ import { makeStyles } from '@material-ui/core/styles'; import { PluginHeader } from '../../components/PluginHeader'; import { ActionOutput } from './lib/ActionOutput/ActionOutput'; import { Layout } from '../../components/Layout'; -import { withStore } from '../../components/Store'; import { useBuildWithSteps } from './hooks'; const BuildName: FC<{ build: BuildWithSteps | null }> = ({ build }) => ( @@ -144,4 +143,4 @@ const ActionsList: FC<{ actions: BuildStepAction[]; name: string }> = ({ ); }; -export default withStore(DetailedViewPage); +export default DetailedViewPage; diff --git a/plugins/circleci/src/pages/SettingsPage/SettingsPage.tsx b/plugins/circleci/src/pages/SettingsPage/SettingsPage.tsx index 6b23207b50..76f37b955b 100644 --- a/plugins/circleci/src/pages/SettingsPage/SettingsPage.tsx +++ b/plugins/circleci/src/pages/SettingsPage/SettingsPage.tsx @@ -27,7 +27,6 @@ import { Alert } from '@material-ui/lab'; import { InfoCard, Content } from '@backstage/core'; import { Layout } from '../../components/Layout'; import { PluginHeader } from '../../components/PluginHeader'; -import { withStore } from '../../components/Store'; import { useSettings } from './settings'; const SettingsPage = () => { @@ -137,4 +136,4 @@ const SettingsPage = () => { ); }; -export default withStore(SettingsPage); +export default SettingsPage; diff --git a/plugins/circleci/src/plugin.ts b/plugins/circleci/src/plugin.ts index fb42c53835..77a223a162 100644 --- a/plugins/circleci/src/plugin.ts +++ b/plugins/circleci/src/plugin.ts @@ -14,15 +14,11 @@ * limitations under the License. */ import { createPlugin } from '@backstage/core'; -import { BuildsPage } from './pages/BuildsPage'; -import { SettingsPage } from './pages/SettingsPage'; -import { DetailedViewPage } from './pages/DetailedViewPage'; +import { App } from './components/App'; export const plugin = createPlugin({ id: 'circleci', register({ router }) { - router.registerRoute('/circleci', BuildsPage); - router.registerRoute('/circleci/settings', SettingsPage); - router.registerRoute('/circleci/build/:buildId', DetailedViewPage); + router.registerRoute('/circleci', App, { exact: false }); }, }); From bd2ac2c68357cfdf10998d12474b73334babf0b5 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Thu, 14 May 2020 16:05:22 +0200 Subject: [PATCH 10/11] refactor: move state Co-authored-by: Patrik Oldsberg Co-authored-by: Nikita Dudnik --- plugins/circleci/src/components/App.tsx | 6 +- .../circleci/src/components/Store/types.ts | 62 ------------------- .../pages/BuildsPage/lib/Builds/Builds.tsx | 20 ++++-- .../pages/BuildsPage/lib/CITable/CITable.tsx | 4 +- .../DetailedViewPage/DetailedViewPage.tsx | 13 +++- .../src/pages/SettingsPage/SettingsPage.tsx | 2 +- .../Store/Store.tsx => state/AppState.tsx} | 52 +++------------- .../src/{components/Store => state}/index.ts | 5 +- plugins/circleci/src/state/types.ts | 38 ++++++++++++ .../hooks.ts => state/useBuildWithSteps.ts} | 33 +++------- .../builds.tsx => state/useBuilds.tsx} | 34 +++------- .../settings.ts => state/useSettings.ts} | 2 +- 12 files changed, 100 insertions(+), 171 deletions(-) delete mode 100644 plugins/circleci/src/components/Store/types.ts rename plugins/circleci/src/{components/Store/Store.tsx => state/AppState.tsx} (53%) rename plugins/circleci/src/{components/Store => state}/index.ts (82%) create mode 100644 plugins/circleci/src/state/types.ts rename plugins/circleci/src/{pages/DetailedViewPage/hooks.ts => state/useBuildWithSteps.ts} (72%) rename plugins/circleci/src/{pages/BuildsPage/builds.tsx => state/useBuilds.tsx} (75%) rename plugins/circleci/src/{pages/SettingsPage/settings.ts => state/useSettings.ts} (96%) diff --git a/plugins/circleci/src/components/App.tsx b/plugins/circleci/src/components/App.tsx index 68a02b70be..21c710e2b1 100644 --- a/plugins/circleci/src/components/App.tsx +++ b/plugins/circleci/src/components/App.tsx @@ -3,10 +3,10 @@ import { Switch, Route } from 'react-router'; import { BuildsPage } from '../pages/BuildsPage'; import { SettingsPage } from '../pages/SettingsPage'; import { DetailedViewPage } from '../pages/DetailedViewPage'; -import { Store } from './Store'; +import { AppStateProvider } from '../state'; export const App = () => ( - + @@ -16,5 +16,5 @@ export const App = () => ( component={DetailedViewPage} /> - + ); diff --git a/plugins/circleci/src/components/Store/types.ts b/plugins/circleci/src/components/Store/types.ts deleted file mode 100644 index d01bcaf391..0000000000 --- a/plugins/circleci/src/components/Store/types.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { BuildSummary, BuildWithSteps } from '../../api'; - -export type SettingsState = { - owner: string; - repo: string; - token: string; -}; - -export enum PollingState { - Polling, - Idle, -} - -export type BuildsState = { - builds: BuildSummary[]; - pollingIntervalId: number | null; - pollingState: PollingState; -}; - -export type State = { - settings: SettingsState; - builds: BuildsState; - buildsWithSteps: BuildsWithStepsState; -}; - -type SettingsAction = { - type: 'setCredentials'; - payload: { - repo: string; - owner: string; - token: string; - }; -}; - -type BuildsAction = - | { - type: 'setBuilds'; - payload: BuildSummary[]; - } - | { - type: 'setPollingIntervalId'; - payload: number | null; - }; - -type BuildsWithStepsAction = - | { - type: 'setBuildWithSteps'; - payload: BuildWithSteps; - } - | { - type: 'setPollingIntervalIdForBuildsWithSteps'; - payload: number | null; - }; - -export type BuildsWithStepsState = { - builds: Record; - pollingIntervalId: number | null; - pollingState: PollingState; - getBuildError: Error | null; -}; - -export type Action = SettingsAction | BuildsAction | BuildsWithStepsAction; diff --git a/plugins/circleci/src/pages/BuildsPage/lib/Builds/Builds.tsx b/plugins/circleci/src/pages/BuildsPage/lib/Builds/Builds.tsx index f52dff30ad..41001c91dd 100644 --- a/plugins/circleci/src/pages/BuildsPage/lib/Builds/Builds.tsx +++ b/plugins/circleci/src/pages/BuildsPage/lib/Builds/Builds.tsx @@ -13,11 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { FC } from 'react'; +import React, { FC, useEffect } from 'react'; import { CITableBuildInfo, CITable } from '../CITable'; import { BuildSummary } from '../../../../api'; -import { useBuilds } from '../../builds'; -import { useSettings } from '../../../SettingsPage/settings'; +import { useSettings, useBuilds } from '../../../../state'; const makeReadableStatus = (status: string | undefined) => { if (!status) return ''; @@ -49,7 +48,7 @@ const transform = ( ? buildData.subject + (buildData.retry_of ? ` (retry of #${buildData.retry_of})` : '') : '', - onRetryClick: () => + onRestartClick: () => typeof buildData.build_num !== 'undefined' && restartBuild(buildData.build_num), source: { @@ -67,9 +66,18 @@ const transform = ( }; export const Builds: FC<{}> = () => { - const [{ builds }, { restartBuild }] = useBuilds(); + const [ + builds, + { restartBuild: handleRestartBuild, startPolling, stopPolling }, + ] = useBuilds(); const [{ repo, owner }] = useSettings(); - const transformedBuilds = transform(builds, restartBuild); + + useEffect(() => { + startPolling(); + return () => stopPolling(); + }, [repo, owner]); + + const transformedBuilds = transform(builds, handleRestartBuild); return ( diff --git a/plugins/circleci/src/pages/BuildsPage/lib/CITable/CITable.tsx b/plugins/circleci/src/pages/BuildsPage/lib/CITable/CITable.tsx index 5524d9985c..962b3b46b2 100644 --- a/plugins/circleci/src/pages/BuildsPage/lib/CITable/CITable.tsx +++ b/plugins/circleci/src/pages/BuildsPage/lib/CITable/CITable.tsx @@ -46,7 +46,7 @@ export type CITableBuildInfo = { failed: number; testUrl: string; // fixme better name }; - onRetryClick: () => void; + onRestartClick: () => void; }; // retried, canceled, infrastructure_fail, timedout, not_run, running, failed, queued, scheduled, not_running, no_tests, fixed, success @@ -106,7 +106,7 @@ const generatedColumns: TableColumn[] = [ { title: 'Actions', render: (row: Partial) => ( - + ), diff --git a/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx b/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx index 19509b32d1..6e6996af58 100644 --- a/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx +++ b/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { FC } from 'react'; +import React, { FC, useEffect } from 'react'; import { useParams } from 'react-router-dom'; import { Content, InfoCard } from '@backstage/core'; import { BuildWithSteps, BuildStepAction } from '../../api'; @@ -22,7 +22,7 @@ import { makeStyles } from '@material-ui/core/styles'; import { PluginHeader } from '../../components/PluginHeader'; import { ActionOutput } from './lib/ActionOutput/ActionOutput'; import { Layout } from '../../components/Layout'; -import { useBuildWithSteps } from './hooks'; +import { useBuildWithSteps, useSettings } from '../../state'; const BuildName: FC<{ build: BuildWithSteps | null }> = ({ build }) => ( <> @@ -89,8 +89,15 @@ const pickClassName = ( const DetailedViewPage: FC<{}> = () => { const { buildId = '' } = useParams(); const classes = useStyles(); + const [settings] = useSettings(); + const [build, { startPolling, stopPolling }] = useBuildWithSteps( + parseInt(buildId, 10), + ); - const [build] = useBuildWithSteps(parseInt(buildId, 10)); + useEffect(() => { + startPolling(); + return () => stopPolling(); + }, [buildId, settings]); return ( diff --git a/plugins/circleci/src/pages/SettingsPage/SettingsPage.tsx b/plugins/circleci/src/pages/SettingsPage/SettingsPage.tsx index 76f37b955b..62b874d70e 100644 --- a/plugins/circleci/src/pages/SettingsPage/SettingsPage.tsx +++ b/plugins/circleci/src/pages/SettingsPage/SettingsPage.tsx @@ -27,7 +27,7 @@ import { Alert } from '@material-ui/lab'; import { InfoCard, Content } from '@backstage/core'; import { Layout } from '../../components/Layout'; import { PluginHeader } from '../../components/PluginHeader'; -import { useSettings } from './settings'; +import { useSettings } from '../../state'; const SettingsPage = () => { const [ diff --git a/plugins/circleci/src/components/Store/Store.tsx b/plugins/circleci/src/state/AppState.tsx similarity index 53% rename from plugins/circleci/src/components/Store/Store.tsx rename to plugins/circleci/src/state/AppState.tsx index f6df1897a0..8c2e46d68b 100644 --- a/plugins/circleci/src/components/Store/Store.tsx +++ b/plugins/circleci/src/state/AppState.tsx @@ -14,8 +14,8 @@ * limitations under the License. */ import React, { FC, useReducer, Dispatch, Reducer } from 'react'; -import { circleCIApiRef } from '../../api'; -import { State, PollingState, Action, SettingsState } from './types'; +import { circleCIApiRef } from '../api'; +import { State, Action, SettingsState } from './types'; export { SettingsState }; export const AppContext = React.createContext<[State, Dispatch]>( @@ -29,17 +29,8 @@ const initialState: State = { repo: '', token: '', }, - builds: { - builds: [], - pollingIntervalId: null, - pollingState: PollingState.Idle, - }, - buildsWithSteps: { - builds: {}, - pollingIntervalId: null, - pollingState: PollingState.Idle, - getBuildError: null, - }, + builds: [], + buildsWithSteps: {}, }; const reducer: Reducer = (state, action) => { @@ -47,55 +38,28 @@ const reducer: Reducer = (state, action) => { case 'setCredentials': return { ...state, - settings: { ...state.settings, ...(action.payload as {}) }, + settings: { ...state.settings, ...action.payload }, }; case 'setBuilds': return { ...state, - builds: { ...state.builds, builds: action.payload }, + 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, - }, + [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, - }, - }; default: return state; } }; -export const Store: FC = ({ children }) => { +export const AppStateProvider: FC = ({ children }) => { const [state, dispatch] = useReducer(reducer, initialState); return ( diff --git a/plugins/circleci/src/components/Store/index.ts b/plugins/circleci/src/state/index.ts similarity index 82% rename from plugins/circleci/src/components/Store/index.ts rename to plugins/circleci/src/state/index.ts index 8ddb059053..24db02aff4 100644 --- a/plugins/circleci/src/components/Store/index.ts +++ b/plugins/circleci/src/state/index.ts @@ -13,4 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export * from './Store'; +export * from './AppState'; +export * from './useBuildWithSteps'; +export * from './useBuilds'; +export * from './useSettings'; diff --git a/plugins/circleci/src/state/types.ts b/plugins/circleci/src/state/types.ts new file mode 100644 index 0000000000..8f4849748f --- /dev/null +++ b/plugins/circleci/src/state/types.ts @@ -0,0 +1,38 @@ +import { BuildSummary, BuildWithSteps } from '../api'; + +export type SettingsState = { + owner: string; + repo: string; + token: string; +}; + +export type BuildsState = BuildSummary[]; + +export type State = { + settings: SettingsState; + builds: BuildsState; + buildsWithSteps: BuildsWithStepsState; +}; + +type SettingsAction = { + type: 'setCredentials'; + payload: { + repo: string; + owner: string; + token: string; + }; +}; + +type BuildsAction = { + type: 'setBuilds'; + payload: BuildSummary[]; +}; + +type BuildsWithStepsAction = { + type: 'setBuildWithSteps'; + payload: BuildWithSteps; +}; + +export type BuildsWithStepsState = Record; + +export type Action = SettingsAction | BuildsAction | BuildsWithStepsAction; diff --git a/plugins/circleci/src/pages/DetailedViewPage/hooks.ts b/plugins/circleci/src/state/useBuildWithSteps.ts similarity index 72% rename from plugins/circleci/src/pages/DetailedViewPage/hooks.ts rename to plugins/circleci/src/state/useBuildWithSteps.ts index 78ac6d8c7a..22f5309963 100644 --- a/plugins/circleci/src/pages/DetailedViewPage/hooks.ts +++ b/plugins/circleci/src/state/useBuildWithSteps.ts @@ -14,16 +14,18 @@ * 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'; +import { useContext, useRef } from 'react'; +import { circleCIApiRef, GitType } from '../api/index'; +import { AppContext } from '.'; +import { useSettings } from './useSettings'; 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); @@ -38,7 +40,7 @@ export function useBuildWithSteps(buildId: number) { }, }; const build = await api.getBuild(buildId, options); - dispatch({ type: 'setBuildWithSteps', payload: build }); + if (isPolling) dispatch({ type: 'setBuildWithSteps', payload: build }); } catch (e) { errorApi.post(e); } @@ -61,33 +63,18 @@ export function useBuildWithSteps(buildId: number) { const startPolling = () => { stopPolling(); - const intervalId = (setInterval( + intervalId.current = (setInterval( () => getBuildWithSteps(), INTERVAL_AMOUNT, ) as any) as number; - dispatch({ - type: 'setPollingIntervalIdForBuildsWithSteps', - payload: intervalId, - }); }; const stopPolling = () => { - const currentIntervalId = buildsWithSteps.pollingIntervalId; + const currentIntervalId = intervalId.current; if (currentIntervalId) clearInterval(currentIntervalId); - dispatch({ - type: 'setPollingIntervalIdForBuildsWithSteps', - payload: null, - }); }; - useEffect(() => { - startPolling(); - return () => { - stopPolling(); - }; - }, [buildId, settings]); - - const build = buildsWithSteps.builds[buildId]; + const build = buildsWithSteps[buildId]; return [ build, diff --git a/plugins/circleci/src/pages/BuildsPage/builds.tsx b/plugins/circleci/src/state/useBuilds.tsx similarity index 75% rename from plugins/circleci/src/pages/BuildsPage/builds.tsx rename to plugins/circleci/src/state/useBuilds.tsx index 9f23e78b31..72e8830042 100644 --- a/plugins/circleci/src/pages/BuildsPage/builds.tsx +++ b/plugins/circleci/src/state/useBuilds.tsx @@ -15,18 +15,15 @@ */ import { errorApiRef, useApi } from '@backstage/core'; import { GitType } from 'circleci-api'; -import { useContext, useEffect } from 'react'; -import { circleCIApiRef } from '../../api/index'; -import { AppContext } from '../../components/Store'; - -export type BuildsDispatch = { - restartBuild: (buildId: number) => Promise; -}; +import { useContext, useRef } from 'react'; +import { circleCIApiRef } from '../api/index'; +import { AppContext } from '.'; 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); @@ -67,36 +64,23 @@ export function useBuilds() { const startPolling = () => { stopPolling(); - const intervalId = (setInterval( + intervalId.current = (setInterval( () => getBuilds(), INTERVAL_AMOUNT, - ) as any) as number; - dispatch({ - type: 'setPollingIntervalId', - payload: intervalId, - }); + ) as unknown) as number; }; const stopPolling = () => { - const currentIntervalId = builds.pollingIntervalId; + const currentIntervalId = intervalId.current; if (currentIntervalId) clearInterval(currentIntervalId); - dispatch({ - type: 'setPollingIntervalId', - payload: null, - }); }; - useEffect(() => { - startPolling(); - return () => { - stopPolling(); - }; - }, [settings]); - return [ builds, { restartBuild, + startPolling, + stopPolling, }, ] as const; } diff --git a/plugins/circleci/src/pages/SettingsPage/settings.ts b/plugins/circleci/src/state/useSettings.ts similarity index 96% rename from plugins/circleci/src/pages/SettingsPage/settings.ts rename to plugins/circleci/src/state/useSettings.ts index 8c6de913e9..68f19ee3b9 100644 --- a/plugins/circleci/src/pages/SettingsPage/settings.ts +++ b/plugins/circleci/src/state/useSettings.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { useContext, useEffect } from 'react'; -import { AppContext, STORAGE_KEY, SettingsState } from '../../components/Store'; +import { AppContext, STORAGE_KEY, SettingsState } from '.'; import { useApi, errorApiRef } from '@backstage/core'; // type Effect = { From 9ff391681bfe7af53f10017f111a12355a716848 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Thu, 14 May 2020 16:07:46 +0200 Subject: [PATCH 11/11] refactor: renaming --- plugins/circleci/src/components/App.tsx | 2 +- .../BuildWithStepsPage.tsx} | 4 ++-- .../pages/{DetailedViewPage => BuildWithStepsPage}/index.ts | 2 +- .../lib/ActionOutput/ActionOutput.tsx | 4 ++-- .../lib/ActionOutput/index.ts | 0 5 files changed, 6 insertions(+), 6 deletions(-) rename plugins/circleci/src/pages/{DetailedViewPage/DetailedViewPage.tsx => BuildWithStepsPage/BuildWithStepsPage.tsx} (98%) rename plugins/circleci/src/pages/{DetailedViewPage => BuildWithStepsPage}/index.ts (89%) rename plugins/circleci/src/pages/{DetailedViewPage => BuildWithStepsPage}/lib/ActionOutput/ActionOutput.tsx (97%) rename plugins/circleci/src/pages/{DetailedViewPage => BuildWithStepsPage}/lib/ActionOutput/index.ts (100%) diff --git a/plugins/circleci/src/components/App.tsx b/plugins/circleci/src/components/App.tsx index 21c710e2b1..701d35ea16 100644 --- a/plugins/circleci/src/components/App.tsx +++ b/plugins/circleci/src/components/App.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { Switch, Route } from 'react-router'; import { BuildsPage } from '../pages/BuildsPage'; import { SettingsPage } from '../pages/SettingsPage'; -import { DetailedViewPage } from '../pages/DetailedViewPage'; +import { DetailedViewPage } from '../pages/BuildWithStepsPage'; import { AppStateProvider } from '../state'; export const App = () => ( diff --git a/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx b/plugins/circleci/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx similarity index 98% rename from plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx rename to plugins/circleci/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx index 6e6996af58..04a1fba753 100644 --- a/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx +++ b/plugins/circleci/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx @@ -86,7 +86,7 @@ const pickClassName = ( return classes.neutral; }; -const DetailedViewPage: FC<{}> = () => { +const BuildWithStepsPage: FC<{}> = () => { const { buildId = '' } = useParams(); const classes = useStyles(); const [settings] = useSettings(); @@ -150,4 +150,4 @@ const ActionsList: FC<{ actions: BuildStepAction[]; name: string }> = ({ ); }; -export default DetailedViewPage; +export default BuildWithStepsPage; diff --git a/plugins/circleci/src/pages/DetailedViewPage/index.ts b/plugins/circleci/src/pages/BuildWithStepsPage/index.ts similarity index 89% rename from plugins/circleci/src/pages/DetailedViewPage/index.ts rename to plugins/circleci/src/pages/BuildWithStepsPage/index.ts index 40d25c4a84..2fb1f48e12 100644 --- a/plugins/circleci/src/pages/DetailedViewPage/index.ts +++ b/plugins/circleci/src/pages/BuildWithStepsPage/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { default as DetailedViewPage } from './DetailedViewPage'; +export { default as DetailedViewPage } from './BuildWithStepsPage'; diff --git a/plugins/circleci/src/pages/DetailedViewPage/lib/ActionOutput/ActionOutput.tsx b/plugins/circleci/src/pages/BuildWithStepsPage/lib/ActionOutput/ActionOutput.tsx similarity index 97% rename from plugins/circleci/src/pages/DetailedViewPage/lib/ActionOutput/ActionOutput.tsx rename to plugins/circleci/src/pages/BuildWithStepsPage/lib/ActionOutput/ActionOutput.tsx index dd526c1b5f..e8e1bfe958 100644 --- a/plugins/circleci/src/pages/DetailedViewPage/lib/ActionOutput/ActionOutput.tsx +++ b/plugins/circleci/src/pages/BuildWithStepsPage/lib/ActionOutput/ActionOutput.tsx @@ -50,8 +50,8 @@ export const ActionOutput: FC<{ const [messages, setMessages] = useState([]); useEffect(() => { fetch(url) - .then(res => res.json()) - .then(actionOutput => { + .then((res) => res.json()) + .then((actionOutput) => { if (typeof actionOutput !== 'undefined') { setMessages( actionOutput.map(({ message }: { message: string }) => message), diff --git a/plugins/circleci/src/pages/DetailedViewPage/lib/ActionOutput/index.ts b/plugins/circleci/src/pages/BuildWithStepsPage/lib/ActionOutput/index.ts similarity index 100% rename from plugins/circleci/src/pages/DetailedViewPage/lib/ActionOutput/index.ts rename to plugins/circleci/src/pages/BuildWithStepsPage/lib/ActionOutput/index.ts