@@ -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"
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
+14
-18
@@ -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;
|
||||
+1
-1
@@ -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';
|
||||
+2
-2
@@ -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;
|
||||
|
||||
@@ -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 });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
+4
-1
@@ -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);
|
||||
},
|
||||
}),
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
}),
|
||||
};
|
||||
@@ -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 });
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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==
|
||||
|
||||
Reference in New Issue
Block a user