From 7f56215da6c58c7e7ee9ec0df3dc28ea4a7b8b24 Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Wed, 13 May 2020 19:08:41 +0200 Subject: [PATCH] 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); - } - }, - }), -};