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 }) => (
-
-
-
- Circle CI
-
- }
- >
-
-
-
- {children}
-
-);
+export const Layout: React.FC = ({ children }) => {
+ return (
+
+
+
+ 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 && (
- }
- >
+ }>
Settings
)}
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.
+
+
+ >
+ );
+};
+
+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: