Merge branch 'master' of github.com:Nek/backstage

This commit is contained in:
Nikita Nek Dudnik
2020-05-18 18:52:55 +02:00
22 changed files with 531 additions and 444 deletions
+8 -1
View File
@@ -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,
+18 -14
View File
@@ -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;
@@ -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;
+11 -27
View File
@@ -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}</>
+3
View File
@@ -14,3 +14,6 @@
* limitations under the License.
*/
export * from './AppState';
export * from './useSettings';
export * from './useBuilds';
export * from './useBuildWithSteps';
+16 -33
View File
@@ -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;
+29 -25
View File
@@ -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;
}
+153
View File
@@ -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;
}
-79
View File
@@ -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;
}
+8 -5
View File
@@ -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;
}