diff --git a/packages/cli/src/commands/plugin/serve.ts b/packages/cli/src/commands/plugin/serve.ts index 5700992644..f9768ec497 100644 --- a/packages/cli/src/commands/plugin/serve.ts +++ b/packages/cli/src/commands/plugin/serve.ts @@ -14,13 +14,19 @@ * limitations under the License. */ +import fs from 'fs-extra'; import { serveBundle } from '../../lib/bundler'; import { Command } from 'commander'; +import { paths } from '../../lib/paths'; export default async (cmd: Command) => { + const pkgPath = paths.resolveTarget('package.json'); + const pkg = await fs.readJson(pkgPath); + const waitForExit = await serveBundle({ entry: 'dev/index', checksEnabled: cmd.check, + proxy: pkg.proxy, }); await waitForExit(); diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index c130c14d4a..ca224fdd1c 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import fs from 'fs-extra'; import yn from 'yn'; import webpack from 'webpack'; import WebpackDevServer from 'webpack-dev-server'; @@ -36,6 +37,8 @@ export async function serveBundle(options: ServeOptions) { const urls = prepareUrls(protocol, host, port); const paths = resolveBundlingPaths(options); + const pkgPath = paths.targetPackageJson; + const pkg = await fs.readJson(pkgPath); const config = createConfig(paths, { ...options, isDev: true }); const compiler = webpack(config); @@ -48,7 +51,7 @@ export async function serveBundle(options: ServeOptions) { https: protocol === 'https', host, port, - proxy: options.proxy, + proxy: pkg.proxy, }); await new Promise((resolve, reject) => { diff --git a/plugins/circleci/dev/index.tsx b/plugins/circleci/dev/index.tsx new file mode 100644 index 0000000000..ed7dd5de9c --- /dev/null +++ b/plugins/circleci/dev/index.tsx @@ -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 { createDevApp } from '@backstage/dev-utils'; +import { plugin } from '../src/plugin'; +import { circleCIApiRef, CircleCIApi } from '../src/api'; + +createDevApp() + .registerPlugin(plugin) + .registerApiFactory({ + deps: {}, + factory: () => new CircleCIApi(), + implements: circleCIApiRef, + }) + .render(); diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index cd00135004..e3e0372d88 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -3,18 +3,31 @@ "version": "0.1.1-alpha.4", "main": "dist/index.esm.js", "module": "dist/index.esm.js", - "types": "dist/index.d.ts", + "types": "src/index.ts", "license": "Apache-2.0", "private": true, + "proxy": { + "/circleci/api": { + "target": "https://circleci.com/api/v1.1", + "changeOrigin": true, + "pathRewrite": { + "^/circleci/api/": "/" + } + } + }, "scripts": { "build": "backstage-cli plugin:build", "lint": "backstage-cli lint", "test": "backstage-cli test", - "clean": "backstage-cli clean" + "clean": "backstage-cli clean", + "diff": "backstage-cli plugin:diff", + "start": "backstage-cli plugin:serve", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.4", - "@backstage/theme": "^0.1.1-alpha.4", + "@backstage/core": "^0.1.1-alpha.5", + "@backstage/theme": "^0.1.1-alpha.5", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -22,24 +35,25 @@ "circleci-api": "^4.0.0", "fast-deep-equal": "^3.1.1", "moment": "^2.25.3", - "react": "16.13.1", - "react-dom": "16.13.1", + "react": "^16.13.1", + "react-dom": "^16.13.1", "react-lazylog": "^4.5.2", "react-router": "^5.1.2", "react-router-dom": "^5.1.2", - "react-use": "^13.0.0" + "react-use": "^14.2.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.4", - "@testing-library/jest-dom": "^4.2.4", + "@backstage/cli": "^0.1.1-alpha.5", + "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", - "@testing-library/user-event": "^7.1.2", - "@types/jest": "^24.0.0", + "@testing-library/user-event": "^10.2.4", + "@types/jest": "^25.2.1", "@types/node": "^12.0.0", - "@types/testing-library__jest-dom": "5.0.2", - "jest-fetch-mock": "^3.0.3" + "@types/testing-library__jest-dom": "^5.0.4", + "jest-fetch-mock": "^3.0.3", + "@backstage/dev-utils": "^0.1.1-alpha.5" }, "files": [ - "dist" + "dist/**/*.{js,d.ts}" ] } diff --git a/plugins/circleci/src/api/index.ts b/plugins/circleci/src/api/index.ts index d98f511f35..f683f79ff9 100644 --- a/plugins/circleci/src/api/index.ts +++ b/plugins/circleci/src/api/index.ts @@ -48,8 +48,15 @@ export class CircleCIApi { }); } - async getBuilds(options: CircleCIOptions) { + async getBuilds( + { limit = 10, offset = 0 }: { limit: number; offset: number }, + options: CircleCIOptions, + ) { return getBuildSummaries(options.token, { + options: { + limit, + offset, + }, vcs: {}, circleHost: this.apiUrl, ...options, diff --git a/plugins/circleci/src/components/App.tsx b/plugins/circleci/src/components/App.tsx index 3324753b88..46b70cb500 100644 --- a/plugins/circleci/src/components/App.tsx +++ b/plugins/circleci/src/components/App.tsx @@ -16,20 +16,24 @@ import React from 'react'; import { Switch, Route } from 'react-router'; import { BuildsPage } from '../pages/BuildsPage'; -import { SettingsPage } from '../pages/SettingsPage'; import { DetailedViewPage } from '../pages/BuildWithStepsPage'; import { AppStateProvider } from '../state'; +import { Settings } from './Settings'; -export const App = () => ( - - - - - - - -); +export const App = () => { + return ( + + <> + + + + + + + + ); +}; diff --git a/plugins/circleci/src/components/Layout/Layout.tsx b/plugins/circleci/src/components/Layout/Layout.tsx index 906db182b4..6ae97532ce 100644 --- a/plugins/circleci/src/components/Layout/Layout.tsx +++ b/plugins/circleci/src/components/Layout/Layout.tsx @@ -17,20 +17,22 @@ import React from 'react'; import { Header, Page, pageTheme, HeaderLabel } from '@backstage/core'; import { Box } from '@material-ui/core'; -export const Layout: React.FC = ({ children }) => ( - -
- Backstage Logo - Circle CI - - } - > - - -
- {children} -
-); +export const Layout: React.FC = ({ children }) => { + return ( + +
+ Backstage Logo + Circle CI + + } + > + + +
+ {children} +
+ ); +}; diff --git a/plugins/circleci/src/components/PluginHeader/PluginHeader.tsx b/plugins/circleci/src/components/PluginHeader/PluginHeader.tsx index 7bd35d0805..4bab4b5921 100644 --- a/plugins/circleci/src/components/PluginHeader/PluginHeader.tsx +++ b/plugins/circleci/src/components/PluginHeader/PluginHeader.tsx @@ -19,9 +19,11 @@ 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 = ({ title = 'Circle CI' }) => { + const [, { showSettings }] = useSettings(); const location = useLocation(); const notRoot = !location.pathname.match(/\/circleci\/?$/); const isSettingsPage = location.pathname.match(/\/circleci\/settings\/?/); @@ -40,11 +42,7 @@ export const PluginHeader: FC = ({ title = 'Circle CI' }) => { )} > {!isSettingsPage && ( - )} diff --git a/plugins/circleci/src/components/Settings/Settings.tsx b/plugins/circleci/src/components/Settings/Settings.tsx new file mode 100644 index 0000000000..5159c156cc --- /dev/null +++ b/plugins/circleci/src/components/Settings/Settings.tsx @@ -0,0 +1,129 @@ +/* + * 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 } 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] = React.useState(() => tokenFromStore); + const [owner, setOwner] = React.useState(() => ownerFromStore); + const [repo, setRepo] = React.useState(() => repoFromStore); + + React.useEffect(() => { + if (tokenFromStore !== token) { + setToken(tokenFromStore); + } + if (ownerFromStore !== owner) { + setOwner(ownerFromStore); + } + if (repoFromStore !== repo) { + setRepo(repoFromStore); + } + }, [ownerFromStore, repoFromStore, tokenFromStore]); + + const [saved, setSaved] = useState(false); + + return ( + <> + setSaved(false)} + > + Credentials saved. + + + + Project Credentials + {/* {authed ? : } */} + + + + + setToken(e.target.value)} + /> + + + setOwner(e.target.value)} + /> + + + setRepo(e.target.value)} + /> + + + + + + + + + + + ); +}; + +export default Settings; diff --git a/plugins/circleci/src/pages/SettingsPage/index.ts b/plugins/circleci/src/components/Settings/index.ts similarity index 91% rename from plugins/circleci/src/pages/SettingsPage/index.ts rename to plugins/circleci/src/components/Settings/index.ts index e0ba8157b2..c04ded6dea 100644 --- a/plugins/circleci/src/pages/SettingsPage/index.ts +++ b/plugins/circleci/src/components/Settings/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { default as SettingsPage } from './SettingsPage'; +export { default as Settings } from './Settings'; diff --git a/plugins/circleci/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx b/plugins/circleci/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx index 021a2ced2b..041c495c90 100644 --- a/plugins/circleci/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx +++ b/plugins/circleci/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx @@ -15,7 +15,7 @@ */ import React, { FC, useEffect } from 'react'; import { useParams } from 'react-router-dom'; -import { Content, InfoCard } from '@backstage/core'; +import { Content, InfoCard, Progress } from '@backstage/core'; import { BuildWithSteps, BuildStepAction } from '../../api'; import { Grid, Box, Link, IconButton } from '@material-ui/core'; import { makeStyles } from '@material-ui/core/styles'; @@ -27,7 +27,7 @@ import { useSettings } from '../../state/useSettings'; import { useBuildWithSteps } from '../../state/useBuildWithSteps'; const IconLink = IconButton as typeof Link; -const BuildName: FC<{ build: BuildWithSteps | null }> = ({ build }) => ( +const BuildName: FC<{ build?: BuildWithSteps }> = ({ build }) => ( #{build?.build_num} - {build?.subject} @@ -96,7 +96,7 @@ const BuildWithStepsPage: FC<{}> = () => { const { buildId = '' } = useParams(); const classes = useStyles(); const [settings] = useSettings(); - const [build, { startPolling, stopPolling }] = useBuildWithSteps( + const [{ loading, value }, { startPolling, stopPolling }] = useBuildWithSteps( parseInt(buildId, 10), ); @@ -113,11 +113,11 @@ const BuildWithStepsPage: FC<{}> = () => { } + className={pickClassName(classes, value)} + title={} cardClassName={classes.cardContent} > - + {loading ? : } @@ -126,7 +126,7 @@ const BuildWithStepsPage: FC<{}> = () => { ); }; -const BuildsList: FC<{ build: BuildWithSteps | null }> = ({ build }) => ( +const BuildsList: FC<{ build?: BuildWithSteps }> = ({ build }) => ( {build && build.steps && diff --git a/plugins/circleci/src/pages/BuildsPage/lib/Builds/Builds.tsx b/plugins/circleci/src/pages/BuildsPage/lib/Builds/Builds.tsx index a121822af3..bed96bb24c 100644 --- a/plugins/circleci/src/pages/BuildsPage/lib/Builds/Builds.tsx +++ b/plugins/circleci/src/pages/BuildsPage/lib/Builds/Builds.tsx @@ -13,74 +13,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { FC, useEffect } from 'react'; -import { CITableBuildInfo, CITable } from '../CITable'; -import { BuildSummary } from '../../../../api'; -import { useBuilds } from '../../../../state/useBuilds'; -import { useSettings } from '../../../../state/useSettings'; - -const makeReadableStatus = (status: string | undefined) => { - if (!status) 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)[status]; -}; - -const transform = ( - buildsData: BuildSummary[], - restartBuild: { (buildId: number): Promise }, -): 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})` : '') - : '', - onRestartClick: () => - typeof buildData.build_num !== 'undefined' && - restartBuild(buildData.build_num), - source: { - branchName: String(buildData.branch), - commit: { - hash: String(buildData.vcs_revision), - url: 'todo', - }, - }, - status: makeReadableStatus(buildData.status), - buildUrl: buildData.build_url, - }; - return tableBuildInfo; - }); -}; +import React, { FC } from 'react'; +import { CITable } from '../CITable'; +import { useBuilds } from '../../../../state'; export const Builds: FC<{}> = () => { const [ - builds, - { restartBuild: handleRestartBuild, startPolling, stopPolling }, + { total, loading, value, projectName, page, pageSize }, + { setPage, retry, setPageSize }, ] = useBuilds(); - const [{ repo, owner }] = useSettings(); - - useEffect(() => { - startPolling(); - return () => stopPolling(); - }, [repo, owner]); - - const transformedBuilds = transform(builds, handleRestartBuild); - return ( - + ); }; diff --git a/plugins/circleci/src/pages/BuildsPage/lib/CITable/CITable.tsx b/plugins/circleci/src/pages/BuildsPage/lib/CITable/CITable.tsx index f8e0026ff5..b349dcfcbe 100644 --- a/plugins/circleci/src/pages/BuildsPage/lib/CITable/CITable.tsx +++ b/plugins/circleci/src/pages/BuildsPage/lib/CITable/CITable.tsx @@ -114,14 +114,46 @@ const generatedColumns: TableColumn[] = [ width: '10%', }, ]; -export const CITable: FC<{ + +type Props = { + loading: boolean; + retry: () => void; builds: CITableBuildInfo[]; projectName: string; -}> = React.memo(({ builds = [], projectName }) => { + page: number; + onChangePage: (page: number) => void; + total: number; + pageSize: number; + onChangePageSize: (pageSize: number) => void; +}; +export const CITable: FC = ({ + projectName, + loading, + pageSize, + page, + retry, + builds, + onChangePage, + onChangePageSize, + total, +}) => { return ( , + tooltip: 'Refresh Data', + isFreeAction: true, + onClick: () => retry(), + }, + ]} data={builds} + onChangePage={onChangePage} + onChangeRowsPerPage={onChangePageSize} title={ @@ -132,4 +164,4 @@ export const CITable: FC<{ columns={generatedColumns} /> ); -}); +}; diff --git a/plugins/circleci/src/pages/SettingsPage/SettingsPage.tsx b/plugins/circleci/src/pages/SettingsPage/SettingsPage.tsx deleted file mode 100644 index f009b7487f..0000000000 --- a/plugins/circleci/src/pages/SettingsPage/SettingsPage.tsx +++ /dev/null @@ -1,139 +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 } from 'react'; -import { - Button, - TextField, - List, - Grid, - ListItem, - Snackbar, - Box, -} from '@material-ui/core'; -import { Alert } from '@material-ui/lab'; -import { InfoCard, Content } from '@backstage/core'; -import { Layout } from '../../components/Layout'; -import { PluginHeader } from '../../components/PluginHeader'; -import { useSettings } from '../../state/useSettings'; - -const SettingsPage = () => { - const [ - { repo: repoFromStore, owner: ownerFromStore, token: tokenFromStore }, - { saveSettings }, - ] = useSettings(); - - const [token, setToken] = React.useState(() => tokenFromStore); - const [owner, setOwner] = React.useState(() => ownerFromStore); - const [repo, setRepo] = React.useState(() => repoFromStore); - - React.useEffect(() => { - if (tokenFromStore !== token) { - setToken(tokenFromStore); - } - if (ownerFromStore !== owner) { - setOwner(ownerFromStore); - } - if (repoFromStore !== repo) { - setRepo(repoFromStore); - } - }, [ownerFromStore, repoFromStore, tokenFromStore]); - - const [saved, setSaved] = useState(false); - - return ( - - - - - - - - Project Credentials - {/* {authed ? : } */} - setSaved(false)} - > - Credentials saved. - - - } - > - - - setToken(e.target.value)} - /> - - - setOwner(e.target.value)} - /> - - - setRepo(e.target.value)} - /> - - - - - - - - - - - - - ); -}; - -export default SettingsPage; diff --git a/plugins/circleci/src/state/AppState.tsx b/plugins/circleci/src/state/AppState.tsx index b4fedb6abd..f229175111 100644 --- a/plugins/circleci/src/state/AppState.tsx +++ b/plugins/circleci/src/state/AppState.tsx @@ -15,9 +15,8 @@ */ import React, { FC, useReducer, Dispatch, Reducer } from 'react'; import { circleCIApiRef } from '../api'; -import type { SettingsState, State, Action } from './types'; -export type { SettingsState }; -import equal from 'fast-deep-equal'; +import type { State, Action, SettingsState } from './types'; +export { SettingsState }; export const AppContext = React.createContext<[State, Dispatch]>( [] as any, @@ -25,13 +24,10 @@ export const AppContext = React.createContext<[State, Dispatch]>( export const STORAGE_KEY = `${circleCIApiRef.id}.settings`; const initialState: State = { - settings: { - owner: '', - repo: '', - token: '', - }, - builds: [], - buildsWithSteps: {}, + owner: '', + repo: '', + token: '', + showSettings: false, }; const reducer: Reducer = (state, action) => { @@ -39,23 +35,12 @@ const reducer: Reducer = (state, action) => { case 'setCredentials': return { ...state, - settings: { ...state.settings, ...action.payload }, + ...action.payload, }; - case 'setBuilds': - if (equal(action.payload, state.builds)) return state; - return { - ...state, - builds: action.payload, - }; - case 'setBuildWithSteps': { - return { - ...state, - buildsWithSteps: { - ...state.buildsWithSteps, - [action.payload.build_num!]: action.payload, - }, - }; - } + case 'showSettings': + return { ...state, showSettings: true }; + case 'hideSettings': + return { ...state, showSettings: false }; default: return state; } @@ -63,7 +48,6 @@ const reducer: Reducer = (state, action) => { export const AppStateProvider: FC = ({ children }) => { const [state, dispatch] = useReducer(reducer, initialState); - return ( <>{children} diff --git a/plugins/circleci/src/state/index.ts b/plugins/circleci/src/state/index.ts index 4ef67f62ea..2321103eb4 100644 --- a/plugins/circleci/src/state/index.ts +++ b/plugins/circleci/src/state/index.ts @@ -14,3 +14,6 @@ * limitations under the License. */ export * from './AppState'; +export * from './useSettings'; +export * from './useBuilds'; +export * from './useBuildWithSteps'; diff --git a/plugins/circleci/src/state/types.ts b/plugins/circleci/src/state/types.ts index de801ad213..41b3577082 100644 --- a/plugins/circleci/src/state/types.ts +++ b/plugins/circleci/src/state/types.ts @@ -13,41 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { BuildSummary, BuildWithSteps } from '../api'; -export type SettingsState = { - owner: string; - repo: string; - token: string; +export type Settings = { owner: string; repo: string; token: string }; +export type SettingsState = Settings & { + showSettings: boolean; }; -export type BuildsState = BuildSummary[]; +export type State = SettingsState; -export type State = { - settings: SettingsState; - builds: BuildsState; - buildsWithSteps: BuildsWithStepsState; -}; +type SettingsAction = + | { + type: 'setCredentials'; + payload: { + repo: string; + owner: string; + token: string; + }; + } + | { type: 'showSettings' } + | { type: 'hideSettings' }; -type SettingsAction = { - type: 'setCredentials'; - payload: { - repo: string; - owner: string; - token: string; - }; -}; - -type BuildsAction = { - type: 'setBuilds'; - payload: BuildSummary[]; -}; - -type BuildsWithStepsAction = { - type: 'setBuildWithSteps'; - payload: BuildWithSteps; -}; - -export type BuildsWithStepsState = Record; - -export type Action = SettingsAction | BuildsAction | BuildsWithStepsAction; +export type Action = SettingsAction; diff --git a/plugins/circleci/src/state/useBuildWithSteps.ts b/plugins/circleci/src/state/useBuildWithSteps.ts index 3cfe830b25..7aba770851 100644 --- a/plugins/circleci/src/state/useBuildWithSteps.ts +++ b/plugins/circleci/src/state/useBuildWithSteps.ts @@ -14,50 +14,47 @@ * limitations under the License. */ import { errorApiRef, useApi } from '@backstage/core'; -import { useContext } from 'react'; +import { useCallback } from 'react'; +import { useAsyncRetry } from 'react-use'; import { circleCIApiRef, GitType } from '../api/index'; -import { AppContext } from '.'; +import { useAsyncPolling } from './useAsyncPolling'; import { useSettings } from './useSettings'; -import { useAsyncPolling } from './useAsyncPolling'; - -const INTERVAL_AMOUNT = 3000; - +const INTERVAL_AMOUNT = 1500; export function useBuildWithSteps(buildId: number) { - const [settings] = useSettings(); - const [{ buildsWithSteps }, dispatch] = useContext(AppContext); + const [{ token, repo, owner }] = useSettings(); const api = useApi(circleCIApiRef); const errorApi = useApi(errorApiRef); - const getBuildWithSteps = async () => { + const getBuildWithSteps = useCallback(async () => { + if (owner === '' || repo === '' || token === '') { + return Promise.reject('No credentials provided'); + } + try { const options = { - token: settings.token, + token: token, vcs: { - owner: settings.owner, - repo: settings.repo, + owner: owner, + repo: repo, type: GitType.GITHUB, }, }; const build = await api.getBuild(buildId, options); - dispatch({ type: 'setBuildWithSteps', payload: build }); + return Promise.resolve(build); } catch (e) { errorApi.post(e); + return Promise.reject(e); } - }; - - const { startPolling, stopPolling } = useAsyncPolling( - getBuildWithSteps, - INTERVAL_AMOUNT, - ); + }, [token, owner, repo, buildId]); const restartBuild = async () => { try { await api.retry(buildId, { - token: settings.token, + token: token, vcs: { - owner: settings.owner, - repo: settings.repo, + owner: owner, + repo: repo, type: GitType.GITHUB, }, }); @@ -66,15 +63,22 @@ export function useBuildWithSteps(buildId: number) { } }; - const build = buildsWithSteps[buildId]; + const { loading, value, retry } = useAsyncRetry(() => getBuildWithSteps(), [ + getBuildWithSteps, + ]); + + const { startPolling, stopPolling } = useAsyncPolling( + getBuildWithSteps, + INTERVAL_AMOUNT, + ); return [ - build, + { loading, value, retry }, { restartBuild, + getBuildWithSteps, startPolling, stopPolling, - getBuildWithSteps, }, ] as const; } diff --git a/plugins/circleci/src/state/useBuilds.ts b/plugins/circleci/src/state/useBuilds.ts new file mode 100644 index 0000000000..248cc677f6 --- /dev/null +++ b/plugins/circleci/src/state/useBuilds.ts @@ -0,0 +1,153 @@ +/* + * 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 { BuildSummary, GitType } from 'circleci-api'; +import { useCallback, useEffect, useState } from 'react'; +import { useAsyncRetry } from 'react-use'; +import { circleCIApiRef } from '../api/index'; +import { CITableBuildInfo } from '../pages/BuildsPage/lib/CITable'; +import { useSettings } from './useSettings'; + +const makeReadableStatus = (status: string | undefined) => { + if (!status) 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)[status]; +}; + +export const transform = ( + buildsData: BuildSummary[], + restartBuild: { (buildId: number): Promise }, +): 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})` : '') + : '', + onRestartClick: () => + typeof buildData.build_num !== 'undefined' && + restartBuild(buildData.build_num), + source: { + branchName: String(buildData.branch), + commit: { + hash: String(buildData.vcs_revision), + url: 'todo', + }, + }, + status: makeReadableStatus(buildData.status), + buildUrl: buildData.build_url, + }; + return tableBuildInfo; + }); +}; + +export function useBuilds() { + const [{ repo, owner, token }] = useSettings(); + + const api = useApi(circleCIApiRef); + const errorApi = useApi(errorApiRef); + + const [total, setTotal] = useState(0); + const [page, setPage] = useState(0); + const [pageSize, setPageSize] = useState(5); + + const getBuilds = useCallback( + async ({ limit, offset }: { limit: number; offset: number }) => { + if (owner === '' || repo === '' || token === '') { + return Promise.reject('No credentials provided'); + } + + try { + return await api.getBuilds( + { limit, offset }, + { + token: token, + vcs: { + owner: owner, + repo: repo, + type: GitType.GITHUB, + }, + }, + ); + } catch (e) { + errorApi.post(e); + return Promise.reject(e); + } + }, + [repo, token, owner], + ); + + const restartBuild = async (buildId: number) => { + try { + await api.retry(buildId, { + token: token, + vcs: { + owner: owner, + repo: repo, + type: GitType.GITHUB, + }, + }); + } catch (e) { + errorApi.post(e); + } + }; + + useEffect(() => { + getBuilds({ limit: 1, offset: 0 }).then((b) => setTotal(b?.[0].build_num!)); + }, [repo]); + + const { loading, value, retry } = useAsyncRetry( + () => + getBuilds({ + offset: page * pageSize, + limit: pageSize, + }).then((builds) => transform(builds ?? [], restartBuild)), + [page, pageSize, getBuilds], + ); + + const projectName = `${owner}/${repo}`; + return [ + { + page, + pageSize, + loading, + value, + projectName, + total, + }, + { + getBuilds, + setPage, + setPageSize, + restartBuild, + retry, + }, + ] as const; +} diff --git a/plugins/circleci/src/state/useBuilds.tsx b/plugins/circleci/src/state/useBuilds.tsx deleted file mode 100644 index 4a8908a09f..0000000000 --- a/plugins/circleci/src/state/useBuilds.tsx +++ /dev/null @@ -1,79 +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 { GitType } from 'circleci-api'; -import { useContext } from 'react'; -import { circleCIApiRef } from '../api/index'; -import { AppContext } from '.'; -import { useAsyncPolling } from './useAsyncPolling'; - -const INTERVAL_AMOUNT = 3000; - -export function useBuilds() { - const [{ builds, settings }, dispatch] = useContext(AppContext); - - const api = useApi(circleCIApiRef); - const errorApi = useApi(errorApiRef); - - const getBuilds = async () => { - if (settings.owner === '' || settings.repo === '') return; - try { - const newBuilds = await api.getBuilds({ - token: settings.token, - vcs: { - owner: settings.owner, - repo: settings.repo, - type: GitType.GITHUB, - }, - }); - dispatch({ - type: 'setBuilds', - payload: newBuilds, - }); - } catch (e) { - errorApi.post(e); - } - }; - - const { startPolling, stopPolling } = useAsyncPolling( - getBuilds, - INTERVAL_AMOUNT, - ); - - const restartBuild = async (buildId: number) => { - try { - await api.retry(buildId, { - token: settings.token, - vcs: { - owner: settings.owner, - repo: settings.repo, - type: GitType.GITHUB, - }, - }); - } catch (e) { - errorApi.post(e); - } - }; - - return [ - builds, - { - restartBuild, - startPolling, - stopPolling, - }, - ] as const; -} diff --git a/plugins/circleci/src/state/useSettings.ts b/plugins/circleci/src/state/useSettings.ts index c2d5714f0f..5482abeb8b 100644 --- a/plugins/circleci/src/state/useSettings.ts +++ b/plugins/circleci/src/state/useSettings.ts @@ -13,12 +13,13 @@ * 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, SettingsState } from '.'; -import { useApi, errorApiRef } from '@backstage/core'; +import { AppContext, STORAGE_KEY } from './AppState'; +import { Settings } from './types'; export function useSettings() { - const [{ settings }, dispatch] = useContext(AppContext); + const [settings, dispatch] = useContext(AppContext); const errorApi = useApi(errorApiRef); @@ -44,20 +45,22 @@ export function useSettings() { rehydrate(); }, []); - const persist = (state: SettingsState) => { + const persist = (state: Settings) => { sessionStorage.setItem(STORAGE_KEY, JSON.stringify(state)); }; return [ settings, { - saveSettings: (state: SettingsState) => { + saveSettings: (state: Settings) => { persist(state); dispatch({ type: 'setCredentials', payload: state, }); }, + showSettings: () => dispatch({ type: 'showSettings' }), + hideSettings: () => dispatch({ type: 'hideSettings' }), }, ] as const; } diff --git a/yarn.lock b/yarn.lock index 4573ebcf46..81393b5d65 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18376,10 +18376,10 @@ source-map-resolve@^0.5.0, source-map-resolve@^0.5.2: source-map-url "^0.4.0" urix "^0.1.0" -source-map-support@^0.5.6, source-map-support@~0.5.12: - version "0.5.16" - resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042" - integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ== +source-map-support@^0.5.17, source-map-support@^0.5.6, source-map-support@~0.5.12: + version "0.5.19" + resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" + integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== dependencies: buffer-from "^1.0.0" source-map "^0.6.0" @@ -19659,14 +19659,14 @@ ts-loader@^7.0.4: semver "^6.0.0" ts-node@^8.6.2: - version "8.8.1" - resolved "https://registry.npmjs.org/ts-node/-/ts-node-8.8.1.tgz#7c4d3e9ed33aa703b64b28d7f9d194768be5064d" - integrity sha512-10DE9ONho06QORKAaCBpPiFCdW+tZJuY/84tyypGtl6r+/C7Asq0dhqbRZURuUlLQtZxxDvT8eoj8cGW0ha6Bg== + version "8.10.1" + resolved "https://registry.npmjs.org/ts-node/-/ts-node-8.10.1.tgz#77da0366ff8afbe733596361d2df9a60fc9c9bd3" + integrity sha512-bdNz1L4ekHiJul6SHtZWs1ujEKERJnHs4HxN7rjTyyVOFf3HaJ6sLqe6aPG62XTzAB/63pKRh5jTSWL0D7bsvw== dependencies: arg "^4.1.0" diff "^4.0.1" make-error "^1.1.1" - source-map-support "^0.5.6" + source-map-support "^0.5.17" yn "3.1.1" ts-pnp@^1.1.2: