Merge pull request #2282 from spotify/shmidt-i/circle-ci-plugin-new-route-api

CircleCI plugin new route api
This commit is contained in:
Ivan Shmidt
2020-09-09 09:06:54 +02:00
committed by GitHub
43 changed files with 279 additions and 610 deletions
+34 -4
View File
@@ -4,8 +4,6 @@ Website: [https://circleci.com/](https://circleci.com/)
<img src="./src/assets/screenshot-1.png" />
<img src="./src/assets/screenshot-2.png" />
<img src="./src/assets/screenshot-3.png" />
<img src="./src/assets/screenshot-4.png" />
## Setup
@@ -35,8 +33,39 @@ export default builder.build() as ApiHolder;
export { plugin as Circleci } from '@backstage/plugin-circleci';
```
3. Run app with `yarn start` and navigate to `/circleci/settings`
4. Enter project settings and **project** token, acquired according to [https://circleci.com/docs/2.0/managing-api-tokens/](https://circleci.com/docs/2.0/managing-api-tokens/)
3. Register the plugin router:
```jsx
// packages/app/src/components/catalog/EntityPage.tsx
import { Router as CircleCIRouter } from '@backstage/plugin-circleci';
// Then somewhere inside <EntityPageLayout>
<EntityPageLayout.Content
path="/ci-cd/*"
title="CI/CD"
element={<CircleCIRouter />}
/>;
```
4. Add proxy config:
```
// app-config.yaml
proxy:
'/circleci/api':
target: https://circleci.com/api/v1.1
changeOrigin: true
pathRewrite:
'^/proxy/circleci/api/': '/'
headers:
Circle-Token:
$secret:
env: CIRCLECI_AUTH_TOKEN
```
5. Get and provide `CIRCLECI_AUTH_TOKEN` as env variable (https://circleci.com/docs/api/#add-an-api-token)
6. Add `circleci.com/project-slug` annotation to your component-info.yaml file in format <git-provider>/<owner>/<project> (https://backstage.io/docs/architecture-decisions/adrs-adr002#format)
## Features
@@ -50,3 +79,4 @@ export { plugin as Circleci } from '@backstage/plugin-circleci';
## Limitations
- CircleCI has pretty strict rate limits per token, be careful with opened tabs
- CircelCI doesn't provide a way to auth by 3rd party (e.g. GitHub) token, nor by calling their OAuth endpoints, which currently stands in the way of better auth integration with Backstage (https://discuss.circleci.com/t/circleci-api-authorization-with-github-token/5356)
+2
View File
@@ -22,6 +22,8 @@
},
"dependencies": {
"@backstage/core": "^0.1.1-alpha.21",
"@backstage/catalog-model": "^0.1.1-alpha.21",
"@backstage/plugin-catalog": "^0.1.1-alpha.21",
"@backstage/theme": "^0.1.1-alpha.21",
"@material-ui/core": "^4.9.1",
"@material-ui/icons": "^4.9.1",
+8 -8
View File
@@ -42,8 +42,8 @@ export class CircleCIApi {
this.apiUrl = apiUrl;
}
async retry(buildNumber: number, options: CircleCIOptions) {
return postBuildActions(options.token, buildNumber, BuildAction.RETRY, {
async retry(buildNumber: number, options: Partial<CircleCIOptions>) {
return postBuildActions('', buildNumber, BuildAction.RETRY, {
circleHost: this.apiUrl,
...options.vcs,
});
@@ -51,9 +51,9 @@ export class CircleCIApi {
async getBuilds(
{ limit = 10, offset = 0 }: { limit: number; offset: number },
options: CircleCIOptions,
options: Partial<CircleCIOptions>,
) {
return getBuildSummaries(options.token, {
return getBuildSummaries('', {
options: {
limit,
offset,
@@ -64,12 +64,12 @@ export class CircleCIApi {
});
}
async getUser(options: CircleCIOptions) {
return getMe(options.token, { circleHost: this.apiUrl, ...options });
async getUser(options: Partial<CircleCIOptions>) {
return getMe('', { circleHost: this.apiUrl, ...options });
}
async getBuild(buildNumber: number, options: CircleCIOptions) {
return getFullBuild(options.token, buildNumber, {
async getBuild(buildNumber: number, options: Partial<CircleCIOptions>) {
return getFullBuild('', buildNumber, {
circleHost: this.apiUrl,
...options.vcs,
});
Binary file not shown.

Before

Width:  |  Height:  |  Size: 972 KiB

After

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 807 KiB

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 856 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 943 KiB

@@ -15,20 +15,22 @@
*/
import React, { FC, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { Content, InfoCard, Progress } from '@backstage/core';
import { InfoCard, Progress, Link } from '@backstage/core';
import { BuildWithSteps, BuildStepAction } from '../../api';
import { Grid, Box, Link, IconButton } from '@material-ui/core';
import {
Grid,
Box,
IconButton,
Breadcrumbs,
Typography,
Link as MaterialLink,
} 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 LaunchIcon from '@material-ui/icons/Launch';
import { useSettings } from '../../state/useSettings';
import { useBuildWithSteps } from '../../state/useBuildWithSteps';
import { AppStateProvider } from '../../state';
import { Settings } from '../../components/Settings';
const IconLink = IconButton as typeof Link;
const IconLink = (IconButton as any) as typeof MaterialLink;
const BuildName: FC<{ build?: BuildWithSteps }> = ({ build }) => (
<Box display="flex" alignItems="center">
#{build?.build_num} - {build?.subject}
@@ -94,56 +96,13 @@ const pickClassName = (
return classes.neutral;
};
const Page = () => (
<AppStateProvider>
<Layout>
<Content>
<BuildWithStepsView />
<Settings />
</Content>
</Layout>
</AppStateProvider>
);
const BuildWithStepsView: FC<{}> = () => {
const { buildId = '' } = useParams();
const classes = useStyles();
const [settings] = useSettings();
const [{ loading, value }, { startPolling, stopPolling }] = useBuildWithSteps(
parseInt(buildId, 10),
);
useEffect(() => {
startPolling();
return () => stopPolling();
}, [buildId, settings, startPolling, stopPolling]);
return (
<>
<PluginHeader title="Build info" />
<Grid container spacing={3} direction="column">
<Grid item>
<InfoCard
className={pickClassName(classes, value)}
title={<BuildName build={value} />}
cardClassName={classes.cardContent}
>
{loading ? <Progress /> : <BuildsList build={value} />}
</InfoCard>
</Grid>
</Grid>
</>
);
};
const BuildsList: FC<{ build?: BuildWithSteps }> = ({ build }) => (
<Box>
{build &&
build.steps &&
build.steps.map(
({ name, actions }: { name: string; actions: BuildStepAction[] }) => (
<ActionsList name={name} actions={actions} />
<ActionsList key={name} name={name} actions={actions} />
),
)}
</Box>
@@ -167,5 +126,35 @@ const ActionsList: FC<{ actions: BuildStepAction[]; name: string }> = ({
);
};
export default Page;
export { BuildWithStepsView as BuildWithSteps };
export const BuildWithStepsPage = () => {
const { buildId = '' } = useParams();
const classes = useStyles();
const [{ loading, value }, { startPolling, stopPolling }] = useBuildWithSteps(
parseInt(buildId, 10),
);
useEffect(() => {
startPolling();
return () => stopPolling();
}, [buildId, startPolling, stopPolling]);
return (
<>
<Breadcrumbs aria-label="breadcrumb">
<Link to="..">All builds</Link>
<Typography>Build details</Typography>
</Breadcrumbs>
<Grid container spacing={3} direction="column">
<Grid item>
<InfoCard
className={pickClassName(classes, value)}
title={<BuildName build={value} />}
cardClassName={classes.cardContent}
>
{loading ? <Progress /> : <BuildsList build={value} />}
</InfoCard>
</Grid>
</Grid>
</>
);
};
@@ -13,4 +13,4 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './PluginHeader';
export { BuildWithStepsPage } from './BuildWithStepsPage';
@@ -13,7 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export {
default as DetailedViewPage,
BuildWithSteps,
} from './BuildWithStepsPage';
import React from 'react';
import { Builds } from './lib/Builds';
import { Grid } from '@material-ui/core';
export const BuildsPage = () => (
<Grid container spacing={3} direction="column">
<Grid item>
<Builds />
</Grid>
</Grid>
);
@@ -13,4 +13,4 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './Layout';
export { BuildsPage } from './BuildsPage';
@@ -17,7 +17,7 @@ import React, { FC } from 'react';
import { Link, Typography, Box, IconButton } from '@material-ui/core';
import RetryIcon from '@material-ui/icons/Replay';
import GitHubIcon from '@material-ui/icons/GitHub';
import { Link as RouterLink } from 'react-router-dom';
import { Link as RouterLink, generatePath } from 'react-router-dom';
import {
StatusError,
StatusWarning,
@@ -27,6 +27,7 @@ import {
Table,
TableColumn,
} from '@backstage/core';
import { circleCIBuildRouteRef } from '../../../../route-refs';
export type CITableBuildInfo = {
id: string;
@@ -80,7 +81,10 @@ const generatedColumns: TableColumn[] = [
field: 'buildName',
highlight: true,
render: (row: Partial<CITableBuildInfo>) => (
<Link component={RouterLink} to={`/circleci/build/${row.id}`}>
<Link
component={RouterLink}
to={`${generatePath(circleCIBuildRouteRef.path, { buildId: row.id! })}`}
>
{row.buildName}
</Link>
),
@@ -1,38 +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 from 'react';
import { Route, MemoryRouter, Routes } from 'react-router';
import { Builds } from '../pages/BuildsPage';
import { BuildWithSteps } from '../pages/BuildWithStepsPage';
import { AppStateProvider } from '../state';
import { Settings } from './Settings';
// TODO: allow pass in settings as props
// When some shared settings workflow
// will be established
export const CircleCIWidget = () => (
<MemoryRouter initialEntries={['/circleci']}>
<AppStateProvider>
<>
<Routes>
<Route path="/circleci" element={<Builds />} />
<Route path="/circleci/build/:buildId" element={<BuildWithSteps />} />
</Routes>
<Settings />
</>
</AppStateProvider>
</MemoryRouter>
);
@@ -1,29 +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 from 'react';
import { Header, Page, pageTheme, HeaderLabel } from '@backstage/core';
export const Layout: React.FC = ({ children }) => {
return (
<Page theme={pageTheme.tool}>
<Header title="CircleCI" subtitle="See recent builds and their status">
<HeaderLabel label="Owner" value="Spotify" />
<HeaderLabel label="Lifecycle" value="Alpha" />
</Header>
{children}
</Page>
);
};
@@ -1,55 +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 { Link as RouterLink, useLocation } from 'react-router-dom';
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 = 'CircleCI' }) => {
const [, { showSettings }] = useSettings();
const location = useLocation();
const notRoot = !location.pathname.match(/\/circleci\/?$/);
const isSettingsPage = location.pathname.match(/\/circleci\/settings\/?/);
return (
<ContentHeader
title={title}
titleComponent={() => (
<Box alignItems="center" display="flex">
{notRoot && (
<IconButton component={RouterLink} to="/circleci">
<ArrowBack />
</IconButton>
)}
<Typography variant="h4">{title}</Typography>
</Box>
)}
>
{!isSettingsPage && (
<Button onClick={showSettings} startIcon={<SettingsIcon />}>
Settings
</Button>
)}
<SupportButton>
This plugin allows you to view and interact with your builds within the
Circle CI environment.
</SupportButton>
</ContentHeader>
);
};
@@ -0,0 +1,43 @@
/*
* 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 from 'react';
import { Routes, Route } from 'react-router';
import { circleCIRouteRef, circleCIBuildRouteRef } from '../route-refs';
import { BuildWithStepsPage } from './BuildWithStepsPage/';
import { BuildsPage } from './BuildsPage';
import { CIRCLECI_ANNOTATION } from '../constants';
import { Entity } from '@backstage/catalog-model';
import { WarningPanel } from '@backstage/core';
export const isPluginApplicableToEntity = (entity: Entity) =>
Boolean(entity.metadata.annotations?.[CIRCLECI_ANNOTATION]) &&
entity.metadata.annotations?.[CIRCLECI_ANNOTATION] !== '';
export const Router = ({ entity }: { entity: Entity }) =>
!isPluginApplicableToEntity(entity) ? (
<WarningPanel title="CircleCI plugin:">
<pre>{CIRCLECI_ANNOTATION}</pre> annotation is missing on the entity.
</WarningPanel>
) : (
<Routes>
<Route path={`/${circleCIRouteRef.path}`} element={<BuildsPage />} />
<Route
path={`/${circleCIBuildRouteRef.path}`}
element={<BuildWithStepsPage />}
/>
</Routes>
);
@@ -1,129 +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, useEffect } 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] = useState(() => tokenFromStore);
const [owner, setOwner] = useState(() => ownerFromStore);
const [repo, setRepo] = useState(() => repoFromStore);
useEffect(() => {
if (tokenFromStore !== token) {
setToken(token);
}
if (ownerFromStore !== owner) {
setOwner(owner);
}
if (repoFromStore !== repo) {
setRepo(repo);
}
}, [ownerFromStore, repoFromStore, tokenFromStore, token, owner, repo]);
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,5 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { default as Settings } from './Settings';
export const CIRCLECI_ANNOTATION = 'circleci.com/project-slug';
+2 -1
View File
@@ -17,4 +17,5 @@
export { plugin } from './plugin';
export * from './api';
export * from './route-refs';
export { CircleCIWidget } from './components/CircleCIWidget';
export { Router, isPluginApplicableToEntity } from './components/Router';
export { CIRCLECI_ANNOTATION } from './constants';
@@ -1,48 +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 { Content } from '@backstage/core';
import { Grid } from '@material-ui/core';
import { Builds as BuildsComp } from './lib/Builds';
import { Layout } from '../../components/Layout';
import { PluginHeader } from '../../components/PluginHeader';
import { AppStateProvider } from '../../state/AppState';
import { Settings } from '../../components/Settings';
const BuildsPage: FC<{}> = () => (
<AppStateProvider>
<Layout>
<Content>
<Builds />
<Settings />
</Content>
</Layout>
</AppStateProvider>
);
const Builds = () => (
<>
<PluginHeader title="All builds" />
<Grid container spacing={3} direction="column">
<Grid item>
<BuildsComp />
</Grid>
</Grid>
</>
);
export default BuildsPage;
export { Builds };
@@ -1,16 +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.
*/
export { default as BuildsPage, Builds } from './BuildsPage';
+1 -7
View File
@@ -13,10 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createPlugin, createApiFactory, configApiRef } from '@backstage/core';
import { circleCIRouteRef, circleCIBuildRouteRef } from './route-refs';
import BuildsPage from './pages/BuildsPage/BuildsPage';
import BuildWithStepsPage from './pages/BuildWithStepsPage/BuildWithStepsPage';
import { circleCIApiRef, CircleCIApi } from './api';
export const plugin = createPlugin({
@@ -31,8 +29,4 @@ export const plugin = createPlugin({
),
}),
],
register({ router }) {
router.addRoute(circleCIRouteRef, BuildsPage);
router.addRoute(circleCIBuildRouteRef, BuildWithStepsPage);
},
});
+2 -2
View File
@@ -33,11 +33,11 @@ const CircleCIIcon: FC<SvgIconProps> = props => (
export const circleCIRouteRef = createRouteRef({
icon: CircleCIIcon,
path: '/circleci',
path: '',
title: 'CircleCI | All builds',
});
export const circleCIBuildRouteRef = createRouteRef({
path: '/circleci/build/:buildId',
path: ':buildId',
title: 'CircleCI | Build info',
});
-57
View File
@@ -1,57 +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, useReducer, Dispatch, Reducer } from 'react';
import { circleCIApiRef } from '../api';
import type { State, Action, SettingsState } from './types';
export type { SettingsState };
export const AppContext = React.createContext<[State, Dispatch<Action>]>(
[] as any,
);
export const STORAGE_KEY = `${circleCIApiRef.id}.settings`;
const initialState: State = {
owner: '',
repo: '',
token: '',
showSettings: false,
};
const reducer: Reducer<State, Action> = (state, action) => {
switch (action.type) {
case 'setCredentials':
return {
...state,
...action.payload,
};
case 'showSettings':
return { ...state, showSettings: true };
case 'hideSettings':
return { ...state, showSettings: false };
default:
return state;
}
};
export const AppStateProvider: FC = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<AppContext.Provider value={[state, dispatch]}>
<>{children}</>
</AppContext.Provider>
);
};
-2
View File
@@ -13,7 +13,5 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './AppState';
export * from './useSettings';
export * from './useBuilds';
export * from './useBuildWithSteps';
-36
View File
@@ -1,36 +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.
*/
export type Settings = { owner: string; repo: string; token: string };
export type SettingsState = Settings & {
showSettings: boolean;
};
export type State = SettingsState;
type SettingsAction =
| {
type: 'setCredentials';
payload: {
repo: string;
owner: string;
token: string;
};
}
| { type: 'showSettings' }
| { type: 'hideSettings' };
export type Action = SettingsAction;
+17 -18
View File
@@ -14,31 +14,35 @@
* limitations under the License.
*/
import { errorApiRef, useApi } from '@backstage/core';
import { useCallback } from 'react';
import { useCallback, useMemo } from 'react';
import { useAsyncRetry } from 'react-use';
import { circleCIApiRef, GitType } from '../api/index';
import { circleCIApiRef } from '../api/index';
import { useAsyncPolling } from './useAsyncPolling';
import { useSettings } from './useSettings';
import { useProjectSlugFromEntity, mapVcsType } from './useBuilds';
const INTERVAL_AMOUNT = 1500;
export function useBuildWithSteps(buildId: number) {
const [{ token, repo, owner }] = useSettings();
const { vcs, repo, owner } = useProjectSlugFromEntity();
const api = useApi(circleCIApiRef);
const errorApi = useApi(errorApiRef);
const vcsOption = useMemo(
() => ({
owner: owner,
repo: repo,
type: mapVcsType(vcs),
}),
[owner, repo, vcs],
);
const getBuildWithSteps = useCallback(async () => {
if (owner === '' || repo === '' || token === '') {
if (owner === '' || repo === '' || vcs === '') {
return Promise.reject('No credentials provided');
}
try {
const options = {
token: token,
vcs: {
owner: owner,
repo: repo,
type: GitType.GITHUB,
},
vcs: vcsOption,
};
const build = await api.getBuild(buildId, options);
return Promise.resolve(build);
@@ -46,17 +50,12 @@ export function useBuildWithSteps(buildId: number) {
errorApi.post(e);
return Promise.reject(e);
}
}, [token, owner, repo, buildId, api, errorApi]);
}, [vcsOption, buildId, api, errorApi]); // eslint-disable-line react-hooks/exhaustive-deps
const restartBuild = async () => {
try {
await api.retry(buildId, {
token: token,
vcs: {
owner: owner,
repo: repo,
type: GitType.GITHUB,
},
vcs: vcsOption,
});
} catch (e) {
errorApi.post(e);
+24 -8
View File
@@ -18,8 +18,9 @@ import { BuildSummary, GitType } from 'circleci-api';
import { useCallback, useEffect, useState } from 'react';
import { useAsyncRetry } from 'react-use';
import { circleCIApiRef } from '../api/index';
import type { CITableBuildInfo } from '../pages/BuildsPage/lib/CITable';
import { useSettings } from './useSettings';
import type { CITableBuildInfo } from '../components/BuildsPage/lib/CITable';
import { useEntity } from '@backstage/plugin-catalog';
import { CIRCLECI_ANNOTATION } from '../constants';
const makeReadableStatus = (status: string | undefined) => {
if (!status) return '';
@@ -68,8 +69,25 @@ export const transform = (
});
};
export const useProjectSlugFromEntity = () => {
const { entity } = useEntity();
const [vcs, owner, repo] = (
entity.metadata.annotations?.[CIRCLECI_ANNOTATION] ?? ''
).split('/');
return { vcs, owner, repo };
};
export function mapVcsType(vcs: string): GitType {
switch (vcs) {
case 'github':
return GitType.GITHUB;
default:
return GitType.BITBUCKET;
}
}
export function useBuilds() {
const [{ repo, owner, token }] = useSettings();
const { repo, owner, vcs } = useProjectSlugFromEntity();
const api = useApi(circleCIApiRef);
const errorApi = useApi(errorApiRef);
@@ -79,7 +97,7 @@ export function useBuilds() {
const getBuilds = useCallback(
async ({ limit, offset }: { limit: number; offset: number }) => {
if (owner === '' || repo === '' || token === '') {
if (owner === '' || repo === '' || vcs === '') {
return Promise.reject('No credentials provided');
}
@@ -87,11 +105,10 @@ export function useBuilds() {
return await api.getBuilds(
{ limit, offset },
{
token: token,
vcs: {
owner: owner,
repo: repo,
type: GitType.GITHUB,
type: mapVcsType(vcs),
},
},
);
@@ -100,13 +117,12 @@ export function useBuilds() {
return Promise.reject(e);
}
},
[repo, token, owner, api, errorApi],
[repo, owner, vcs, api, errorApi],
);
const restartBuild = async (buildId: number) => {
try {
await api.retry(buildId, {
token: token,
vcs: {
owner: owner,
repo: repo,
-68
View File
@@ -1,68 +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 { useContext, useEffect } from 'react';
import { AppContext, STORAGE_KEY } from './AppState';
import { Settings } from './types';
export function useSettings() {
const [settings, dispatch] = useContext(AppContext);
const errorApi = useApi(errorApiRef);
useEffect(() => {
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);
}
};
rehydrate();
}, [dispatch, errorApi, settings]);
const persist = (state: Settings) => {
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(state));
};
return [
settings,
{
saveSettings: (state: Settings) => {
persist(state);
dispatch({
type: 'setCredentials',
payload: state,
});
},
showSettings: () => dispatch({ type: 'showSettings' }),
hideSettings: () => dispatch({ type: 'hideSettings' }),
},
] as const;
}
@@ -22,7 +22,7 @@ import { WorkflowRunsTable } from './WorkflowRunsTable';
import { GITHUB_ACTIONS_ANNOTATION } from './useProjectName';
import { WarningPanel } from '@backstage/core';
const isPluginApplicableToEntity = (entity: Entity) =>
export const isPluginApplicableToEntity = (entity: Entity) =>
Boolean(entity.metadata.annotations?.[GITHUB_ACTIONS_ANNOTATION]) &&
entity.metadata.annotations?.[GITHUB_ACTIONS_ANNOTATION] !== '';
@@ -30,8 +30,8 @@ export const Router = ({ entity }: { entity: Entity }) =>
// TODO(shmidt-i): move warning to a separate standardized component
!isPluginApplicableToEntity(entity) ? (
<WarningPanel title=" GitHubActions plugin:">
`entity.metadata.annotations['
{GITHUB_ACTIONS_ANNOTATION}']` key is missing on the entity.{' '}
<pre>{GITHUB_ACTIONS_ANNOTATION}</pre> annotation is missing on the
entity.
</WarningPanel>
) : (
<Routes>
+1 -1
View File
@@ -16,6 +16,6 @@
export { plugin } from './plugin';
export * from './api';
export { Router } from './components/Router';
export { Router, isPluginApplicableToEntity } from './components/Router';
export * from './components/Cards';
export { GITHUB_ACTIONS_ANNOTATION } from './components/useProjectName';
+1 -4
View File
@@ -32,10 +32,7 @@ type Props = {
export const Router = ({ entity }: Props) =>
!isPluginApplicableToEntity(entity) ? (
<WarningPanel title="Rollbar plugin:">
<pre>
entity.metadata.annotations['{ROLLBAR_ANNOTATION}']` key is missing on
the entity.
</pre>
<pre>{ROLLBAR_ANNOTATION}</pre> annotation is missing on the entity.
</WarningPanel>
) : (
<Routes>