diff --git a/packages/core/src/layout/InfoCard/InfoCard.tsx b/packages/core/src/layout/InfoCard/InfoCard.tsx index 7077167828..42c5401de5 100644 --- a/packages/core/src/layout/InfoCard/InfoCard.tsx +++ b/packages/core/src/layout/InfoCard/InfoCard.tsx @@ -126,6 +126,7 @@ type Props = { actions?: ReactNode; cardClassName?: string; actionsTopRight?: ReactNode; + className?: string; }; const InfoCard: FC = ({ @@ -142,6 +143,7 @@ const InfoCard: FC = ({ actions, cardClassName, actionsTopRight, + className, }) => { const classes = useStyles(); @@ -169,7 +171,7 @@ const InfoCard: FC = ({ } return ( - + {title && ( <> diff --git a/plugins/circleci/src/components/App.tsx b/plugins/circleci/src/App.tsx similarity index 84% rename from plugins/circleci/src/components/App.tsx rename to plugins/circleci/src/App.tsx index 799016130f..ec802a88b3 100644 --- a/plugins/circleci/src/components/App.tsx +++ b/plugins/circleci/src/App.tsx @@ -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 = () => { + - {/* */} ); }; diff --git a/plugins/circleci/src/api/index.ts b/plugins/circleci/src/api/index.ts index f7645ab760..c44988509f 100644 --- a/plugins/circleci/src/api/index.ts +++ b/plugins/circleci/src/api/index.ts @@ -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, }); } } diff --git a/plugins/circleci/src/assets/circle-logo-horizontal-black.png b/plugins/circleci/src/assets/circle-logo-horizontal-black.png new file mode 100644 index 0000000000..419d97e69b Binary files /dev/null and b/plugins/circleci/src/assets/circle-logo-horizontal-black.png differ diff --git a/plugins/circleci/src/assets/circle-logo-horizontal-white.png b/plugins/circleci/src/assets/circle-logo-horizontal-white.png new file mode 100644 index 0000000000..0cb076dee1 Binary files /dev/null and b/plugins/circleci/src/assets/circle-logo-horizontal-white.png differ diff --git a/plugins/circleci/src/components/CircleCIFetch/index.ts b/plugins/circleci/src/components/CircleCIFetch/index.ts deleted file mode 100644 index e204a791cd..0000000000 --- a/plugins/circleci/src/components/CircleCIFetch/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { CircleCIFetch } from './CirleCIFetch'; diff --git a/plugins/circleci/src/components/Layout/Layout.tsx b/plugins/circleci/src/components/Layout/Layout.tsx index fe82bf90e8..cea9bc4081 100644 --- a/plugins/circleci/src/components/Layout/Layout.tsx +++ b/plugins/circleci/src/components/Layout/Layout.tsx @@ -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 }) => ( -
+
}>
diff --git a/plugins/circleci/src/components/PluginHeader/PluginHeader.tsx b/plugins/circleci/src/components/PluginHeader/PluginHeader.tsx index f725a979a9..25719c3f42 100644 --- a/plugins/circleci/src/components/PluginHeader/PluginHeader.tsx +++ b/plugins/circleci/src/components/PluginHeader/PluginHeader.tsx @@ -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 = () => ( - - - A description of your plugin goes here. - -); + {!isSettingsPage && ( + + )} + A description of your plugin goes here. + + ); +}; diff --git a/plugins/circleci/src/pages/BuildsPage/BuildsPage.tsx b/plugins/circleci/src/pages/BuildsPage/BuildsPage.tsx index 32939d2ebe..ff078dcd95 100644 --- a/plugins/circleci/src/pages/BuildsPage/BuildsPage.tsx +++ b/plugins/circleci/src/pages/BuildsPage/BuildsPage.tsx @@ -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<{}> = () => ( - - - A description of your plugin goes here. - + - - - + diff --git a/plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx b/plugins/circleci/src/pages/BuildsPage/lib/Builds/Builds.tsx similarity index 93% rename from plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx rename to plugins/circleci/src/pages/BuildsPage/lib/Builds/Builds.tsx index 68358dccd6..b23f768c7b 100644 --- a/plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx +++ b/plugins/circleci/src/pages/BuildsPage/lib/Builds/Builds.tsx @@ -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 ; + return ( + + ); }; diff --git a/plugins/circleci/src/pages/BuildsPage/lib/Builds/index.ts b/plugins/circleci/src/pages/BuildsPage/lib/Builds/index.ts new file mode 100644 index 0000000000..c2649d7919 --- /dev/null +++ b/plugins/circleci/src/pages/BuildsPage/lib/Builds/index.ts @@ -0,0 +1 @@ +export { Builds } from './Builds'; diff --git a/plugins/circleci/src/components/CITable/CITable.tsx b/plugins/circleci/src/pages/BuildsPage/lib/CITable/CITable.tsx similarity index 89% rename from plugins/circleci/src/components/CITable/CITable.tsx rename to plugins/circleci/src/pages/BuildsPage/lib/CITable/CITable.tsx index 52594b9227..ed720446bc 100644 --- a/plugins/circleci/src/components/CITable/CITable.tsx +++ b/plugins/circleci/src/pages/BuildsPage/lib/CITable/CITable.tsx @@ -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 ( + + + {projectName} + + } columns={generatedColumns} /> ); diff --git a/plugins/circleci/src/components/CITable/index.ts b/plugins/circleci/src/pages/BuildsPage/lib/CITable/index.ts similarity index 100% rename from plugins/circleci/src/components/CITable/index.ts rename to plugins/circleci/src/pages/BuildsPage/lib/CITable/index.ts diff --git a/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx b/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx index 37ff1e0655..d8ee53114a 100644 --- a/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx +++ b/plugins/circleci/src/pages/DetailedViewPage/DetailedViewPage.tsx @@ -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(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 ( - - {!api.authed ? ( -
Not authenticated
- ) : ( - - - - - - + + + + + } + cardClassName={classes.cardContent} + > + + - )} +
); @@ -64,14 +97,18 @@ const BuildsList: FC<{ build: BuildWithSteps | null }> = ({ build }) => ( const ActionsList: FC<{ actions: BuildStepAction[]; name: string }> = ({ actions, -}) => ( - <> - {actions.map((action: BuildStepAction) => ( - - ))} - -); +}) => { + const classes = useStyles(); + return ( + <> + {actions.map((action: BuildStepAction) => ( + + ))} + + ); +}; diff --git a/plugins/circleci/src/components/ActionOutput/ActionOutput.tsx b/plugins/circleci/src/pages/DetailedViewPage/lib/ActionOutput/ActionOutput.tsx similarity index 60% rename from plugins/circleci/src/components/ActionOutput/ActionOutput.tsx rename to plugins/circleci/src/pages/DetailedViewPage/lib/ActionOutput/ActionOutput.tsx index 7cc92098a1..18293b790b 100644 --- a/plugins/circleci/src/components/ActionOutput/ActionOutput.tsx +++ b/plugins/circleci/src/pages/DetailedViewPage/lib/ActionOutput/ActionOutput.tsx @@ -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 ( - + } aria-controls={`panel-${name}-content`} id={`panel-${name}-header`} + IconButtonProps={{ + className: classes.button, + }} > - {name} + {name} - + {messages.length === 0 ? ( 'Nothing here...' ) : ( -
+
diff --git a/plugins/circleci/src/components/ActionOutput/index.ts b/plugins/circleci/src/pages/DetailedViewPage/lib/ActionOutput/index.ts similarity index 100% rename from plugins/circleci/src/components/ActionOutput/index.ts rename to plugins/circleci/src/pages/DetailedViewPage/lib/ActionOutput/index.ts diff --git a/plugins/circleci/src/pages/SettingsPage/SettingsPage.tsx b/plugins/circleci/src/pages/SettingsPage/SettingsPage.tsx index 033c5cd2ee..f99181c26c 100644 --- a/plugins/circleci/src/pages/SettingsPage/SettingsPage.tsx +++ b/plugins/circleci/src/pages/SettingsPage/SettingsPage.tsx @@ -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 ( - - - A description of your plugin goes here. - + + { setSaved(false)} > Credentials saved. diff --git a/plugins/circleci/src/plugin.ts b/plugins/circleci/src/plugin.ts index 77a223a162..8e308bd12c 100644 --- a/plugins/circleci/src/plugin.ts +++ b/plugins/circleci/src/plugin.ts @@ -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', diff --git a/plugins/circleci/src/state/models/buildWithSteps.ts b/plugins/circleci/src/state/models/buildWithSteps.ts new file mode 100644 index 0000000000..109ee08df0 --- /dev/null +++ b/plugins/circleci/src/state/models/buildWithSteps.ts @@ -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); + }, + }), +}; diff --git a/plugins/circleci/src/state/models/index.ts b/plugins/circleci/src/state/models/index.ts index 39623b96f9..ff1396d97d 100644 --- a/plugins/circleci/src/state/models/index.ts +++ b/plugins/circleci/src/state/models/index.ts @@ -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, }; diff --git a/plugins/circleci/src/utils/index.ts b/plugins/circleci/src/utils/index.ts index 77697659e0..2bf45aa3e4 100644 --- a/plugins/circleci/src/utils/index.ts +++ b/plugins/circleci/src/utils/index.ts @@ -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: {