Merge branch 'master' of github.com:Nek/backstage
This commit is contained in:
@@ -48,8 +48,15 @@ export class CircleCIApi {
|
||||
});
|
||||
}
|
||||
|
||||
async getBuilds(options: CircleCIOptions) {
|
||||
async getBuilds(
|
||||
{ limit = 10, offset = 0 }: { limit: number; offset: number },
|
||||
options: CircleCIOptions,
|
||||
) {
|
||||
return getBuildSummaries(options.token, {
|
||||
options: {
|
||||
limit,
|
||||
offset,
|
||||
},
|
||||
vcs: {},
|
||||
circleHost: this.apiUrl,
|
||||
...options,
|
||||
|
||||
@@ -16,20 +16,24 @@
|
||||
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';
|
||||
import { Settings } from './Settings';
|
||||
|
||||
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>
|
||||
);
|
||||
export const App = () => {
|
||||
return (
|
||||
<AppStateProvider>
|
||||
<>
|
||||
<Switch>
|
||||
<Route path="/circleci" exact component={BuildsPage} />
|
||||
<Route
|
||||
path="/circleci/build/:buildId"
|
||||
exact
|
||||
component={DetailedViewPage}
|
||||
/>
|
||||
</Switch>
|
||||
<Settings />
|
||||
</>
|
||||
</AppStateProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,20 +17,22 @@ import React from 'react';
|
||||
import { Header, Page, pageTheme, HeaderLabel } from '@backstage/core';
|
||||
import { Box } from '@material-ui/core';
|
||||
|
||||
export const Layout: React.FC = ({ children }) => (
|
||||
<Page theme={pageTheme.tool}>
|
||||
<Header
|
||||
pageTitleOverride="Circle CI"
|
||||
title={
|
||||
<Box display="flex" alignItems="center">
|
||||
<img src={logo} style={{ height: '1em' }} alt="Backstage Logo" />
|
||||
<Box mr={1} /> Circle CI
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<HeaderLabel label="Owner" value="Team X" />
|
||||
<HeaderLabel label="Lifecycle" value="Alpha" />
|
||||
</Header>
|
||||
{children}
|
||||
</Page>
|
||||
);
|
||||
export const Layout: React.FC = ({ children }) => {
|
||||
return (
|
||||
<Page theme={pageTheme.tool}>
|
||||
<Header
|
||||
pageTitleOverride="Circle CI"
|
||||
title={
|
||||
<Box display="flex" alignItems="center">
|
||||
<img src={logo} style={{ height: '1em' }} alt="Backstage Logo" />
|
||||
<Box mr={1} /> Circle CI
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<HeaderLabel label="Owner" value="Team X" />
|
||||
<HeaderLabel label="Lifecycle" value="Alpha" />
|
||||
</Header>
|
||||
{children}
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -19,9 +19,11 @@ import { ContentHeader, SupportButton } from '@backstage/core';
|
||||
import { Button, IconButton, Box, Typography } from '@material-ui/core';
|
||||
import ArrowBack from '@material-ui/icons/ArrowBack';
|
||||
import SettingsIcon from '@material-ui/icons/Settings';
|
||||
import { useSettings } from '../../state';
|
||||
|
||||
export type Props = { title?: string };
|
||||
export const PluginHeader: FC<Props> = ({ title = 'Circle CI' }) => {
|
||||
const [, { showSettings }] = useSettings();
|
||||
const location = useLocation();
|
||||
const notRoot = !location.pathname.match(/\/circleci\/?$/);
|
||||
const isSettingsPage = location.pathname.match(/\/circleci\/settings\/?/);
|
||||
@@ -40,11 +42,7 @@ export const PluginHeader: FC<Props> = ({ title = 'Circle CI' }) => {
|
||||
)}
|
||||
>
|
||||
{!isSettingsPage && (
|
||||
<Button
|
||||
component={RouterLink}
|
||||
to="/circleci/settings"
|
||||
startIcon={<SettingsIcon />}
|
||||
>
|
||||
<Button onClick={showSettings} startIcon={<SettingsIcon />}>
|
||||
Settings
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* 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, { useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
TextField,
|
||||
List,
|
||||
ListItem,
|
||||
Snackbar,
|
||||
Box,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
} from '@material-ui/core';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import { useSettings } from '../../state';
|
||||
|
||||
const Settings = () => {
|
||||
const [
|
||||
{
|
||||
repo: repoFromStore,
|
||||
owner: ownerFromStore,
|
||||
token: tokenFromStore,
|
||||
showSettings,
|
||||
},
|
||||
{ saveSettings, hideSettings },
|
||||
] = useSettings();
|
||||
|
||||
const [token, setToken] = React.useState(() => tokenFromStore);
|
||||
const [owner, setOwner] = React.useState(() => ownerFromStore);
|
||||
const [repo, setRepo] = React.useState(() => repoFromStore);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (tokenFromStore !== token) {
|
||||
setToken(tokenFromStore);
|
||||
}
|
||||
if (ownerFromStore !== owner) {
|
||||
setOwner(ownerFromStore);
|
||||
}
|
||||
if (repoFromStore !== repo) {
|
||||
setRepo(repoFromStore);
|
||||
}
|
||||
}, [ownerFromStore, repoFromStore, tokenFromStore]);
|
||||
|
||||
const [saved, setSaved] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Snackbar
|
||||
autoHideDuration={1000}
|
||||
open={saved}
|
||||
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
|
||||
onClose={() => setSaved(false)}
|
||||
>
|
||||
<Alert severity="success">Credentials saved.</Alert>
|
||||
</Snackbar>
|
||||
<Dialog open={showSettings} onClose={hideSettings}>
|
||||
<DialogTitle>
|
||||
Project Credentials
|
||||
{/* {authed ? <StatusOK /> : <StatusFailed />} */}
|
||||
</DialogTitle>
|
||||
<Box minWidth="400px">
|
||||
<List>
|
||||
<ListItem>
|
||||
<TextField
|
||||
name="circleci-token"
|
||||
label="Token"
|
||||
value={token}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={(e) => setToken(e.target.value)}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<TextField
|
||||
name="circleci-owner"
|
||||
fullWidth
|
||||
label="Owner"
|
||||
variant="outlined"
|
||||
value={owner}
|
||||
onChange={(e) => setOwner(e.target.value)}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<TextField
|
||||
name="circleci-repo"
|
||||
label="Repo"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={repo}
|
||||
onChange={(e) => setRepo(e.target.value)}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Box mt={2} display="flex" width="100%" justifyContent="center">
|
||||
<Button
|
||||
data-testid="github-auth-button"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setSaved(true);
|
||||
saveSettings({ repo, owner, token });
|
||||
hideSettings();
|
||||
}}
|
||||
>
|
||||
Save credentials
|
||||
</Button>
|
||||
</Box>
|
||||
</ListItem>
|
||||
</List>
|
||||
</Box>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
+1
-1
@@ -13,4 +13,4 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { default as SettingsPage } from './SettingsPage';
|
||||
export { default as Settings } from './Settings';
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
import React, { FC, useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Content, InfoCard } from '@backstage/core';
|
||||
import { Content, InfoCard, Progress } from '@backstage/core';
|
||||
import { BuildWithSteps, BuildStepAction } from '../../api';
|
||||
import { Grid, Box, Link, IconButton } from '@material-ui/core';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
@@ -27,7 +27,7 @@ import { useSettings } from '../../state/useSettings';
|
||||
import { useBuildWithSteps } from '../../state/useBuildWithSteps';
|
||||
|
||||
const IconLink = IconButton as typeof Link;
|
||||
const BuildName: FC<{ build: BuildWithSteps | null }> = ({ build }) => (
|
||||
const BuildName: FC<{ build?: BuildWithSteps }> = ({ build }) => (
|
||||
<Box display="flex" alignItems="center">
|
||||
#{build?.build_num} - {build?.subject}
|
||||
<IconLink href={build?.build_url} target="_blank">
|
||||
@@ -96,7 +96,7 @@ const BuildWithStepsPage: FC<{}> = () => {
|
||||
const { buildId = '' } = useParams();
|
||||
const classes = useStyles();
|
||||
const [settings] = useSettings();
|
||||
const [build, { startPolling, stopPolling }] = useBuildWithSteps(
|
||||
const [{ loading, value }, { startPolling, stopPolling }] = useBuildWithSteps(
|
||||
parseInt(buildId, 10),
|
||||
);
|
||||
|
||||
@@ -113,11 +113,11 @@ const BuildWithStepsPage: FC<{}> = () => {
|
||||
<Grid container spacing={3} direction="column">
|
||||
<Grid item>
|
||||
<InfoCard
|
||||
className={pickClassName(classes, build)}
|
||||
title={<BuildName build={build} />}
|
||||
className={pickClassName(classes, value)}
|
||||
title={<BuildName build={value} />}
|
||||
cardClassName={classes.cardContent}
|
||||
>
|
||||
<BuildsList build={build} />
|
||||
{loading ? <Progress /> : <BuildsList build={value} />}
|
||||
</InfoCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
@@ -126,7 +126,7 @@ const BuildWithStepsPage: FC<{}> = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const BuildsList: FC<{ build: BuildWithSteps | null }> = ({ build }) => (
|
||||
const BuildsList: FC<{ build?: BuildWithSteps }> = ({ build }) => (
|
||||
<Box>
|
||||
{build &&
|
||||
build.steps &&
|
||||
|
||||
@@ -13,74 +13,26 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React, { FC, useEffect } from 'react';
|
||||
import { CITableBuildInfo, CITable } from '../CITable';
|
||||
import { BuildSummary } from '../../../../api';
|
||||
import { useBuilds } from '../../../../state/useBuilds';
|
||||
import { useSettings } from '../../../../state/useSettings';
|
||||
|
||||
const makeReadableStatus = (status: string | undefined) => {
|
||||
if (!status) return '';
|
||||
return ({
|
||||
retried: 'Retried',
|
||||
canceled: 'Canceled',
|
||||
infrastructure_fail: 'Infra fail',
|
||||
timedout: 'Timedout',
|
||||
not_run: 'Not run',
|
||||
running: 'Running',
|
||||
failed: 'Failed',
|
||||
queued: 'Queued',
|
||||
scheduled: 'Scheduled',
|
||||
not_running: 'Not running',
|
||||
no_tests: 'No tests',
|
||||
fixed: 'Fixed',
|
||||
success: 'Success',
|
||||
} as Record<string, string>)[status];
|
||||
};
|
||||
|
||||
const transform = (
|
||||
buildsData: BuildSummary[],
|
||||
restartBuild: { (buildId: number): Promise<void> },
|
||||
): CITableBuildInfo[] => {
|
||||
return buildsData.map((buildData) => {
|
||||
const tableBuildInfo: CITableBuildInfo = {
|
||||
id: String(buildData.build_num),
|
||||
buildName: buildData.subject
|
||||
? buildData.subject +
|
||||
(buildData.retry_of ? ` (retry of #${buildData.retry_of})` : '')
|
||||
: '',
|
||||
onRestartClick: () =>
|
||||
typeof buildData.build_num !== 'undefined' &&
|
||||
restartBuild(buildData.build_num),
|
||||
source: {
|
||||
branchName: String(buildData.branch),
|
||||
commit: {
|
||||
hash: String(buildData.vcs_revision),
|
||||
url: 'todo',
|
||||
},
|
||||
},
|
||||
status: makeReadableStatus(buildData.status),
|
||||
buildUrl: buildData.build_url,
|
||||
};
|
||||
return tableBuildInfo;
|
||||
});
|
||||
};
|
||||
import React, { FC } from 'react';
|
||||
import { CITable } from '../CITable';
|
||||
import { useBuilds } from '../../../../state';
|
||||
|
||||
export const Builds: FC<{}> = () => {
|
||||
const [
|
||||
builds,
|
||||
{ restartBuild: handleRestartBuild, startPolling, stopPolling },
|
||||
{ total, loading, value, projectName, page, pageSize },
|
||||
{ setPage, retry, setPageSize },
|
||||
] = useBuilds();
|
||||
const [{ repo, owner }] = useSettings();
|
||||
|
||||
useEffect(() => {
|
||||
startPolling();
|
||||
return () => stopPolling();
|
||||
}, [repo, owner]);
|
||||
|
||||
const transformedBuilds = transform(builds, handleRestartBuild);
|
||||
|
||||
return (
|
||||
<CITable builds={transformedBuilds} projectName={`${owner}/${repo}`} />
|
||||
<CITable
|
||||
total={total}
|
||||
loading={loading}
|
||||
retry={retry}
|
||||
builds={value ?? []}
|
||||
projectName={projectName}
|
||||
page={page}
|
||||
onChangePage={setPage}
|
||||
pageSize={pageSize}
|
||||
onChangePageSize={setPageSize}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -114,14 +114,46 @@ const generatedColumns: TableColumn[] = [
|
||||
width: '10%',
|
||||
},
|
||||
];
|
||||
export const CITable: FC<{
|
||||
|
||||
type Props = {
|
||||
loading: boolean;
|
||||
retry: () => void;
|
||||
builds: CITableBuildInfo[];
|
||||
projectName: string;
|
||||
}> = React.memo(({ builds = [], projectName }) => {
|
||||
page: number;
|
||||
onChangePage: (page: number) => void;
|
||||
total: number;
|
||||
pageSize: number;
|
||||
onChangePageSize: (pageSize: number) => void;
|
||||
};
|
||||
export const CITable: FC<Props> = ({
|
||||
projectName,
|
||||
loading,
|
||||
pageSize,
|
||||
page,
|
||||
retry,
|
||||
builds,
|
||||
onChangePage,
|
||||
onChangePageSize,
|
||||
total,
|
||||
}) => {
|
||||
return (
|
||||
<Table
|
||||
options={{ paging: false }}
|
||||
isLoading={loading}
|
||||
options={{ paging: true, pageSize }}
|
||||
totalCount={total}
|
||||
page={page}
|
||||
actions={[
|
||||
{
|
||||
icon: () => <RetryIcon />,
|
||||
tooltip: 'Refresh Data',
|
||||
isFreeAction: true,
|
||||
onClick: () => retry(),
|
||||
},
|
||||
]}
|
||||
data={builds}
|
||||
onChangePage={onChangePage}
|
||||
onChangeRowsPerPage={onChangePageSize}
|
||||
title={
|
||||
<Box display="flex" alignItems="center">
|
||||
<GitHubIcon />
|
||||
@@ -132,4 +164,4 @@ export const CITable: FC<{
|
||||
columns={generatedColumns}
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,139 +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, { useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
TextField,
|
||||
List,
|
||||
Grid,
|
||||
ListItem,
|
||||
Snackbar,
|
||||
Box,
|
||||
} from '@material-ui/core';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import { InfoCard, Content } from '@backstage/core';
|
||||
import { Layout } from '../../components/Layout';
|
||||
import { PluginHeader } from '../../components/PluginHeader';
|
||||
import { useSettings } from '../../state/useSettings';
|
||||
|
||||
const SettingsPage = () => {
|
||||
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);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (tokenFromStore !== token) {
|
||||
setToken(tokenFromStore);
|
||||
}
|
||||
if (ownerFromStore !== owner) {
|
||||
setOwner(ownerFromStore);
|
||||
}
|
||||
if (repoFromStore !== repo) {
|
||||
setRepo(repoFromStore);
|
||||
}
|
||||
}, [ownerFromStore, repoFromStore, tokenFromStore]);
|
||||
|
||||
const [saved, setSaved] = useState(false);
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Content>
|
||||
<PluginHeader title="Settings" />
|
||||
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={3}>
|
||||
<InfoCard
|
||||
title={
|
||||
<>
|
||||
Project Credentials
|
||||
{/* {authed ? <StatusOK /> : <StatusFailed />} */}
|
||||
<Snackbar
|
||||
autoHideDuration={1000}
|
||||
open={saved}
|
||||
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
|
||||
onClose={() => setSaved(false)}
|
||||
>
|
||||
<Alert severity="success">Credentials saved.</Alert>
|
||||
</Snackbar>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<List>
|
||||
<ListItem>
|
||||
<TextField
|
||||
name="circleci-token"
|
||||
label="Token"
|
||||
value={token}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={(e) => setToken(e.target.value)}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<TextField
|
||||
name="circleci-owner"
|
||||
fullWidth
|
||||
label="Owner"
|
||||
variant="outlined"
|
||||
value={owner}
|
||||
onChange={(e) => setOwner(e.target.value)}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<TextField
|
||||
name="circleci-repo"
|
||||
label="Repo"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={repo}
|
||||
onChange={(e) => setRepo(e.target.value)}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Box
|
||||
mt={2}
|
||||
display="flex"
|
||||
width="100%"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Button
|
||||
data-testid="github-auth-button"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setSaved(true);
|
||||
saveSettings({ repo, owner, token });
|
||||
}}
|
||||
>
|
||||
Save credentials
|
||||
</Button>
|
||||
</Box>
|
||||
</ListItem>
|
||||
</List>
|
||||
</InfoCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Content>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsPage;
|
||||
@@ -15,9 +15,8 @@
|
||||
*/
|
||||
import React, { FC, useReducer, Dispatch, Reducer } from 'react';
|
||||
import { circleCIApiRef } from '../api';
|
||||
import type { SettingsState, State, Action } from './types';
|
||||
export type { SettingsState };
|
||||
import equal from 'fast-deep-equal';
|
||||
import type { State, Action, SettingsState } from './types';
|
||||
export { SettingsState };
|
||||
|
||||
export const AppContext = React.createContext<[State, Dispatch<Action>]>(
|
||||
[] as any,
|
||||
@@ -25,13 +24,10 @@ export const AppContext = React.createContext<[State, Dispatch<Action>]>(
|
||||
export const STORAGE_KEY = `${circleCIApiRef.id}.settings`;
|
||||
|
||||
const initialState: State = {
|
||||
settings: {
|
||||
owner: '',
|
||||
repo: '',
|
||||
token: '',
|
||||
},
|
||||
builds: [],
|
||||
buildsWithSteps: {},
|
||||
owner: '',
|
||||
repo: '',
|
||||
token: '',
|
||||
showSettings: false,
|
||||
};
|
||||
|
||||
const reducer: Reducer<State, Action> = (state, action) => {
|
||||
@@ -39,23 +35,12 @@ const reducer: Reducer<State, Action> = (state, action) => {
|
||||
case 'setCredentials':
|
||||
return {
|
||||
...state,
|
||||
settings: { ...state.settings, ...action.payload },
|
||||
...action.payload,
|
||||
};
|
||||
case 'setBuilds':
|
||||
if (equal(action.payload, state.builds)) return state;
|
||||
return {
|
||||
...state,
|
||||
builds: action.payload,
|
||||
};
|
||||
case 'setBuildWithSteps': {
|
||||
return {
|
||||
...state,
|
||||
buildsWithSteps: {
|
||||
...state.buildsWithSteps,
|
||||
[action.payload.build_num!]: action.payload,
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'showSettings':
|
||||
return { ...state, showSettings: true };
|
||||
case 'hideSettings':
|
||||
return { ...state, showSettings: false };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
@@ -63,7 +48,6 @@ const reducer: Reducer<State, Action> = (state, action) => {
|
||||
|
||||
export const AppStateProvider: FC = ({ children }) => {
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
|
||||
return (
|
||||
<AppContext.Provider value={[state, dispatch]}>
|
||||
<>{children}</>
|
||||
|
||||
@@ -14,3 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
export * from './AppState';
|
||||
export * from './useSettings';
|
||||
export * from './useBuilds';
|
||||
export * from './useBuildWithSteps';
|
||||
|
||||
@@ -13,41 +13,24 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { BuildSummary, BuildWithSteps } from '../api';
|
||||
|
||||
export type SettingsState = {
|
||||
owner: string;
|
||||
repo: string;
|
||||
token: string;
|
||||
export type Settings = { owner: string; repo: string; token: string };
|
||||
export type SettingsState = Settings & {
|
||||
showSettings: boolean;
|
||||
};
|
||||
|
||||
export type BuildsState = BuildSummary[];
|
||||
export type State = SettingsState;
|
||||
|
||||
export type State = {
|
||||
settings: SettingsState;
|
||||
builds: BuildsState;
|
||||
buildsWithSteps: BuildsWithStepsState;
|
||||
};
|
||||
type SettingsAction =
|
||||
| {
|
||||
type: 'setCredentials';
|
||||
payload: {
|
||||
repo: string;
|
||||
owner: string;
|
||||
token: string;
|
||||
};
|
||||
}
|
||||
| { type: 'showSettings' }
|
||||
| { type: 'hideSettings' };
|
||||
|
||||
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;
|
||||
export type Action = SettingsAction;
|
||||
|
||||
@@ -14,50 +14,47 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { errorApiRef, useApi } from '@backstage/core';
|
||||
import { useContext } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useAsyncRetry } from 'react-use';
|
||||
import { circleCIApiRef, GitType } from '../api/index';
|
||||
import { AppContext } from '.';
|
||||
import { useAsyncPolling } from './useAsyncPolling';
|
||||
import { useSettings } from './useSettings';
|
||||
|
||||
import { useAsyncPolling } from './useAsyncPolling';
|
||||
|
||||
const INTERVAL_AMOUNT = 3000;
|
||||
|
||||
const INTERVAL_AMOUNT = 1500;
|
||||
export function useBuildWithSteps(buildId: number) {
|
||||
const [settings] = useSettings();
|
||||
const [{ buildsWithSteps }, dispatch] = useContext(AppContext);
|
||||
const [{ token, repo, owner }] = useSettings();
|
||||
const api = useApi(circleCIApiRef);
|
||||
const errorApi = useApi(errorApiRef);
|
||||
|
||||
const getBuildWithSteps = async () => {
|
||||
const getBuildWithSteps = useCallback(async () => {
|
||||
if (owner === '' || repo === '' || token === '') {
|
||||
return Promise.reject('No credentials provided');
|
||||
}
|
||||
|
||||
try {
|
||||
const options = {
|
||||
token: settings.token,
|
||||
token: token,
|
||||
vcs: {
|
||||
owner: settings.owner,
|
||||
repo: settings.repo,
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
type: GitType.GITHUB,
|
||||
},
|
||||
};
|
||||
const build = await api.getBuild(buildId, options);
|
||||
dispatch({ type: 'setBuildWithSteps', payload: build });
|
||||
return Promise.resolve(build);
|
||||
} catch (e) {
|
||||
errorApi.post(e);
|
||||
return Promise.reject(e);
|
||||
}
|
||||
};
|
||||
|
||||
const { startPolling, stopPolling } = useAsyncPolling(
|
||||
getBuildWithSteps,
|
||||
INTERVAL_AMOUNT,
|
||||
);
|
||||
}, [token, owner, repo, buildId]);
|
||||
|
||||
const restartBuild = async () => {
|
||||
try {
|
||||
await api.retry(buildId, {
|
||||
token: settings.token,
|
||||
token: token,
|
||||
vcs: {
|
||||
owner: settings.owner,
|
||||
repo: settings.repo,
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
type: GitType.GITHUB,
|
||||
},
|
||||
});
|
||||
@@ -66,15 +63,22 @@ export function useBuildWithSteps(buildId: number) {
|
||||
}
|
||||
};
|
||||
|
||||
const build = buildsWithSteps[buildId];
|
||||
const { loading, value, retry } = useAsyncRetry(() => getBuildWithSteps(), [
|
||||
getBuildWithSteps,
|
||||
]);
|
||||
|
||||
const { startPolling, stopPolling } = useAsyncPolling(
|
||||
getBuildWithSteps,
|
||||
INTERVAL_AMOUNT,
|
||||
);
|
||||
|
||||
return [
|
||||
build,
|
||||
{ loading, value, retry },
|
||||
{
|
||||
restartBuild,
|
||||
getBuildWithSteps,
|
||||
startPolling,
|
||||
stopPolling,
|
||||
getBuildWithSteps,
|
||||
},
|
||||
] as const;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* 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 { BuildSummary, GitType } from 'circleci-api';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useAsyncRetry } from 'react-use';
|
||||
import { circleCIApiRef } from '../api/index';
|
||||
import { CITableBuildInfo } from '../pages/BuildsPage/lib/CITable';
|
||||
import { useSettings } from './useSettings';
|
||||
|
||||
const makeReadableStatus = (status: string | undefined) => {
|
||||
if (!status) return '';
|
||||
return ({
|
||||
retried: 'Retried',
|
||||
canceled: 'Canceled',
|
||||
infrastructure_fail: 'Infra fail',
|
||||
timedout: 'Timedout',
|
||||
not_run: 'Not run',
|
||||
running: 'Running',
|
||||
failed: 'Failed',
|
||||
queued: 'Queued',
|
||||
scheduled: 'Scheduled',
|
||||
not_running: 'Not running',
|
||||
no_tests: 'No tests',
|
||||
fixed: 'Fixed',
|
||||
success: 'Success',
|
||||
} as Record<string, string>)[status];
|
||||
};
|
||||
|
||||
export const transform = (
|
||||
buildsData: BuildSummary[],
|
||||
restartBuild: { (buildId: number): Promise<void> },
|
||||
): CITableBuildInfo[] => {
|
||||
return buildsData.map((buildData) => {
|
||||
const tableBuildInfo: CITableBuildInfo = {
|
||||
id: String(buildData.build_num),
|
||||
buildName: buildData.subject
|
||||
? buildData.subject +
|
||||
(buildData.retry_of ? ` (retry of #${buildData.retry_of})` : '')
|
||||
: '',
|
||||
onRestartClick: () =>
|
||||
typeof buildData.build_num !== 'undefined' &&
|
||||
restartBuild(buildData.build_num),
|
||||
source: {
|
||||
branchName: String(buildData.branch),
|
||||
commit: {
|
||||
hash: String(buildData.vcs_revision),
|
||||
url: 'todo',
|
||||
},
|
||||
},
|
||||
status: makeReadableStatus(buildData.status),
|
||||
buildUrl: buildData.build_url,
|
||||
};
|
||||
return tableBuildInfo;
|
||||
});
|
||||
};
|
||||
|
||||
export function useBuilds() {
|
||||
const [{ repo, owner, token }] = useSettings();
|
||||
|
||||
const api = useApi(circleCIApiRef);
|
||||
const errorApi = useApi(errorApiRef);
|
||||
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(5);
|
||||
|
||||
const getBuilds = useCallback(
|
||||
async ({ limit, offset }: { limit: number; offset: number }) => {
|
||||
if (owner === '' || repo === '' || token === '') {
|
||||
return Promise.reject('No credentials provided');
|
||||
}
|
||||
|
||||
try {
|
||||
return await api.getBuilds(
|
||||
{ limit, offset },
|
||||
{
|
||||
token: token,
|
||||
vcs: {
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
type: GitType.GITHUB,
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
errorApi.post(e);
|
||||
return Promise.reject(e);
|
||||
}
|
||||
},
|
||||
[repo, token, owner],
|
||||
);
|
||||
|
||||
const restartBuild = async (buildId: number) => {
|
||||
try {
|
||||
await api.retry(buildId, {
|
||||
token: token,
|
||||
vcs: {
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
type: GitType.GITHUB,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
errorApi.post(e);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getBuilds({ limit: 1, offset: 0 }).then((b) => setTotal(b?.[0].build_num!));
|
||||
}, [repo]);
|
||||
|
||||
const { loading, value, retry } = useAsyncRetry(
|
||||
() =>
|
||||
getBuilds({
|
||||
offset: page * pageSize,
|
||||
limit: pageSize,
|
||||
}).then((builds) => transform(builds ?? [], restartBuild)),
|
||||
[page, pageSize, getBuilds],
|
||||
);
|
||||
|
||||
const projectName = `${owner}/${repo}`;
|
||||
return [
|
||||
{
|
||||
page,
|
||||
pageSize,
|
||||
loading,
|
||||
value,
|
||||
projectName,
|
||||
total,
|
||||
},
|
||||
{
|
||||
getBuilds,
|
||||
setPage,
|
||||
setPageSize,
|
||||
restartBuild,
|
||||
retry,
|
||||
},
|
||||
] as const;
|
||||
}
|
||||
@@ -1,79 +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 { errorApiRef, useApi } from '@backstage/core';
|
||||
import { GitType } from 'circleci-api';
|
||||
import { useContext } from 'react';
|
||||
import { circleCIApiRef } from '../api/index';
|
||||
import { AppContext } from '.';
|
||||
import { useAsyncPolling } from './useAsyncPolling';
|
||||
|
||||
const INTERVAL_AMOUNT = 3000;
|
||||
|
||||
export function useBuilds() {
|
||||
const [{ builds, settings }, dispatch] = useContext(AppContext);
|
||||
|
||||
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 { startPolling, stopPolling } = useAsyncPolling(
|
||||
getBuilds,
|
||||
INTERVAL_AMOUNT,
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
return [
|
||||
builds,
|
||||
{
|
||||
restartBuild,
|
||||
startPolling,
|
||||
stopPolling,
|
||||
},
|
||||
] as const;
|
||||
}
|
||||
@@ -13,12 +13,13 @@
|
||||
* 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 { AppContext, STORAGE_KEY, SettingsState } from '.';
|
||||
import { useApi, errorApiRef } from '@backstage/core';
|
||||
import { AppContext, STORAGE_KEY } from './AppState';
|
||||
import { Settings } from './types';
|
||||
|
||||
export function useSettings() {
|
||||
const [{ settings }, dispatch] = useContext(AppContext);
|
||||
const [settings, dispatch] = useContext(AppContext);
|
||||
|
||||
const errorApi = useApi(errorApiRef);
|
||||
|
||||
@@ -44,20 +45,22 @@ export function useSettings() {
|
||||
rehydrate();
|
||||
}, []);
|
||||
|
||||
const persist = (state: SettingsState) => {
|
||||
const persist = (state: Settings) => {
|
||||
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
||||
};
|
||||
|
||||
return [
|
||||
settings,
|
||||
{
|
||||
saveSettings: (state: SettingsState) => {
|
||||
saveSettings: (state: Settings) => {
|
||||
persist(state);
|
||||
dispatch({
|
||||
type: 'setCredentials',
|
||||
payload: state,
|
||||
});
|
||||
},
|
||||
showSettings: () => dispatch({ type: 'showSettings' }),
|
||||
hideSettings: () => dispatch({ type: 'hideSettings' }),
|
||||
},
|
||||
] as const;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user