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/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/App.tsx b/plugins/circleci/src/components/App.tsx new file mode 100644 index 0000000000..701d35ea16 --- /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/BuildWithStepsPage'; +import { AppStateProvider } from '../state'; + +export const App = () => ( + + + + + + + +); diff --git a/plugins/circleci/src/components/Store/Store.tsx b/plugins/circleci/src/components/Store/Store.tsx deleted file mode 100644 index 897f19d0ce..0000000000 --- a/plugins/circleci/src/components/Store/Store.tsx +++ /dev/null @@ -1,45 +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 React, { FC } from 'react'; -import { Provider, useDispatch } from 'react-redux'; - -import store, { Dispatch } from '../../state/store'; - -const RehydrateSettings = () => { - const dispatch: Dispatch = useDispatch(); - - React.useEffect(() => { - dispatch.settings.rehydrate(); - }, []); - return null; -}; - -export const Store: FC = ({ children }) => { - return ( - -
- - {children} -
-
- ); -}; - -export const withStore = (Component: React.ComponentType) => () => ( - - - -); diff --git a/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx b/plugins/circleci/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx similarity index 81% rename from plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx rename to plugins/circleci/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx index 15598cd8f4..04a1fba753 100644 --- a/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx +++ b/plugins/circleci/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx @@ -13,18 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { FC } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import React, { FC, useEffect } from 'react'; 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, useSettings } from '../../state'; const BuildName: FC<{ build: BuildWithSteps | null }> = ({ build }) => ( <> @@ -88,20 +86,18 @@ const pickClassName = ( return classes.neutral; }; -const DetailedViewPage: FC<{}> = () => { +const BuildWithStepsPage: FC<{}> = () => { const { buildId = '' } = useParams(); const classes = useStyles(); - const dispatch: Dispatch = useDispatch(); - const api = useApi(circleCIApiRef); + const [settings] = useSettings(); + const [build, { startPolling, stopPolling }] = useBuildWithSteps( + parseInt(buildId, 10), + ); - 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)]; + useEffect(() => { + startPolling(); + return () => stopPolling(); + }, [buildId, settings]); return ( @@ -154,4 +150,4 @@ const ActionsList: FC<{ actions: BuildStepAction[]; name: string }> = ({ ); }; -export default withStore(DetailedViewPage); +export default BuildWithStepsPage; diff --git a/plugins/circleci/src/components/Store/index.ts b/plugins/circleci/src/pages/BuildWithStepsPage/index.ts similarity index 89% rename from plugins/circleci/src/components/Store/index.ts rename to plugins/circleci/src/pages/BuildWithStepsPage/index.ts index 8ddb059053..2fb1f48e12 100644 --- a/plugins/circleci/src/components/Store/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 * from './Store'; +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 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/BuildsPage/lib/Builds/Builds.tsx b/plugins/circleci/src/pages/BuildsPage/lib/Builds/Builds.tsx index 90202ef9a2..41001c91dd 100644 --- a/plugins/circleci/src/pages/BuildsPage/lib/Builds/Builds.tsx +++ b/plugins/circleci/src/pages/BuildsPage/lib/Builds/Builds.tsx @@ -14,11 +14,9 @@ * 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 { CITableBuildInfo, CITable } from '../CITable'; +import { BuildSummary } from '../../../../api'; +import { useSettings, useBuilds } from '../../../../state'; const makeReadableStatus = (status: string | undefined) => { if (!status) return ''; @@ -41,8 +39,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 = { @@ -51,8 +48,9 @@ const transform = ( ? buildData.subject + (buildData.retry_of ? ` (retry of #${buildData.retry_of})` : '') : '', - onRetryClick: () => - dispatch.builds.restartBuild({ buildId: buildData.build_num, api }), + onRestartClick: () => + typeof buildData.build_num !== 'undefined' && + restartBuild(buildData.build_num), source: { branchName: String(buildData.branch), commit: { @@ -68,19 +66,18 @@ const transform = ( }; export const Builds: FC<{}> = () => { - const dispatch: Dispatch = useDispatch(); - const api = useApi(circleCIApiRef); + const [ + builds, + { restartBuild: handleRestartBuild, startPolling, stopPolling }, + ] = useBuilds(); + const [{ repo, owner }] = useSettings(); useEffect(() => { - dispatch.builds.startPolling(api); - return () => { - dispatch.builds.stopPolling(); - }; - }, []); + startPolling(); + return () => stopPolling(); + }, [repo, owner]); - const { builds } = useSelector((state: iRootState) => state.builds); - const { repo, owner } = useSelector((state: iRootState) => state.settings); - const transformedBuilds = transform(builds, dispatch, api); + 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/SettingsPage/SettingsPage.tsx b/plugins/circleci/src/pages/SettingsPage/SettingsPage.tsx index 536535bc85..62b874d70e 100644 --- a/plugins/circleci/src/pages/SettingsPage/SettingsPage.tsx +++ b/plugins/circleci/src/pages/SettingsPage/SettingsPage.tsx @@ -14,7 +14,6 @@ * limitations under the License. */ import React, { useState } from 'react'; -import { useSelector, useDispatch } from 'react-redux'; import { Button, TextField, @@ -28,23 +27,18 @@ 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 { useSettings } from '../../state'; const SettingsPage = () => { - const { - token: tokenFromStore, - owner: ownerFromStore, - repo: repoFromStore, - } = useSelector((state: iRootState): SettingsState => state.settings); + const [ + { 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 dispatch: Dispatch = useDispatch(); - React.useEffect(() => { if (tokenFromStore !== token) { setToken(tokenFromStore); @@ -126,11 +120,7 @@ const SettingsPage = () => { color="primary" onClick={() => { setSaved(true); - dispatch.settings.setCredentials({ - owner, - repo, - token, - }); + saveSettings({ repo, owner, token }); }} > Save credentials @@ -146,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 }); }, }); diff --git a/plugins/circleci/src/state/AppState.tsx b/plugins/circleci/src/state/AppState.tsx new file mode 100644 index 0000000000..8c2e46d68b --- /dev/null +++ b/plugins/circleci/src/state/AppState.tsx @@ -0,0 +1,70 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { FC, useReducer, Dispatch, Reducer } from 'react'; +import { circleCIApiRef } from '../api'; +import { State, Action, SettingsState } from './types'; +export { SettingsState }; + +export const AppContext = React.createContext<[State, Dispatch]>( + [] as any, +); +export const STORAGE_KEY = `${circleCIApiRef.id}.settings`; + +const initialState: State = { + settings: { + owner: '', + repo: '', + token: '', + }, + builds: [], + buildsWithSteps: {}, +}; + +const reducer: Reducer = (state, action) => { + switch (action.type) { + case 'setCredentials': + return { + ...state, + settings: { ...state.settings, ...action.payload }, + }; + case 'setBuilds': + return { + ...state, + builds: action.payload, + }; + case 'setBuildWithSteps': { + return { + ...state, + buildsWithSteps: { + ...state.buildsWithSteps, + [action.payload.build_num!]: action.payload, + }, + }; + } + default: + return state; + } +}; + +export const AppStateProvider: FC = ({ children }) => { + const [state, dispatch] = useReducer(reducer, initialState); + + return ( + + <>{children} + + ); +}; diff --git a/plugins/circleci/src/pages/DetailedViewPage/index.ts b/plugins/circleci/src/state/index.ts similarity index 82% rename from plugins/circleci/src/pages/DetailedViewPage/index.ts rename to plugins/circleci/src/state/index.ts index 40d25c4a84..24db02aff4 100644 --- a/plugins/circleci/src/pages/DetailedViewPage/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 { default as DetailedViewPage } from './DetailedViewPage'; +export * from './AppState'; +export * from './useBuildWithSteps'; +export * from './useBuilds'; +export * from './useSettings'; 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 44c87dcc3b..0000000000 --- a/plugins/circleci/src/state/models/index.ts +++ /dev/null @@ -1,31 +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 { 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 49b261852a..0000000000 --- a/plugins/circleci/src/state/models/settings.ts +++ /dev/null @@ -1,75 +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 type SettingsState = { - token: string; - owner: string; - repo: string; -}; - -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); - } - }, - }), -}; 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 }); 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/state/useBuildWithSteps.ts b/plugins/circleci/src/state/useBuildWithSteps.ts new file mode 100644 index 0000000000..22f5309963 --- /dev/null +++ b/plugins/circleci/src/state/useBuildWithSteps.ts @@ -0,0 +1,88 @@ +/* + * 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, 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); + + 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); + if (isPolling) 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(); + intervalId.current = (setInterval( + () => getBuildWithSteps(), + INTERVAL_AMOUNT, + ) as any) as number; + }; + + const stopPolling = () => { + const currentIntervalId = intervalId.current; + if (currentIntervalId) clearInterval(currentIntervalId); + }; + + const build = buildsWithSteps[buildId]; + + return [ + build, + { + restartBuild, + startPolling, + stopPolling, + getBuildWithSteps, + }, + ] as const; +} diff --git a/plugins/circleci/src/state/useBuilds.tsx b/plugins/circleci/src/state/useBuilds.tsx new file mode 100644 index 0000000000..72e8830042 --- /dev/null +++ b/plugins/circleci/src/state/useBuilds.tsx @@ -0,0 +1,86 @@ +/* + * 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, 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); + + const getBuilds = async () => { + 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 = () => { + stopPolling(); + intervalId.current = (setInterval( + () => getBuilds(), + INTERVAL_AMOUNT, + ) as unknown) as number; + }; + + const stopPolling = () => { + const currentIntervalId = intervalId.current; + if (currentIntervalId) clearInterval(currentIntervalId); + }; + + return [ + builds, + { + restartBuild, + startPolling, + stopPolling, + }, + ] as const; +} diff --git a/plugins/circleci/src/state/useSettings.ts b/plugins/circleci/src/state/useSettings.ts new file mode 100644 index 0000000000..68f19ee3b9 --- /dev/null +++ b/plugins/circleci/src/state/useSettings.ts @@ -0,0 +1,83 @@ +/* + * 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, SettingsState } from '.'; +import { useApi, errorApiRef } from '@backstage/core'; + +// type Effect = { +// type: 'rehydrate', +// payload: any +// }; + +// const effects = []; +// pushEffect, popEffect + +export function useSettings() { + const [{ settings }, dispatch] = useContext(AppContext); + + // const interpret = eff => { + // return { + // async rehydrate() {}, + + // }[eff.type](eff.payload) + // } + // useEffect(() => { + // const effectToInterpret = effects[0]; + // removeEffect(0); + // interpret(effectToInterpret) + // },[effects]) + + const errorApi = useApi(errorApiRef); + + 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, + }); + }, + }, + ] 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==