diff --git a/app-config.yaml b/app-config.yaml index da2f5f2d11..0e8fa7a6d2 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -12,7 +12,16 @@ backend: # See README.md in the proxy-backend plugin for information on the configuration format proxy: - '/circleci/api': https://circleci.com/api/v1.1 + '/circleci/api': + target: https://circleci.com/api/v1.1 + changeOrigin: true + pathRewrite: + '^/proxy/circleci/api/': '/' + headers: + Circle-Token: + $secret: + env: CIRCLECI_AUTH_TOKEN + '/jenkins/api': target: http://localhost:8080 headers: diff --git a/docs/architecture-decisions/adr002-default-catalog-file-format.md b/docs/architecture-decisions/adr002-default-catalog-file-format.md index c1cb719bf5..af22d33020 100644 --- a/docs/architecture-decisions/adr002-default-catalog-file-format.md +++ b/docs/architecture-decisions/adr002-default-catalog-file-format.md @@ -68,7 +68,7 @@ metadata: lifecycle: production example.com/service-discovery-name: frobsawesome annotations: - circleci.com/project-slug: gh/example-org/frobs-awesome + circleci.com/project-slug: github/example-org/frobs-awesome spec: type: service ``` diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index e90792c628..fa4b8c23da 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -34,7 +34,7 @@ software catalog API. "annotations": { "backstage.io/managed-by-location": "file:/tmp/component-info.yaml", "example.com/service-discovery": "artistweb", - "circleci.com/project-slug": "gh/example-org/artist-website" + "circleci.com/project-slug": "github/example-org/artist-website" }, "description": "The place to be, for great artists", "etag": "ZjU2MWRkZWUtMmMxZS00YTZiLWFmMWMtOTE1NGNiZDdlYzNk", @@ -66,7 +66,7 @@ metadata: system: public-websites annotations: example.com/service-discovery: artistweb - circleci.com/project-slug: gh/example-org/artist-website + circleci.com/project-slug: github/example-org/artist-website tags: - java spec: diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index ae0d24fa5e..e91504d619 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -19,7 +19,6 @@ import PropTypes from 'prop-types'; import { Link, makeStyles } from '@material-ui/core'; import HomeIcon from '@material-ui/icons/Home'; import ExtensionIcon from '@material-ui/icons/Extension'; -import BuildIcon from '@material-ui/icons/BuildRounded'; import RuleIcon from '@material-ui/icons/AssignmentTurnedIn'; import MapIcon from '@material-ui/icons/MyLocation'; import LibraryBooks from '@material-ui/icons/LibraryBooks'; @@ -97,7 +96,6 @@ const Root: FC<{}> = ({ children }) => ( - { + // This component is just an example of how you can implement your company's logic in entity page. + // You can for example enforce that all components of type 'service' should use GitHubActions + switch (true) { + case isGitHubActionsAvailable(entity): + return ; + case isCircleCIAvailable(entity): + return ; + default: + return ( + + No CI/CD is available for this entity. Check corresponding + annotations! + + ); + } +}; const OverviewContent = ({ entity }: { entity: Entity }) => ( @@ -44,7 +70,7 @@ const ServiceEntityPage = ({ entity }: { entity: Entity }) => ( } + element={} /> ( } + element={} /> { + // This component is just an example of how you can implement your company's logic in entity page. + // You can for example enforce that all components of type 'service' should use GitHubActions + switch (true) { + case isGitHubActionsAvailable(entity): + return ; + case isCircleCIAvailable(entity): + return ; + default: + return ( + + No CI/CD is available for this entity. Check corresponding + annotations! + + ); + } +}; const OverviewContent = ({ entity }: { entity: Entity }) => ( - + + + + + ); const ServiceEntityPage = ({ entity }: { entity: Entity }) => ( @@ -39,7 +70,7 @@ const ServiceEntityPage = ({ entity }: { entity: Entity }) => ( } + element={} /> ( } + element={} /> - - ## Setup @@ -35,8 +33,39 @@ export default builder.build() as ApiHolder; export { plugin as Circleci } from '@backstage/plugin-circleci'; ``` -3. Run app with `yarn start` and navigate to `/circleci/settings` -4. Enter project settings and **project** token, acquired according to [https://circleci.com/docs/2.0/managing-api-tokens/](https://circleci.com/docs/2.0/managing-api-tokens/) +3. Register the plugin router: + +```jsx +// packages/app/src/components/catalog/EntityPage.tsx + +import { Router as CircleCIRouter } from '@backstage/plugin-circleci'; + +// Then somewhere inside +} +/>; +``` + +4. Add proxy config: + +``` +// app-config.yaml +proxy: + '/circleci/api': + target: https://circleci.com/api/v1.1 + changeOrigin: true + pathRewrite: + '^/proxy/circleci/api/': '/' + headers: + Circle-Token: + $secret: + env: CIRCLECI_AUTH_TOKEN +``` + +5. Get and provide `CIRCLECI_AUTH_TOKEN` as env variable (https://circleci.com/docs/api/#add-an-api-token) +6. Add `circleci.com/project-slug` annotation to your component-info.yaml file in format // (https://backstage.io/docs/architecture-decisions/adrs-adr002#format) ## Features @@ -50,3 +79,4 @@ export { plugin as Circleci } from '@backstage/plugin-circleci'; ## Limitations - CircleCI has pretty strict rate limits per token, be careful with opened tabs +- CircelCI doesn't provide a way to auth by 3rd party (e.g. GitHub) token, nor by calling their OAuth endpoints, which currently stands in the way of better auth integration with Backstage (https://discuss.circleci.com/t/circleci-api-authorization-with-github-token/5356) diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index b5824d3869..f86563ea7a 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -22,6 +22,8 @@ }, "dependencies": { "@backstage/core": "^0.1.1-alpha.21", + "@backstage/catalog-model": "^0.1.1-alpha.21", + "@backstage/plugin-catalog": "^0.1.1-alpha.21", "@backstage/theme": "^0.1.1-alpha.21", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", diff --git a/plugins/circleci/src/api/index.ts b/plugins/circleci/src/api/index.ts index 64a33e4c13..da4c811812 100644 --- a/plugins/circleci/src/api/index.ts +++ b/plugins/circleci/src/api/index.ts @@ -42,8 +42,8 @@ export class CircleCIApi { this.apiUrl = apiUrl; } - async retry(buildNumber: number, options: CircleCIOptions) { - return postBuildActions(options.token, buildNumber, BuildAction.RETRY, { + async retry(buildNumber: number, options: Partial) { + return postBuildActions('', buildNumber, BuildAction.RETRY, { circleHost: this.apiUrl, ...options.vcs, }); @@ -51,9 +51,9 @@ export class CircleCIApi { async getBuilds( { limit = 10, offset = 0 }: { limit: number; offset: number }, - options: CircleCIOptions, + options: Partial, ) { - return getBuildSummaries(options.token, { + return getBuildSummaries('', { options: { limit, offset, @@ -64,12 +64,12 @@ export class CircleCIApi { }); } - async getUser(options: CircleCIOptions) { - return getMe(options.token, { circleHost: this.apiUrl, ...options }); + async getUser(options: Partial) { + return getMe('', { circleHost: this.apiUrl, ...options }); } - async getBuild(buildNumber: number, options: CircleCIOptions) { - return getFullBuild(options.token, buildNumber, { + async getBuild(buildNumber: number, options: Partial) { + return getFullBuild('', buildNumber, { circleHost: this.apiUrl, ...options.vcs, }); diff --git a/plugins/circleci/src/assets/screenshot-1.png b/plugins/circleci/src/assets/screenshot-1.png index 2e3f1f420b..db99230a65 100644 Binary files a/plugins/circleci/src/assets/screenshot-1.png and b/plugins/circleci/src/assets/screenshot-1.png differ diff --git a/plugins/circleci/src/assets/screenshot-2.png b/plugins/circleci/src/assets/screenshot-2.png index 4e97cbcf8e..4f9ddcaec6 100644 Binary files a/plugins/circleci/src/assets/screenshot-2.png and b/plugins/circleci/src/assets/screenshot-2.png differ diff --git a/plugins/circleci/src/assets/screenshot-3.png b/plugins/circleci/src/assets/screenshot-3.png deleted file mode 100644 index ac20d58246..0000000000 Binary files a/plugins/circleci/src/assets/screenshot-3.png and /dev/null differ diff --git a/plugins/circleci/src/assets/screenshot-4.png b/plugins/circleci/src/assets/screenshot-4.png deleted file mode 100644 index 2d4ab5fe77..0000000000 Binary files a/plugins/circleci/src/assets/screenshot-4.png and /dev/null differ diff --git a/plugins/circleci/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx b/plugins/circleci/src/components/BuildWithStepsPage/BuildWithStepsPage.tsx similarity index 80% rename from plugins/circleci/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx rename to plugins/circleci/src/components/BuildWithStepsPage/BuildWithStepsPage.tsx index d0e0c655c3..30994f5ce3 100644 --- a/plugins/circleci/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx +++ b/plugins/circleci/src/components/BuildWithStepsPage/BuildWithStepsPage.tsx @@ -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 }) => ( #{build?.build_num} - {build?.subject} @@ -94,56 +96,13 @@ const pickClassName = ( return classes.neutral; }; -const Page = () => ( - - - - - - - - -); - -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 ( - <> - - - - - } - cardClassName={classes.cardContent} - > - {loading ? : } - - - - - ); -}; - const BuildsList: FC<{ build?: BuildWithSteps }> = ({ build }) => ( {build && build.steps && build.steps.map( ({ name, actions }: { name: string; actions: BuildStepAction[] }) => ( - + ), )} @@ -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 ( + <> + + All builds + Build details + + + + } + cardClassName={classes.cardContent} + > + {loading ? : } + + + + + ); +}; diff --git a/plugins/circleci/src/components/PluginHeader/index.ts b/plugins/circleci/src/components/BuildWithStepsPage/index.ts similarity index 90% rename from plugins/circleci/src/components/PluginHeader/index.ts rename to plugins/circleci/src/components/BuildWithStepsPage/index.ts index 4de972f6f2..c5627bda1c 100644 --- a/plugins/circleci/src/components/PluginHeader/index.ts +++ b/plugins/circleci/src/components/BuildWithStepsPage/index.ts @@ -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'; diff --git a/plugins/circleci/src/pages/BuildWithStepsPage/lib/ActionOutput/ActionOutput.tsx b/plugins/circleci/src/components/BuildWithStepsPage/lib/ActionOutput/ActionOutput.tsx similarity index 100% rename from plugins/circleci/src/pages/BuildWithStepsPage/lib/ActionOutput/ActionOutput.tsx rename to plugins/circleci/src/components/BuildWithStepsPage/lib/ActionOutput/ActionOutput.tsx diff --git a/plugins/circleci/src/pages/BuildWithStepsPage/lib/ActionOutput/index.ts b/plugins/circleci/src/components/BuildWithStepsPage/lib/ActionOutput/index.ts similarity index 100% rename from plugins/circleci/src/pages/BuildWithStepsPage/lib/ActionOutput/index.ts rename to plugins/circleci/src/components/BuildWithStepsPage/lib/ActionOutput/index.ts diff --git a/plugins/circleci/src/pages/BuildWithStepsPage/index.ts b/plugins/circleci/src/components/BuildsPage/BuildsPage.tsx similarity index 70% rename from plugins/circleci/src/pages/BuildWithStepsPage/index.ts rename to plugins/circleci/src/components/BuildsPage/BuildsPage.tsx index fddff7088c..d8c38de481 100644 --- a/plugins/circleci/src/pages/BuildWithStepsPage/index.ts +++ b/plugins/circleci/src/components/BuildsPage/BuildsPage.tsx @@ -13,7 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { - default as DetailedViewPage, - BuildWithSteps, -} from './BuildWithStepsPage'; +import React from 'react'; +import { Builds } from './lib/Builds'; +import { Grid } from '@material-ui/core'; + +export const BuildsPage = () => ( + + + + + +); diff --git a/plugins/circleci/src/components/Layout/index.ts b/plugins/circleci/src/components/BuildsPage/index.ts similarity index 93% rename from plugins/circleci/src/components/Layout/index.ts rename to plugins/circleci/src/components/BuildsPage/index.ts index 236fc98851..f9543ed0a8 100644 --- a/plugins/circleci/src/components/Layout/index.ts +++ b/plugins/circleci/src/components/BuildsPage/index.ts @@ -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'; diff --git a/plugins/circleci/src/pages/BuildsPage/lib/Builds/Builds.tsx b/plugins/circleci/src/components/BuildsPage/lib/Builds/Builds.tsx similarity index 100% rename from plugins/circleci/src/pages/BuildsPage/lib/Builds/Builds.tsx rename to plugins/circleci/src/components/BuildsPage/lib/Builds/Builds.tsx diff --git a/plugins/circleci/src/pages/BuildsPage/lib/Builds/index.ts b/plugins/circleci/src/components/BuildsPage/lib/Builds/index.ts similarity index 100% rename from plugins/circleci/src/pages/BuildsPage/lib/Builds/index.ts rename to plugins/circleci/src/components/BuildsPage/lib/Builds/index.ts diff --git a/plugins/circleci/src/pages/BuildsPage/lib/CITable/CITable.tsx b/plugins/circleci/src/components/BuildsPage/lib/CITable/CITable.tsx similarity index 93% rename from plugins/circleci/src/pages/BuildsPage/lib/CITable/CITable.tsx rename to plugins/circleci/src/components/BuildsPage/lib/CITable/CITable.tsx index b349dcfcbe..8e693429c2 100644 --- a/plugins/circleci/src/pages/BuildsPage/lib/CITable/CITable.tsx +++ b/plugins/circleci/src/components/BuildsPage/lib/CITable/CITable.tsx @@ -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) => ( - + {row.buildName} ), diff --git a/plugins/circleci/src/pages/BuildsPage/lib/CITable/index.ts b/plugins/circleci/src/components/BuildsPage/lib/CITable/index.ts similarity index 100% rename from plugins/circleci/src/pages/BuildsPage/lib/CITable/index.ts rename to plugins/circleci/src/components/BuildsPage/lib/CITable/index.ts diff --git a/plugins/circleci/src/components/CircleCIWidget.tsx b/plugins/circleci/src/components/CircleCIWidget.tsx deleted file mode 100644 index 767eb6d5c7..0000000000 --- a/plugins/circleci/src/components/CircleCIWidget.tsx +++ /dev/null @@ -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 = () => ( - - - <> - - } /> - } /> - - - - - -); diff --git a/plugins/circleci/src/components/Layout/Layout.tsx b/plugins/circleci/src/components/Layout/Layout.tsx deleted file mode 100644 index 09479e6ef0..0000000000 --- a/plugins/circleci/src/components/Layout/Layout.tsx +++ /dev/null @@ -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 ( - -
- - -
- {children} -
- ); -}; diff --git a/plugins/circleci/src/components/PluginHeader/PluginHeader.tsx b/plugins/circleci/src/components/PluginHeader/PluginHeader.tsx deleted file mode 100644 index 9f94cff480..0000000000 --- a/plugins/circleci/src/components/PluginHeader/PluginHeader.tsx +++ /dev/null @@ -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 = ({ title = 'CircleCI' }) => { - const [, { showSettings }] = useSettings(); - const location = useLocation(); - const notRoot = !location.pathname.match(/\/circleci\/?$/); - const isSettingsPage = location.pathname.match(/\/circleci\/settings\/?/); - return ( - ( - - {notRoot && ( - - - - )} - {title} - - )} - > - {!isSettingsPage && ( - - )} - - This plugin allows you to view and interact with your builds within the - Circle CI environment. - - - ); -}; diff --git a/plugins/circleci/src/components/Router.tsx b/plugins/circleci/src/components/Router.tsx new file mode 100644 index 0000000000..282eac9111 --- /dev/null +++ b/plugins/circleci/src/components/Router.tsx @@ -0,0 +1,43 @@ +/* + * 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 { Routes, Route } from 'react-router'; +import { circleCIRouteRef, circleCIBuildRouteRef } from '../route-refs'; +import { BuildWithStepsPage } from './BuildWithStepsPage/'; +import { BuildsPage } from './BuildsPage'; +import { CIRCLECI_ANNOTATION } from '../constants'; +import { Entity } from '@backstage/catalog-model'; +import { WarningPanel } from '@backstage/core'; + +export const isPluginApplicableToEntity = (entity: Entity) => + Boolean(entity.metadata.annotations?.[CIRCLECI_ANNOTATION]) && + entity.metadata.annotations?.[CIRCLECI_ANNOTATION] !== ''; + +export const Router = ({ entity }: { entity: Entity }) => + !isPluginApplicableToEntity(entity) ? ( + +
{CIRCLECI_ANNOTATION}
annotation is missing on the entity. +
+ ) : ( + + } /> + } + /> + + ); diff --git a/plugins/circleci/src/components/Settings/Settings.tsx b/plugins/circleci/src/components/Settings/Settings.tsx deleted file mode 100644 index b3b62ce930..0000000000 --- a/plugins/circleci/src/components/Settings/Settings.tsx +++ /dev/null @@ -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 ( - <> - 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/components/Settings/index.ts b/plugins/circleci/src/constants.ts similarity index 90% rename from plugins/circleci/src/components/Settings/index.ts rename to plugins/circleci/src/constants.ts index c04ded6dea..8c96db93e2 100644 --- a/plugins/circleci/src/components/Settings/index.ts +++ b/plugins/circleci/src/constants.ts @@ -13,4 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { default as Settings } from './Settings'; + +export const CIRCLECI_ANNOTATION = 'circleci.com/project-slug'; diff --git a/plugins/circleci/src/index.ts b/plugins/circleci/src/index.ts index e2e6c4fa69..c6cb140208 100644 --- a/plugins/circleci/src/index.ts +++ b/plugins/circleci/src/index.ts @@ -17,4 +17,5 @@ export { plugin } from './plugin'; export * from './api'; export * from './route-refs'; -export { CircleCIWidget } from './components/CircleCIWidget'; +export { Router, isPluginApplicableToEntity } from './components/Router'; +export { CIRCLECI_ANNOTATION } from './constants'; diff --git a/plugins/circleci/src/pages/BuildsPage/BuildsPage.tsx b/plugins/circleci/src/pages/BuildsPage/BuildsPage.tsx deleted file mode 100644 index 6e7fc44249..0000000000 --- a/plugins/circleci/src/pages/BuildsPage/BuildsPage.tsx +++ /dev/null @@ -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<{}> = () => ( - - - - - - - - -); - -const Builds = () => ( - <> - - - - - - - -); - -export default BuildsPage; -export { Builds }; diff --git a/plugins/circleci/src/pages/BuildsPage/index.ts b/plugins/circleci/src/pages/BuildsPage/index.ts deleted file mode 100644 index 72b46d6bc9..0000000000 --- a/plugins/circleci/src/pages/BuildsPage/index.ts +++ /dev/null @@ -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'; diff --git a/plugins/circleci/src/plugin.ts b/plugins/circleci/src/plugin.ts index 86be898366..f966caa383 100644 --- a/plugins/circleci/src/plugin.ts +++ b/plugins/circleci/src/plugin.ts @@ -13,10 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { createPlugin, createApiFactory, configApiRef } from '@backstage/core'; -import { circleCIRouteRef, circleCIBuildRouteRef } from './route-refs'; -import BuildsPage from './pages/BuildsPage/BuildsPage'; -import BuildWithStepsPage from './pages/BuildWithStepsPage/BuildWithStepsPage'; import { circleCIApiRef, CircleCIApi } from './api'; export const plugin = createPlugin({ @@ -31,8 +29,4 @@ export const plugin = createPlugin({ ), }), ], - register({ router }) { - router.addRoute(circleCIRouteRef, BuildsPage); - router.addRoute(circleCIBuildRouteRef, BuildWithStepsPage); - }, }); diff --git a/plugins/circleci/src/route-refs.tsx b/plugins/circleci/src/route-refs.tsx index 581f035c1d..d6bad363ed 100644 --- a/plugins/circleci/src/route-refs.tsx +++ b/plugins/circleci/src/route-refs.tsx @@ -33,11 +33,11 @@ const CircleCIIcon: FC = props => ( export const circleCIRouteRef = createRouteRef({ icon: CircleCIIcon, - path: '/circleci', + path: '', title: 'CircleCI | All builds', }); export const circleCIBuildRouteRef = createRouteRef({ - path: '/circleci/build/:buildId', + path: ':buildId', title: 'CircleCI | Build info', }); diff --git a/plugins/circleci/src/state/AppState.tsx b/plugins/circleci/src/state/AppState.tsx deleted file mode 100644 index 2ee00362ee..0000000000 --- a/plugins/circleci/src/state/AppState.tsx +++ /dev/null @@ -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]>( - [] as any, -); -export const STORAGE_KEY = `${circleCIApiRef.id}.settings`; - -const initialState: State = { - owner: '', - repo: '', - token: '', - showSettings: false, -}; - -const reducer: Reducer = (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 ( - - <>{children} - - ); -}; diff --git a/plugins/circleci/src/state/index.ts b/plugins/circleci/src/state/index.ts index 2321103eb4..d21a380c2a 100644 --- a/plugins/circleci/src/state/index.ts +++ b/plugins/circleci/src/state/index.ts @@ -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'; diff --git a/plugins/circleci/src/state/types.ts b/plugins/circleci/src/state/types.ts deleted file mode 100644 index 41b3577082..0000000000 --- a/plugins/circleci/src/state/types.ts +++ /dev/null @@ -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; diff --git a/plugins/circleci/src/state/useBuildWithSteps.ts b/plugins/circleci/src/state/useBuildWithSteps.ts index 8fa7fa896d..c169b8fe63 100644 --- a/plugins/circleci/src/state/useBuildWithSteps.ts +++ b/plugins/circleci/src/state/useBuildWithSteps.ts @@ -14,31 +14,35 @@ * limitations under the License. */ import { errorApiRef, useApi } from '@backstage/core'; -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import { useAsyncRetry } from 'react-use'; -import { circleCIApiRef, GitType } from '../api/index'; +import { circleCIApiRef } from '../api/index'; import { useAsyncPolling } from './useAsyncPolling'; -import { useSettings } from './useSettings'; +import { useProjectSlugFromEntity, mapVcsType } from './useBuilds'; const INTERVAL_AMOUNT = 1500; export function useBuildWithSteps(buildId: number) { - const [{ token, repo, owner }] = useSettings(); + const { vcs, repo, owner } = useProjectSlugFromEntity(); const api = useApi(circleCIApiRef); const errorApi = useApi(errorApiRef); + const vcsOption = useMemo( + () => ({ + owner: owner, + repo: repo, + type: mapVcsType(vcs), + }), + [owner, repo, vcs], + ); + const getBuildWithSteps = useCallback(async () => { - if (owner === '' || repo === '' || token === '') { + if (owner === '' || repo === '' || vcs === '') { return Promise.reject('No credentials provided'); } try { const options = { - token: token, - vcs: { - owner: owner, - repo: repo, - type: GitType.GITHUB, - }, + vcs: vcsOption, }; const build = await api.getBuild(buildId, options); return Promise.resolve(build); @@ -46,17 +50,12 @@ export function useBuildWithSteps(buildId: number) { errorApi.post(e); return Promise.reject(e); } - }, [token, owner, repo, buildId, api, errorApi]); + }, [vcsOption, buildId, api, errorApi]); // eslint-disable-line react-hooks/exhaustive-deps const restartBuild = async () => { try { await api.retry(buildId, { - token: token, - vcs: { - owner: owner, - repo: repo, - type: GitType.GITHUB, - }, + vcs: vcsOption, }); } catch (e) { errorApi.post(e); diff --git a/plugins/circleci/src/state/useBuilds.ts b/plugins/circleci/src/state/useBuilds.ts index 282ef68f12..2a4aed7fd6 100644 --- a/plugins/circleci/src/state/useBuilds.ts +++ b/plugins/circleci/src/state/useBuilds.ts @@ -18,8 +18,9 @@ import { BuildSummary, GitType } from 'circleci-api'; import { useCallback, useEffect, useState } from 'react'; import { useAsyncRetry } from 'react-use'; import { circleCIApiRef } from '../api/index'; -import type { CITableBuildInfo } from '../pages/BuildsPage/lib/CITable'; -import { useSettings } from './useSettings'; +import type { CITableBuildInfo } from '../components/BuildsPage/lib/CITable'; +import { useEntity } from '@backstage/plugin-catalog'; +import { CIRCLECI_ANNOTATION } from '../constants'; const makeReadableStatus = (status: string | undefined) => { if (!status) return ''; @@ -68,8 +69,25 @@ export const transform = ( }); }; +export const useProjectSlugFromEntity = () => { + const { entity } = useEntity(); + const [vcs, owner, repo] = ( + entity.metadata.annotations?.[CIRCLECI_ANNOTATION] ?? '' + ).split('/'); + return { vcs, owner, repo }; +}; + +export function mapVcsType(vcs: string): GitType { + switch (vcs) { + case 'github': + return GitType.GITHUB; + default: + return GitType.BITBUCKET; + } +} + export function useBuilds() { - const [{ repo, owner, token }] = useSettings(); + const { repo, owner, vcs } = useProjectSlugFromEntity(); const api = useApi(circleCIApiRef); const errorApi = useApi(errorApiRef); @@ -79,7 +97,7 @@ export function useBuilds() { const getBuilds = useCallback( async ({ limit, offset }: { limit: number; offset: number }) => { - if (owner === '' || repo === '' || token === '') { + if (owner === '' || repo === '' || vcs === '') { return Promise.reject('No credentials provided'); } @@ -87,11 +105,10 @@ export function useBuilds() { return await api.getBuilds( { limit, offset }, { - token: token, vcs: { owner: owner, repo: repo, - type: GitType.GITHUB, + type: mapVcsType(vcs), }, }, ); @@ -100,13 +117,12 @@ export function useBuilds() { return Promise.reject(e); } }, - [repo, token, owner, api, errorApi], + [repo, owner, vcs, api, errorApi], ); const restartBuild = async (buildId: number) => { try { await api.retry(buildId, { - token: token, vcs: { owner: owner, repo: repo, diff --git a/plugins/circleci/src/state/useSettings.ts b/plugins/circleci/src/state/useSettings.ts deleted file mode 100644 index 3cc58a65bd..0000000000 --- a/plugins/circleci/src/state/useSettings.ts +++ /dev/null @@ -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; -} diff --git a/plugins/github-actions/src/components/Router.tsx b/plugins/github-actions/src/components/Router.tsx index d0745f67d4..7d340fb8b8 100644 --- a/plugins/github-actions/src/components/Router.tsx +++ b/plugins/github-actions/src/components/Router.tsx @@ -22,7 +22,7 @@ import { WorkflowRunsTable } from './WorkflowRunsTable'; import { GITHUB_ACTIONS_ANNOTATION } from './useProjectName'; import { WarningPanel } from '@backstage/core'; -const isPluginApplicableToEntity = (entity: Entity) => +export const isPluginApplicableToEntity = (entity: Entity) => Boolean(entity.metadata.annotations?.[GITHUB_ACTIONS_ANNOTATION]) && entity.metadata.annotations?.[GITHUB_ACTIONS_ANNOTATION] !== ''; @@ -30,8 +30,8 @@ export const Router = ({ entity }: { entity: Entity }) => // TODO(shmidt-i): move warning to a separate standardized component !isPluginApplicableToEntity(entity) ? ( - `entity.metadata.annotations[' - {GITHUB_ACTIONS_ANNOTATION}']` key is missing on the entity.{' '} +
{GITHUB_ACTIONS_ANNOTATION}
annotation is missing on the + entity.
) : ( diff --git a/plugins/github-actions/src/index.ts b/plugins/github-actions/src/index.ts index 17c1fa2dd7..24fe6fc90d 100644 --- a/plugins/github-actions/src/index.ts +++ b/plugins/github-actions/src/index.ts @@ -16,6 +16,6 @@ export { plugin } from './plugin'; export * from './api'; -export { Router } from './components/Router'; +export { Router, isPluginApplicableToEntity } from './components/Router'; export * from './components/Cards'; export { GITHUB_ACTIONS_ANNOTATION } from './components/useProjectName'; diff --git a/plugins/rollbar/src/components/Router.tsx b/plugins/rollbar/src/components/Router.tsx index c765c14f14..bcf325f281 100644 --- a/plugins/rollbar/src/components/Router.tsx +++ b/plugins/rollbar/src/components/Router.tsx @@ -32,10 +32,7 @@ type Props = { export const Router = ({ entity }: Props) => !isPluginApplicableToEntity(entity) ? ( -
-        entity.metadata.annotations['{ROLLBAR_ANNOTATION}']` key is missing on
-        the entity.
-      
+
{ROLLBAR_ANNOTATION}
annotation is missing on the entity.
) : (