feat: total upgrade

This commit is contained in:
Ivan Shmidt
2020-05-07 22:22:38 +02:00
parent fc9cfa0c75
commit b842989456
21 changed files with 269 additions and 109 deletions
@@ -126,6 +126,7 @@ type Props = {
actions?: ReactNode;
cardClassName?: string;
actionsTopRight?: ReactNode;
className?: string;
};
const InfoCard: FC<Props> = ({
@@ -142,6 +143,7 @@ const InfoCard: FC<Props> = ({
actions,
cardClassName,
actionsTopRight,
className,
}) => {
const classes = useStyles();
@@ -169,7 +171,7 @@ const InfoCard: FC<Props> = ({
}
return (
<Card style={calculatedStyle}>
<Card style={calculatedStyle} className={className}>
<ErrorBoundary slackChannel={slackChannel}>
{title && (
<>
@@ -1,7 +1,7 @@
import { Route, Switch } from 'react-router';
import React from 'react';
import { BuildsPage } from 'pages/BuildsPage';
// import { DetailedViewPage } from 'pages/DetailedViewPage';
import { DetailedViewPage } from 'pages/DetailedViewPage';
import { SettingsPage } from 'pages/SettingsPage';
import { Provider, useDispatch } from 'react-redux';
@@ -23,9 +23,9 @@ export const App = () => {
<Switch>
<Route path="/circleci" component={BuildsPage} exact />
<Route path="/circleci/settings" component={SettingsPage} />
<Route path="/circleci/build/:buildId" component={DetailedViewPage} />
</Switch>
</>
{/* <Route path="/build/:buildId" component={DetailedViewPage} /> */}
</Provider>
);
};
+2 -1
View File
@@ -57,9 +57,10 @@ export class CircleCIApi {
}
async getBuild(buildNumber: number, options: CircleCIOptions) {
console.log({ buildNumber, options });
return getFullBuild(options.token, buildNumber, {
circleHost: this.apiUrl,
...options,
...options.vcs,
});
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@@ -1 +0,0 @@
export { CircleCIFetch } from './CirleCIFetch';
@@ -1,9 +1,11 @@
import React from 'react';
import { Header, Page, pageTheme, HeaderLabel } from '@backstage/core';
// @ts-ignore
import logo from '../../assets/circle-logo-horizontal-white.png';
export const Layout: React.FC = ({ children }) => (
<Page theme={pageTheme.tool}>
<Header title="Welcome to circleci!" subtitle="Optional subtitle">
<Header title={<img src={logo} style={{ height: '2em' }} />}>
<HeaderLabel label="Owner" value="Team X" />
<HeaderLabel label="Lifecycle" value="Alpha" />
</Header>
@@ -1,18 +1,39 @@
import React from 'react';
import { Link as RouterLink } from 'react-router-dom';
import { Link as RouterLink, useLocation } 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 />}
import { Button, IconButton, Box } from '@material-ui/core';
import { Settings as SettingsIcon, ArrowBack } from '@material-ui/icons';
export const PluginHeader: React.FC<{ title?: string }> = ({
title = 'Circle CI',
}) => {
const location = useLocation();
const notRoot = !location.pathname.match(/\/circleci\/?$/);
const isSettingsPage = location.pathname.match(/\/circleci\/settings\/?/);
return (
<ContentHeader
title={
(
<Box alignItems="center" display="flex">
{notRoot && (
<IconButton component={RouterLink} to="/circleci">
<ArrowBack />
</IconButton>
)}
{title}
</Box>
) as any
}
>
Settings
</Button>
<SupportButton>A description of your plugin goes here.</SupportButton>
</ContentHeader>
);
{!isSettingsPage && (
<Button
component={RouterLink}
to="/circleci/settings"
startIcon={<SettingsIcon />}
>
Settings
</Button>
)}
<SupportButton>A description of your plugin goes here.</SupportButton>
</ContentHeader>
);
};
@@ -1,34 +1,17 @@
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 { Content } from '@backstage/core';
import { Grid } from '@material-ui/core';
import { Builds } from './lib/Builds';
import { Layout } from 'components/Layout';
import { PluginHeader } from 'components/PluginHeader';
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>
<PluginHeader title="All builds" />
<Grid container spacing={3} direction="column">
<Grid item>
<InfoCard>
<CircleCIFetch />
</InfoCard>
<Builds />
</Grid>
</Grid>
</Content>
@@ -78,7 +78,7 @@ const transform = (buildsData: BuildSummary[]): CITableBuildInfo[] => {
});
};
export const CircleCIFetch: FC<{}> = () => {
export const Builds: FC<{}> = () => {
const dispatch: Dispatch = useDispatch();
const api = useApi(circleCIApiRef);
@@ -89,7 +89,10 @@ export const CircleCIFetch: FC<{}> = () => {
};
}, []);
const { builds } = useSelector((state: iRootState) => state.builds);
const { repo, owner } = useSelector((state: iRootState) => state.settings);
const transformedBuilds = transform(builds);
return <CITable builds={transformedBuilds} />;
return (
<CITable builds={transformedBuilds} projectName={`${owner}/${repo}`} />
);
};
@@ -0,0 +1 @@
export { Builds } from './Builds';
@@ -1,8 +1,14 @@
// 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 { Link, CircularProgress, Button } from '@material-ui/core';
import { Replay as RetryIcon } from '@material-ui/icons';
import {
Link,
CircularProgress,
Button,
Typography,
Box,
} from '@material-ui/core';
import { Replay as RetryIcon, GitHub as GithubIcon } from '@material-ui/icons';
import { Link as RouterLink } from 'react-router-dom';
import {
StatusFailed,
@@ -111,7 +117,7 @@ const generatedColumns: TableColumn[] = [
field: 'id',
type: 'numeric',
// @ts-ignore
width: '10%',
width: '80px',
},
{
title: 'Build',
@@ -154,13 +160,21 @@ const generatedColumns: TableColumn[] = [
];
export const CITable: FC<{
builds: CITableBuildInfo[];
}> = React.memo(({ builds = [] }) => {
projectName: string;
}> = React.memo(({ builds = [], projectName }) => {
// const classes = useStyles();
// const isTestDataAvailable = builds.some(build => build.tests);
return (
<Table
options={{ paging: false }}
data={builds}
title={
<Box display="flex" alignItems="center">
<GithubIcon />
<Box mr={1} />
<Typography variant="h6">{projectName}</Typography>
</Box>
}
columns={generatedColumns}
/>
);
@@ -5,46 +5,79 @@ 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 { ActionOutput } from './lib/ActionOutput/ActionOutput';
import { Layout } from 'components/Layout';
import { Dispatch, iRootState } from 'state/store';
import { useDispatch, useSelector } from 'react-redux';
const BuildName: FC<{ build: BuildWithSteps | null }> = ({ build }) => (
<>
#{build?.build_num} - {build?.branch}
</>
);
import { makeStyles } from '@material-ui/core/styles';
const useStyles = makeStyles((theme) => ({
failed: {
position: 'relative',
'&:after': {
pointerEvents: 'none',
content: '""',
position: 'absolute',
top: 0,
right: 0,
left: 0,
bottom: 0,
boxShadow: `inset 4px 0px 0px ${theme.palette.error.main}`,
},
},
cardContent: {
backgroundColor: theme.palette.background.default,
},
success: {
position: 'relative',
'&:after': {
pointerEvents: 'none',
content: '""',
position: 'absolute',
top: 0,
right: 0,
left: 0,
bottom: 0,
boxShadow: `inset 4px 0px 0px ${theme.palette.success.main}`,
},
},
}));
export const DetailedViewPage: FC<{}> = () => {
let { buildId = '' } = useParams();
const [authed, setAuthed] = React.useState(false);
const [build, setBuild] = React.useState<BuildWithSteps | null>(null);
const classes = useStyles();
const dispatch: Dispatch = useDispatch();
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);
dispatch.buildWithSteps.startPolling({ api, buildId: Number(buildId) });
return () => {
dispatch.buildWithSteps.stopPolling();
};
getBuildAsync();
}, [authed, buildId]);
}, []);
const { build } = useSelector((state: iRootState) => state.buildWithSteps);
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>
<PluginHeader title="Build info" />
<Grid container spacing={3} direction="column">
<Grid item>
<InfoCard
className={build?.failed ? classes.failed : classes.success}
title={<BuildName build={build} />}
cardClassName={classes.cardContent}
>
<BuildsList build={build} />
</InfoCard>
</Grid>
)}
</Grid>
</Content>
</Layout>
);
@@ -64,14 +97,18 @@ const BuildsList: FC<{ build: BuildWithSteps | null }> = ({ build }) => (
const ActionsList: FC<{ actions: BuildStepAction[]; name: string }> = ({
actions,
}) => (
<>
{actions.map((action: BuildStepAction) => (
<ActionOutput
action={action}
name={action.name}
url={action.output_url || ''}
/>
))}
</>
);
}) => {
const classes = useStyles();
return (
<>
{actions.map((action: BuildStepAction) => (
<ActionOutput
className={action.failed ? classes.failed : classes.success}
action={action}
name={action.name}
url={action.output_url || ''}
/>
))}
</>
);
};
@@ -7,20 +7,36 @@ import {
} from '@material-ui/core';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import { makeStyles } from '@material-ui/core/styles';
import { BuildStepAction } from 'circleci-api';
const LazyLog = React.lazy(() => import('react-lazylog/build/LazyLog'));
const useStyles = makeStyles({
expansionPanelDetails: {
padding: 0,
},
button: {
order: -1,
marginRight: 0,
// FIXME: how not to hardcode this
marginLeft: '-20px',
},
});
export const ActionOutput: FC<{
url: string;
name: string;
className?: string;
action: BuildStepAction;
}> = ({ url, name }) => {
}> = ({ url, name, className }) => {
const classes = useStyles();
const [messages, setMessages] = useState([]);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(actionOutput => {
.then((res) => res.json())
.then((actionOutput) => {
actionOutput &&
setMessages(
actionOutput.map(({ message }: { message: string }) => message),
@@ -28,20 +44,26 @@ export const ActionOutput: FC<{
});
}, [url]);
return (
<ExpansionPanel TransitionProps={{ unmountOnExit: true }}>
<ExpansionPanel
TransitionProps={{ unmountOnExit: true }}
className={className}
>
<ExpansionPanelSummary
expandIcon={<ExpandMoreIcon />}
aria-controls={`panel-${name}-content`}
id={`panel-${name}-header`}
IconButtonProps={{
className: classes.button,
}}
>
<Typography>{name}</Typography>
<Typography variant="button">{name}</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<ExpansionPanelDetails className={classes.expansionPanelDetails}>
{messages.length === 0 ? (
'Nothing here...'
) : (
<Suspense fallback="...">
<div style={{ height: '200px', width: '100%' }}>
<div style={{ height: '20vh', width: '100%' }}>
<LazyLog text={messages.join('\n')} extraLines={1} enableSearch />
</div>
</Suspense>
@@ -12,17 +12,15 @@ import {
import {
InfoCard,
Content,
ContentHeader,
SupportButton,
// StatusOK,
// StatusFailed,
} from '@backstage/core';
import { Link as RouterLink } from 'react-router-dom';
import { Layout } from 'components/Layout';
import { SettingsState } from 'state/models/settings';
import { iRootState } from 'state/store';
import { Dispatch } from '../../state/store';
import { Alert } from '@material-ui/lab';
import { PluginHeader } from 'components/PluginHeader';
export const SettingsPage = () => {
const {
@@ -60,12 +58,8 @@ export const SettingsPage = () => {
return (
<Layout>
<Content>
<ContentHeader title="Settings">
<Button component={RouterLink} to="/circleci">
Back
</Button>
<SupportButton>A description of your plugin goes here.</SupportButton>
</ContentHeader>
<PluginHeader title="Settings" />
<Grid container spacing={3}>
<Grid item xs={12} md={3}>
<InfoCard
@@ -76,6 +70,7 @@ export const SettingsPage = () => {
<Snackbar
autoHideDuration={1000}
open={saved}
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
onClose={() => setSaved(false)}
>
<Alert severity="success">Credentials saved.</Alert>
+1 -1
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { createPlugin } from '@backstage/core';
import { App } from './components/App';
import { App } from './App';
export const plugin = createPlugin({
id: 'circleci',
@@ -0,0 +1,77 @@
import { Dispatch, iRootState } from '../store';
import { GitType, BuildWithSteps } from 'circleci-api';
import { CircleCIApi } from 'api';
export type BuildState = {
build: BuildWithSteps | null;
pollingIntervalId: number | null;
pollingState: PollingState;
};
const INTERVAL_AMOUNT = 1500;
export enum PollingState {
Polling,
Idle,
}
export const buildWithSteps = {
state: {
build: null,
pollingIntervalId: null,
pollingState: PollingState.Idle,
} as BuildState,
reducers: {
setBuild(state: BuildState, payload: BuildWithSteps) {
if (state.pollingState !== PollingState.Polling) {
return state;
}
return { ...state, build: payload };
},
setPollingIntervalId(state: BuildState, payload: number | null) {
return {
...state,
pollingIntervalId: payload,
pollingState:
payload === null ? PollingState.Idle : PollingState.Polling,
};
},
},
effects: (dispatch: Dispatch) => ({
async getBuild(
{ api, buildId }: { api: CircleCIApi; buildId: number },
state: iRootState,
) {
try {
const options = {
token: state.settings.token,
vcs: {
owner: state.settings.owner,
repo: state.settings.repo,
type: GitType.GITHUB,
},
};
const build = await api.getBuild(buildId, options);
dispatch.buildWithSteps.setBuild(build);
} catch (e) {
console.log(e);
}
},
startPolling(
{ api, buildId }: { api: CircleCIApi; buildId: number },
state: iRootState,
) {
if (state.buildWithSteps.pollingIntervalId) return;
const intervalId = (setInterval(
() => dispatch.buildWithSteps.getBuild({ buildId, api }),
INTERVAL_AMOUNT,
) as any) as number;
dispatch.buildWithSteps.setPollingIntervalId(intervalId);
},
stopPolling(_: any, state: iRootState) {
const currentIntervalId = state.buildWithSteps.pollingIntervalId;
if (currentIntervalId) clearInterval(currentIntervalId);
dispatch.buildWithSteps.setPollingIntervalId(null);
},
}),
};
@@ -1,13 +1,16 @@
import { settings } from './settings';
import { builds } from './builds';
import { buildWithSteps } from './buildWithSteps';
// no need to extend from Models
export interface RootModel {
settings: typeof settings;
builds: typeof builds;
buildWithSteps: typeof buildWithSteps;
}
export const models = {
settings,
builds,
buildWithSteps,
};
+3 -3
View File
@@ -1,6 +1,6 @@
import { circleCIApiRef } from 'api';
import { BuildSummary } from 'circleci-api';
import { CITableBuildInfo } from 'components/CITable';
import { CITableBuildInfo } from 'pages/BuildsPage/lib/CITable';
const makeReadableStatus = (status: string | undefined) => {
if (typeof status === 'undefined') return '';
@@ -22,7 +22,7 @@ const makeReadableStatus = (status: string | undefined) => {
};
export const transformBuildSummary = (
api: typeof circleCIApiRef.T,
_: typeof circleCIApiRef.T,
buildData: BuildSummary,
) => {
const tableBuildInfo: CITableBuildInfo = {
@@ -31,7 +31,7 @@ export const transformBuildSummary = (
? buildData.subject +
(buildData.retry_of ? ` (retry of #${buildData.retry_of})` : '')
: '',
onRetryClick: () => api.retry(String(buildData.build_num)),
onRetryClick: () => {}, //api.retry(String(buildData.build_num)),
source: {
branchName: String(buildData.branch),
commit: {