Merge branch 'circleci-plugin' into master
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint')],
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
# Title
|
||||
Welcome to the circleci plugin!
|
||||
|
||||
## Sub-section 1
|
||||
|
||||
## Sub-section 2
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "@backstage/plugin-circleci",
|
||||
"version": "0.1.1-alpha.4",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "backstage-cli plugin:build",
|
||||
"lint": "backstage-cli lint",
|
||||
"test": "backstage-cli test",
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.4",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"@types/jest": "^24.0.0",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/testing-library__jest-dom": "5.0.2",
|
||||
"jest-fetch-mock": "^3.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/core": "^0.1.1-alpha.4",
|
||||
"@backstage/theme": "^0.1.1-alpha.4",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
"@types/react-lazylog": "^4.5.0",
|
||||
"circleci-api": "^4.0.0",
|
||||
"react": "16.13.1",
|
||||
"react-dom": "16.13.1",
|
||||
"react-lazylog": "^4.5.2",
|
||||
"react-router": "^5.1.2",
|
||||
"react-use": "^13.0.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* 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 { CircleCI, GitType, CircleCIOptions } from 'circleci-api';
|
||||
import { ApiRef } from '@backstage/core';
|
||||
|
||||
const defaultOptions: Partial<CircleCIOptions> = {
|
||||
circleHost: '/circleci/api',
|
||||
vcs: {
|
||||
type: GitType.GITHUB,
|
||||
owner: 'CircleCITest3',
|
||||
repo: 'circleci-test',
|
||||
},
|
||||
};
|
||||
export const circleCIApiRef = new ApiRef<CircleCIApi>({
|
||||
id: 'plugin.circleci.service',
|
||||
description: 'Used by the CircleCI plugin to make requests',
|
||||
});
|
||||
|
||||
export class CircleCIApi {
|
||||
|
||||
private token: string = '';
|
||||
options: Partial<CircleCIOptions>;
|
||||
|
||||
authed: boolean = false;
|
||||
constructor(options?: Partial<CircleCIOptions>) {
|
||||
this.options = Object.assign(Object.create(null), defaultOptions, options);
|
||||
}
|
||||
|
||||
setToken(token: string) {
|
||||
this.token = token;
|
||||
this.persistToken();
|
||||
}
|
||||
|
||||
setVCSOptions(vcs: CircleCIOptions['vcs']) {
|
||||
this.options.vcs = vcs;
|
||||
this.persistVCSOptions();
|
||||
}
|
||||
|
||||
async persistVCSOptions() {
|
||||
const key = circleCIApiRef.id;
|
||||
sessionStorage.setItem(key + '_options', JSON.stringify(this.options.vcs));
|
||||
}
|
||||
|
||||
async restorePersistedSettings() {
|
||||
if (this.authed) return Promise.resolve();
|
||||
const key = circleCIApiRef.id;
|
||||
const persistedToken = sessionStorage.getItem(key);
|
||||
let persistedVCSOptions: {} | undefined;
|
||||
try {
|
||||
persistedVCSOptions = JSON.parse(
|
||||
sessionStorage.getItem(key + '_options') as string,
|
||||
);
|
||||
} catch (e) {}
|
||||
if (persistedToken && persistedVCSOptions) {
|
||||
this.token = persistedToken;
|
||||
this.options.vcs = persistedVCSOptions;
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
async persistToken() {
|
||||
if (this.authed) return;
|
||||
const key = circleCIApiRef.id;
|
||||
sessionStorage.setItem(key, this.token);
|
||||
}
|
||||
|
||||
async validateToken() {
|
||||
if (!this.token || this.token === '') {
|
||||
return Promise.reject('Wrong token');
|
||||
}
|
||||
|
||||
// TODO: switch towards using personal token
|
||||
await this.api.builds();
|
||||
this.authed = true;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
private get api() {
|
||||
return new CircleCI({ ...this.options, token: this.token });
|
||||
}
|
||||
|
||||
async retry(buildId: string) {
|
||||
return this.api.retry(Number(buildId));
|
||||
}
|
||||
|
||||
async getBuilds() {
|
||||
return this.api.builds();
|
||||
}
|
||||
|
||||
async getUser() {
|
||||
return this.api.me();
|
||||
}
|
||||
|
||||
async getBuild(buildId: string) {
|
||||
return this.api.build(parseInt(buildId, 10));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import React, { useEffect, useState, FC, Suspense } from 'react';
|
||||
import {
|
||||
ExpansionPanel,
|
||||
ExpansionPanelSummary,
|
||||
Typography,
|
||||
ExpansionPanelDetails,
|
||||
} from '@material-ui/core';
|
||||
|
||||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import { BuildStepAction } from 'circleci-api';
|
||||
|
||||
const LazyLog = React.lazy(() => import('react-lazylog/build/LazyLog'));
|
||||
|
||||
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]);
|
||||
return (
|
||||
<ExpansionPanel TransitionProps={{ unmountOnExit: true }}>
|
||||
<ExpansionPanelSummary
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
aria-controls={`panel-${name}-content`}
|
||||
id={`panel-${name}-header`}
|
||||
>
|
||||
<Typography>{name}</Typography>
|
||||
</ExpansionPanelSummary>
|
||||
<ExpansionPanelDetails>
|
||||
{messages.length === 0 ? (
|
||||
'Nothing here...'
|
||||
) : (
|
||||
<Suspense fallback="...">
|
||||
<div style={{ height: '200px', width: '100%' }}>
|
||||
<LazyLog text={messages.join('\n')} extraLines={1} enableSearch />
|
||||
</div>
|
||||
</Suspense>
|
||||
)}
|
||||
</ExpansionPanelDetails>
|
||||
</ExpansionPanel>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { ActionOutput } from './ActionOutput';
|
||||
@@ -0,0 +1,133 @@
|
||||
// Idea for this component to be somehow reusable representation of CI table view
|
||||
import React, { FC } from 'react';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import {
|
||||
Button,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableContainer,
|
||||
TableRow,
|
||||
CircularProgress,
|
||||
} from '@material-ui/core';
|
||||
import { Replay as RetryIcon } from '@material-ui/icons';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
StatusFailed,
|
||||
StatusOK,
|
||||
StatusPending,
|
||||
StatusNA,
|
||||
} from '@backstage/core';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
table: {
|
||||
minWidth: 650,
|
||||
},
|
||||
avatar: {
|
||||
height: 32,
|
||||
width: 32,
|
||||
borderRadius: '50%',
|
||||
},
|
||||
});
|
||||
|
||||
export type CITableBuildInfo = {
|
||||
id: string;
|
||||
buildName: string;
|
||||
buildUrl?: string;
|
||||
source: {
|
||||
branchName: string;
|
||||
commit: {
|
||||
hash: string;
|
||||
url: string;
|
||||
};
|
||||
};
|
||||
status: string;
|
||||
tests?: {
|
||||
total: number;
|
||||
passed: number;
|
||||
skipped: number;
|
||||
failed: number;
|
||||
testUrl: string; //fixme better name
|
||||
};
|
||||
onRetryClick: () => void;
|
||||
};
|
||||
|
||||
// :retried, :canceled, :infrastructure_fail, :timedout, :not_run, :running, :failed, :queued, :scheduled, :not_running, :no_tests, :fixed, :success
|
||||
const getStatusComponent = (status: string) => {
|
||||
switch (status.toLowerCase()) {
|
||||
case 'queued':
|
||||
case 'scheduled':
|
||||
return <StatusPending />;
|
||||
case 'running':
|
||||
return <CircularProgress size={12} />;
|
||||
case 'failed':
|
||||
return <StatusFailed />;
|
||||
case 'success':
|
||||
return <StatusOK />;
|
||||
case 'canceled':
|
||||
default:
|
||||
return <StatusNA />;
|
||||
}
|
||||
};
|
||||
|
||||
export const CITableBuildRow: FC<{ build: CITableBuildInfo }> = ({ build }) => (
|
||||
<TableRow key={build.id}>
|
||||
<TableCell>{build.id}</TableCell>
|
||||
<TableCell>
|
||||
<Link to={`/circleci/build/${build.id}`}>{build.buildName}</Link>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{build.source.branchName}
|
||||
<br />
|
||||
{build.source.commit.hash}
|
||||
</TableCell>
|
||||
<TableCell align="center">{getStatusComponent(build.status)}</TableCell>
|
||||
{build.tests && (
|
||||
<TableCell>
|
||||
{
|
||||
<>
|
||||
{build.tests.passed}/{build.tests.total} (
|
||||
{build.tests.failed ? build.tests.failed + ', ' : ''}
|
||||
{build.tests.skipped ? build.tests.skipped : ''})
|
||||
</>
|
||||
}
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell align="center">
|
||||
<Button onClick={build.onRetryClick}>
|
||||
<RetryIcon />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
export const CITableBuildHeadRow:FC<{isTestDataAvailable: boolean}> = ({isTestDataAvailable}) => (
|
||||
<TableRow>
|
||||
<TableCell>ID</TableCell>
|
||||
<TableCell>Build</TableCell>
|
||||
<TableCell>Source</TableCell>
|
||||
<TableCell align="center">Status</TableCell>
|
||||
{isTestDataAvailable && <TableCell>Tests</TableCell>}
|
||||
<TableCell align="center">Actions</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
export const CITable: FC<{
|
||||
builds: CITableBuildInfo[];
|
||||
}> = ({ builds }) => {
|
||||
const classes = useStyles();
|
||||
const isTestDataAvailable = builds.some(build => build.tests);
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table className={classes.table} size="small" aria-label="a dense table">
|
||||
<TableHead><CITableBuildHeadRow isTestDataAvailable={isTestDataAvailable}/></TableHead>
|
||||
<TableBody>
|
||||
{builds.map(build => (
|
||||
<CITableBuildRow build={build} />
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { CITable, CITableBuildInfo } from './CITable';
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* 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 wr iting, 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 Alert from '@material-ui/lab/Alert';
|
||||
// import { Progress } from '@backstage/core';
|
||||
|
||||
import { BuildSummary } from 'circleci-api';
|
||||
|
||||
import { CITable, CITableBuildInfo } from '../CITable';
|
||||
import { circleCIApiRef } from 'api';
|
||||
import { useApi } from '@backstage/core';
|
||||
|
||||
// "lifecycle" : "finished", // :queued, :scheduled, :not_run, :not_running, :running or :finished
|
||||
// "outcome" : "failed", // :canceled, :infrastructure_fail, :timedout, :failed, :no_tests or :success
|
||||
|
||||
const makeReadableStatus = (status: string | undefined) => {
|
||||
if (typeof status === 'undefined') 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[],
|
||||
api: typeof circleCIApiRef.T,
|
||||
): 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})` : '')
|
||||
: '',
|
||||
onRetryClick: () => api.retry(String(buildData.build_num)),
|
||||
source: {
|
||||
branchName: String(buildData.branch),
|
||||
commit: {
|
||||
hash: String(buildData.vcs_revision),
|
||||
url: 'todo',
|
||||
},
|
||||
},
|
||||
status: makeReadableStatus(buildData.status),
|
||||
buildUrl: buildData.build_url,
|
||||
// tests: {
|
||||
// failed: 0,
|
||||
// passed: 10,
|
||||
// skipped: 3,
|
||||
// testUrl: 'nourlnow',
|
||||
// total: 13,
|
||||
// },
|
||||
};
|
||||
return tableBuildInfo;
|
||||
});
|
||||
};
|
||||
|
||||
export const CircleCIFetch: FC<{}> = () => {
|
||||
const [authed, setAuthed] = React.useState(false);
|
||||
const [builds, setBuilds] = React.useState<BuildSummary[]>([]);
|
||||
const api = useApi(circleCIApiRef);
|
||||
|
||||
React.useEffect(() => {
|
||||
const intervalId = setInterval(async () => {
|
||||
if (!authed) {
|
||||
await api.restorePersistedSettings();
|
||||
await api
|
||||
.validateToken()
|
||||
.then(() => {
|
||||
setAuthed(true);
|
||||
})
|
||||
.catch(() => setAuthed(false));
|
||||
}
|
||||
api.getBuilds().then(setBuilds);
|
||||
}, 1500);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [authed]);
|
||||
|
||||
if (!authed) return <div>Not authenticated</div>;
|
||||
const transformedBuilds = transform(builds || [], api);
|
||||
return (
|
||||
<>
|
||||
{!api.authed ? (
|
||||
<div>Not authenticated</div>
|
||||
) : (
|
||||
<CITable builds={transformedBuilds} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { CircleCIFetch } from './CirleCIFetch';
|
||||
@@ -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,48 @@
|
||||
import { useState, FC, useEffect } from 'react';
|
||||
import { List, ListItem, TextField, Button } from '@material-ui/core';
|
||||
import React from 'react';
|
||||
|
||||
export const ProjectInput: FC<{
|
||||
setGitInfo: (info: { owner: string; repo: string }) => void;
|
||||
apiGitInfo?: { owner?: string; repo?: string };
|
||||
}> = ({ setGitInfo, apiGitInfo = {} }) => {
|
||||
const [owner, setOwner] = useState('');
|
||||
const [repo, setRepo] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (apiGitInfo.owner !== owner && apiGitInfo.owner)
|
||||
setOwner(apiGitInfo.owner);
|
||||
if (apiGitInfo.repo !== repo && apiGitInfo.repo) setRepo(apiGitInfo.repo);
|
||||
}, [apiGitInfo]);
|
||||
|
||||
return (
|
||||
<List>
|
||||
<ListItem>
|
||||
<TextField
|
||||
name="circleci-owner"
|
||||
label="Owner"
|
||||
value={owner}
|
||||
onChange={e => setOwner(e.target.value)}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<TextField
|
||||
name="circleci-repo"
|
||||
label="Repo"
|
||||
value={repo}
|
||||
onChange={e => setRepo(e.target.value)}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Button
|
||||
data-testid="load-build-button"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => setGitInfo({ owner, repo })}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</ListItem>
|
||||
</List>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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 { plugin } from './plugin';
|
||||
export * from './api';
|
||||
export * from './proxy';
|
||||
@@ -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,77 @@
|
||||
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,
|
||||
}) => (
|
||||
<>
|
||||
{actions.map((action: BuildStepAction) => (
|
||||
<ActionOutput
|
||||
action={action}
|
||||
name={action.name}
|
||||
url={action.output_url || ''}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
export * from './DetailedViewPage';
|
||||
@@ -0,0 +1,88 @@
|
||||
import React from 'react';
|
||||
import { Button, TextField, List, Grid, ListItem } from '@material-ui/core';
|
||||
import { circleCIApiRef } from 'api';
|
||||
import {
|
||||
InfoCard,
|
||||
useApi,
|
||||
Content,
|
||||
ContentHeader,
|
||||
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);
|
||||
const [authed, setAuthed] = React.useState(api.authed);
|
||||
const [token, setToken] = React.useState('');
|
||||
|
||||
React.useEffect(() => {
|
||||
api
|
||||
.restorePersistedSettings()
|
||||
.then(() => api.validateToken())
|
||||
.then(() => setAuthed(true))
|
||||
.catch(() => setAuthed(false));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Content>
|
||||
<ContentHeader title="Settings">
|
||||
<Button component={RouterLink} to="/circleci">
|
||||
Back
|
||||
</Button>
|
||||
<SupportButton>A description of your plugin goes here.</SupportButton>
|
||||
</ContentHeader>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={6}>
|
||||
<InfoCard title="Authentication">
|
||||
<List>
|
||||
{authed ? (
|
||||
<>Already authed</>
|
||||
) : (
|
||||
<>
|
||||
<ListItem>
|
||||
<TextField
|
||||
name="circleci-token"
|
||||
type="password"
|
||||
label="Token"
|
||||
value={token}
|
||||
onChange={e => setToken(e.target.value)}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<ListItem>
|
||||
<Button
|
||||
data-testid="github-auth-button"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={async () => {
|
||||
api.setToken(token);
|
||||
api
|
||||
.validateToken()
|
||||
.then(() => setAuthed(true))
|
||||
.catch(() => setAuthed(false));
|
||||
}}
|
||||
>
|
||||
Authenticate
|
||||
</Button>
|
||||
</ListItem>
|
||||
</>
|
||||
)}
|
||||
</List>
|
||||
</InfoCard>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<InfoCard title="Project configuration">
|
||||
<ProjectInput
|
||||
apiGitInfo={api.options.vcs}
|
||||
setGitInfo={info => api.setVCSOptions(info)}
|
||||
/>
|
||||
</InfoCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Content>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './SettingsPage';
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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 { plugin } from './plugin';
|
||||
|
||||
describe('circleci', () => {
|
||||
it('should export plugin', () => {
|
||||
expect(plugin).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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 { createPlugin } from '@backstage/core';
|
||||
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', BuildsPage);
|
||||
router.registerRoute('/circleci/build/:buildId', DetailedViewPage);
|
||||
router.registerRoute('/circleci/settings', SettingsPage);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import type {Options} from 'http-proxy-middleware';
|
||||
|
||||
export const proxySettings: Record<string, Options> = {
|
||||
'/circleci/api': {
|
||||
target: 'https://circleci.com/api/v1.1',
|
||||
changeOrigin: true,
|
||||
logLevel: 'debug',
|
||||
pathRewrite: {
|
||||
'^/circleci/api/': '/',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* 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 '@testing-library/jest-dom/extend-expect';
|
||||
require('jest-fetch-mock').enableMocks();
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["src"],
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"baseUrl": "src"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user