fix(circleci): big refactor and cleanup

This commit is contained in:
Ivan Shmidt
2020-09-04 15:58:25 +02:00
parent 5b3ad2a284
commit 72b0d9c9f5
26 changed files with 96 additions and 560 deletions
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,4 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { default as Settings } from './Settings';
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>
);
};
@@ -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;
@@ -1,19 +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 DetailedViewPage,
BuildWithSteps,
} from './BuildWithStepsPage';
@@ -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';
-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;
-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;
}