@@ -31,6 +31,7 @@ export const circleCIApiRef = new ApiRef<CircleCIApi>({
|
||||
});
|
||||
|
||||
export class CircleCIApi {
|
||||
|
||||
private token: string = '';
|
||||
options: Partial<CircleCIOptions>;
|
||||
|
||||
@@ -104,4 +105,8 @@ export class CircleCIApi {
|
||||
async getUser() {
|
||||
return this.api.me();
|
||||
}
|
||||
|
||||
async getBuild(buildId: string) {
|
||||
return this.api.build(parseInt(buildId, 10));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import React, { useEffect, useState, FC } from 'react';
|
||||
import {
|
||||
ExpansionPanel,
|
||||
ExpansionPanelSummary,
|
||||
Typography,
|
||||
ExpansionPanelDetails,
|
||||
} from '@material-ui/core';
|
||||
|
||||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import { BuildStepAction } from 'circleci-api';
|
||||
|
||||
export const ActionOutput: FC<{
|
||||
url: string;
|
||||
name: string;
|
||||
action: BuildStepAction;
|
||||
}> = ({ url, name }) => {
|
||||
const [messages, setMessages] = useState([]);
|
||||
useEffect(() => {
|
||||
fetch(url)
|
||||
.then(res => res.json())
|
||||
.then(actionOutput => {
|
||||
actionOutput &&
|
||||
setMessages(
|
||||
actionOutput.map(({ message }: { message: string }) => message),
|
||||
);
|
||||
});
|
||||
}, [url]);
|
||||
console.log(messages);
|
||||
return (
|
||||
<ExpansionPanel>
|
||||
<ExpansionPanelSummary
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
aria-controls="panel1a-content"
|
||||
id="panel1a-header"
|
||||
>
|
||||
<Typography>{name}</Typography>
|
||||
</ExpansionPanelSummary>
|
||||
<ExpansionPanelDetails>
|
||||
{messages.length === 0
|
||||
? 'Nothing here...'
|
||||
: messages.map(message => (
|
||||
<p style={{ whiteSpace: 'pre-wrap' }}>{message}</p>
|
||||
))}
|
||||
</ExpansionPanelDetails>
|
||||
</ExpansionPanel>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { ActionOutput } from './ActionOutput';
|
||||
@@ -9,10 +9,10 @@ import {
|
||||
TableHead,
|
||||
TableContainer,
|
||||
TableRow,
|
||||
Link,
|
||||
CircularProgress,
|
||||
} from '@material-ui/core';
|
||||
import { Replay as RetryIcon } from '@material-ui/icons';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
StatusFailed,
|
||||
StatusOK,
|
||||
@@ -94,7 +94,7 @@ export const CITable: FC<{
|
||||
<TableRow key={build.id}>
|
||||
<TableCell>{build.id}</TableCell>
|
||||
<TableCell>
|
||||
<Link href={build.buildUrl} target="_blank">
|
||||
<Link to={`/circleci/build/${build.id}`}>
|
||||
{build.buildName}
|
||||
</Link>
|
||||
</TableCell>
|
||||
|
||||
@@ -1,69 +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 { Route } from 'react-router';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { Grid, Button } from '@material-ui/core';
|
||||
import { Settings as SettingsIcon } from '@material-ui/icons';
|
||||
import {
|
||||
InfoCard,
|
||||
Header,
|
||||
Page,
|
||||
pageTheme,
|
||||
Content,
|
||||
ContentHeader,
|
||||
HeaderLabel,
|
||||
SupportButton,
|
||||
} from '@backstage/core';
|
||||
import { CircleCIFetch } from '../CircleCIFetch';
|
||||
import { SettingsPage } from '../SettingsPage';
|
||||
export const CircleCIPage: FC<{}> = () => {
|
||||
return (
|
||||
<>
|
||||
<Route path="/circleci/settings" component={SettingsPage} />
|
||||
<Page theme={pageTheme.tool}>
|
||||
<Header title="Welcome to circleci!" subtitle="Optional subtitle">
|
||||
<HeaderLabel label="Owner" value="Team X" />
|
||||
<HeaderLabel label="Lifecycle" value="Alpha" />
|
||||
</Header>
|
||||
<Content>
|
||||
<ContentHeader title="Circle CI">
|
||||
<Button
|
||||
component={RouterLink}
|
||||
to="/circleci/settings"
|
||||
startIcon={<SettingsIcon />}
|
||||
>
|
||||
Settings
|
||||
</Button>
|
||||
<SupportButton>
|
||||
A description of your plugin goes here.
|
||||
</SupportButton>
|
||||
</ContentHeader>
|
||||
<Grid container spacing={3} direction="column">
|
||||
<Grid item>
|
||||
<InfoCard title="Pipelines">
|
||||
<CircleCIFetch />
|
||||
</InfoCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Content>
|
||||
</Page>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CircleCIPage;
|
||||
@@ -1 +0,0 @@
|
||||
export { CircleCIPage } from './CircleCIPage';
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import { Header, Page, pageTheme, HeaderLabel } from '@backstage/core';
|
||||
|
||||
export const Layout: React.FC = ({ children }) => (
|
||||
<Page theme={pageTheme.tool}>
|
||||
<Header title="Welcome to circleci!" subtitle="Optional subtitle">
|
||||
<HeaderLabel label="Owner" value="Team X" />
|
||||
<HeaderLabel label="Lifecycle" value="Alpha" />
|
||||
</Header>
|
||||
{children}
|
||||
</Page>
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
export * from './Layout';
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { ContentHeader, SupportButton } from '@backstage/core';
|
||||
import { Button } from '@material-ui/core';
|
||||
import { Settings as SettingsIcon } from '@material-ui/icons';
|
||||
|
||||
export const PluginHeader = () => (
|
||||
<ContentHeader title="Circle CI">
|
||||
<Button
|
||||
component={RouterLink}
|
||||
to="/circleci/settings"
|
||||
startIcon={<SettingsIcon />}
|
||||
>
|
||||
Settings
|
||||
</Button>
|
||||
<SupportButton>A description of your plugin goes here.</SupportButton>
|
||||
</ContentHeader>
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
export * from './PluginHeader';
|
||||
@@ -0,0 +1,36 @@
|
||||
import React, { FC } from 'react';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import {
|
||||
Content,
|
||||
ContentHeader,
|
||||
SupportButton,
|
||||
InfoCard,
|
||||
} from '@backstage/core';
|
||||
import { Button, Grid } from '@material-ui/core';
|
||||
import { CircleCIFetch } from 'components/CircleCIFetch';
|
||||
import { Settings as SettingsIcon } from '@material-ui/icons';
|
||||
import { Layout } from 'components/Layout';
|
||||
|
||||
export const BuildsPage: FC<{}> = () => (
|
||||
<Layout>
|
||||
<Content>
|
||||
<ContentHeader title="Circle CI">
|
||||
<Button
|
||||
component={RouterLink}
|
||||
to="/circleci/settings"
|
||||
startIcon={<SettingsIcon />}
|
||||
>
|
||||
Settings
|
||||
</Button>
|
||||
<SupportButton>A description of your plugin goes here.</SupportButton>
|
||||
</ContentHeader>
|
||||
<Grid container spacing={3} direction="column">
|
||||
<Grid item>
|
||||
<InfoCard title="Pipelines">
|
||||
<CircleCIFetch />
|
||||
</InfoCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Content>
|
||||
</Layout>
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
export * from './BuildsPage';
|
||||
@@ -0,0 +1,78 @@
|
||||
import React, { FC } from 'react';
|
||||
import { Content, InfoCard, useApi } from '@backstage/core';
|
||||
import { Grid, Box } from '@material-ui/core';
|
||||
import { PluginHeader } from 'components/PluginHeader';
|
||||
import { BuildWithSteps, BuildStepAction } from 'circleci-api';
|
||||
import { circleCIApiRef } from 'api';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { ActionOutput } from '../../components/ActionOutput/ActionOutput';
|
||||
import { Layout } from 'components/Layout';
|
||||
|
||||
export const DetailedViewPage: FC<{}> = () => {
|
||||
let { buildId = '' } = useParams();
|
||||
|
||||
const [authed, setAuthed] = React.useState(false);
|
||||
const [build, setBuild] = React.useState<BuildWithSteps | null>(null);
|
||||
const api = useApi(circleCIApiRef);
|
||||
|
||||
React.useEffect(() => {
|
||||
const getBuildAsync = async () => {
|
||||
if (!authed) {
|
||||
await api.restorePersistedSettings();
|
||||
await api
|
||||
.validateToken()
|
||||
.then(() => {
|
||||
setAuthed(true);
|
||||
})
|
||||
.catch(() => setAuthed(false));
|
||||
}
|
||||
api.getBuild(buildId).then(setBuild);
|
||||
};
|
||||
getBuildAsync();
|
||||
}, [authed, buildId]);
|
||||
return (
|
||||
<Layout>
|
||||
<Content>
|
||||
<PluginHeader />
|
||||
{!api.authed ? (
|
||||
<div>Not authenticated</div>
|
||||
) : (
|
||||
<Grid container spacing={3} direction="column">
|
||||
<Grid item>
|
||||
<InfoCard title="Pipelines">
|
||||
<BuildsList build={build} />
|
||||
</InfoCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
</Content>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
const BuildsList: FC<{ build: BuildWithSteps | null }> = ({ build }) => (
|
||||
<Box>
|
||||
{build &&
|
||||
build.steps &&
|
||||
build.steps.map(
|
||||
({ name, actions }: { name: string; actions: BuildStepAction[] }) => (
|
||||
<ActionsList name={name} actions={actions} />
|
||||
),
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
const ActionsList: FC<{ actions: BuildStepAction[]; name: string }> = ({
|
||||
actions,
|
||||
name,
|
||||
}) => (
|
||||
<Box key={name}>
|
||||
{actions.map((action: BuildStepAction) => (
|
||||
<ActionOutput
|
||||
action={action}
|
||||
name={action.name}
|
||||
url={action.output_url || ''}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
export * from './DetailedViewPage';
|
||||
+3
-10
@@ -4,16 +4,13 @@ import { circleCIApiRef } from 'api';
|
||||
import {
|
||||
InfoCard,
|
||||
useApi,
|
||||
Header,
|
||||
Page,
|
||||
pageTheme,
|
||||
Content,
|
||||
ContentHeader,
|
||||
HeaderLabel,
|
||||
SupportButton,
|
||||
} from '@backstage/core';
|
||||
import { ProjectInput } from 'components/ProjectInput/ProjectInput';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { Layout } from 'components/Layout';
|
||||
|
||||
export const SettingsPage = () => {
|
||||
const api = useApi(circleCIApiRef);
|
||||
@@ -29,11 +26,7 @@ export const SettingsPage = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Page theme={pageTheme.tool}>
|
||||
<Header title="Circle CI" subtitle="Settings">
|
||||
<HeaderLabel label="Owner" value="Team X" />
|
||||
<HeaderLabel label="Lifecycle" value="Alpha" />
|
||||
</Header>
|
||||
<Layout>
|
||||
<Content>
|
||||
<ContentHeader title="Settings">
|
||||
<Button component={RouterLink} to="/circleci">
|
||||
@@ -90,6 +83,6 @@ export const SettingsPage = () => {
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Content>
|
||||
</Page>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
@@ -14,13 +14,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { createPlugin } from '@backstage/core';
|
||||
import { CircleCIPage } from './components/CircleCIPage';
|
||||
import { SettingsPage } from './components/SettingsPage';
|
||||
import { BuildsPage } from './pages/BuildsPage';
|
||||
import { SettingsPage } from './pages/SettingsPage';
|
||||
import { DetailedViewPage } from './pages/DetailedViewPage';
|
||||
|
||||
export const plugin = createPlugin({
|
||||
id: 'circleci',
|
||||
register({ router }) {
|
||||
router.registerRoute('/circleci', CircleCIPage);
|
||||
router.registerRoute('/circleci', BuildsPage);
|
||||
router.registerRoute('/circleci/build/:buildId', DetailedViewPage);
|
||||
router.registerRoute('/circleci/settings', SettingsPage);
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user