Merge pull request #11 from Nek/feature/state

Feature/state
This commit is contained in:
Ivan Shmidt
2020-05-14 16:13:21 +02:00
committed by GitHub
25 changed files with 438 additions and 487 deletions
-3
View File
@@ -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"
+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',
+20
View File
@@ -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 = () => (
<AppStateProvider>
<Switch>
<Route path="/circleci" exact component={BuildsPage} />
<Route path="/circleci/settings" exact component={SettingsPage} />
<Route
path="/circleci/build/:buildId"
exact
component={DetailedViewPage}
/>
</Switch>
</AppStateProvider>
);
@@ -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 (
<Provider store={store}>
<div>
<RehydrateSettings />
{children}
</div>
</Provider>
);
};
export const withStore = (Component: React.ComponentType<any>) => () => (
<Store>
<Component />
</Store>
);
@@ -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 (
<Layout>
@@ -154,4 +150,4 @@ const ActionsList: FC<{ actions: BuildStepAction[]; name: string }> = ({
);
};
export default withStore(DetailedViewPage);
export default BuildWithStepsPage;
@@ -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';
@@ -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),
@@ -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<{}> = () => (
<Layout>
@@ -34,4 +33,4 @@ const BuildsPage: FC<{}> = () => (
</Layout>
);
export default withStore(BuildsPage);
export default BuildsPage;
@@ -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<void> },
): 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 (
<CITable builds={transformedBuilds} projectName={`${owner}/${repo}`} />
@@ -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<CITableBuildInfo>) => (
<IconButton onClick={row.onRetryClick}>
<IconButton onClick={row.onRestartClick}>
<RetryIcon />
</IconButton>
),
@@ -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;
+2 -6
View File
@@ -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 });
},
});
+70
View File
@@ -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<Action>]>(
[] as any,
);
export const STORAGE_KEY = `${circleCIApiRef.id}.settings`;
const initialState: State = {
settings: {
owner: '',
repo: '',
token: '',
},
builds: [],
buildsWithSteps: {},
};
const reducer: Reducer<State, Action> = (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 (
<AppContext.Provider value={[state, dispatch]}>
<>{children}</>
</AppContext.Provider>
);
};
@@ -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';
@@ -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,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,
};
@@ -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);
}
},
}),
};
-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 });
+38
View File
@@ -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<number, BuildWithSteps>;
export type Action = SettingsAction | BuildsAction | BuildsWithStepsAction;
@@ -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<number | null>(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;
}
+86
View File
@@ -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<number | null>(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;
}
+83
View File
@@ -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;
}
+2 -27
View File
@@ -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==