refactor: detailedViewPage moved to hooks

Co-authored-by: Nikita Dudnik <nikdudnik@gmail.com>
This commit is contained in:
Ivan Shmidt
2020-05-14 11:38:39 +02:00
parent aac4ac0df6
commit 7b424c0337
8 changed files with 160 additions and 283 deletions
+2 -1
View File
@@ -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<CircleCIApi>({
id: 'plugin.circleci.service',
@@ -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<Action>]>(
[] 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<number, BuildWithSteps>;
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,
@@ -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 (
<Layout>
@@ -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;
}
@@ -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<number, BuildWithSteps>;
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);
},
}),
};
-115
View File
@@ -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);
},
}),
};
@@ -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,
};
-21
View File
@@ -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<RootModel>;
export type iRootState = RematchRootState<RootModel>;
export default init({ models });