diff --git a/docs/architecture-decisions/adr001-add-adr-log.md b/docs/architecture-decisions/adr001-add-adr-log.md
index 453905ab7a..16783367ab 100644
--- a/docs/architecture-decisions/adr001-add-adr-log.md
+++ b/docs/architecture-decisions/adr001-add-adr-log.md
@@ -1,7 +1,6 @@
---
id: adrs-adr001
title: ADR001: Architecture Decision Record (ADR) log
-sidebar_label: ADR001
description: Architecture Decision Record (ADR) logs as a reference point for the team
---
diff --git a/docs/architecture-decisions/adr002-default-catalog-file-format.md b/docs/architecture-decisions/adr002-default-catalog-file-format.md
index da98aafaed..f4fd3a1158 100644
--- a/docs/architecture-decisions/adr002-default-catalog-file-format.md
+++ b/docs/architecture-decisions/adr002-default-catalog-file-format.md
@@ -1,7 +1,6 @@
---
id: adrs-adr002
title: ADR002: Default Software Catalog File Format
-sidebar_label: ADR002
description: Architecture Decision Record (ADR) log on Default Software Catalog File Format
---
diff --git a/docs/architecture-decisions/adr003-avoid-default-exports.md b/docs/architecture-decisions/adr003-avoid-default-exports.md
index 9d1f9c83a4..8634a19632 100644
--- a/docs/architecture-decisions/adr003-avoid-default-exports.md
+++ b/docs/architecture-decisions/adr003-avoid-default-exports.md
@@ -1,7 +1,6 @@
---
id: adrs-adr003
title: ADR003: Avoid Default Exports and Prefer Named Exports
-sidebar_label: ADR003
description: Architecture Decision Record (ADR) log on Avoid Default Exports and Prefer Named Exports
---
diff --git a/docs/architecture-decisions/adr004-module-export-structure.md b/docs/architecture-decisions/adr004-module-export-structure.md
index 3dbda3f8b9..12408abac1 100644
--- a/docs/architecture-decisions/adr004-module-export-structure.md
+++ b/docs/architecture-decisions/adr004-module-export-structure.md
@@ -1,7 +1,6 @@
---
id: adrs-adr004
title: ADR004: Module Export Structure
-sidebar_label: ADR004
description: Architecture Decision Record (ADR) log on Module Export Structure
---
diff --git a/docs/architecture-decisions/adr005-catalog-core-entities.md b/docs/architecture-decisions/adr005-catalog-core-entities.md
index ae615f647d..f91698c5ff 100644
--- a/docs/architecture-decisions/adr005-catalog-core-entities.md
+++ b/docs/architecture-decisions/adr005-catalog-core-entities.md
@@ -1,7 +1,6 @@
---
id: adrs-adr005
title: ADR005: Catalog Core Entities
-sidebar_label: ADR005
description: Architecture Decision Record (ADR) log on Catalog Core Entities
---
diff --git a/docs/architecture-decisions/adr006-avoid-react-fc.md b/docs/architecture-decisions/adr006-avoid-react-fc.md
index 696cdcf537..bea36712d7 100644
--- a/docs/architecture-decisions/adr006-avoid-react-fc.md
+++ b/docs/architecture-decisions/adr006-avoid-react-fc.md
@@ -1,7 +1,6 @@
---
id: adrs-adr006
title: ADR006: Avoid React.FC and React.SFC
-sidebar_label: ADR006
description: Architecture Decision Record (ADR) log on Avoid React.FC and React.SFC
---
diff --git a/docs/architecture-decisions/adr007-use-msw-to-mock-service-requests.md b/docs/architecture-decisions/adr007-use-msw-to-mock-service-requests.md
index d91af19068..6b18f67d40 100644
--- a/docs/architecture-decisions/adr007-use-msw-to-mock-service-requests.md
+++ b/docs/architecture-decisions/adr007-use-msw-to-mock-service-requests.md
@@ -1,7 +1,6 @@
---
id: adrs-adr007
title: ADR007: Use MSW to mock http requests
-sidebar_label: ADR007
description: Architecture Decision Record (ADR) log on Use MSW to mock http requests
---
diff --git a/docs/architecture-decisions/adr008-default-catalog-file-name.md b/docs/architecture-decisions/adr008-default-catalog-file-name.md
index 01afefd690..c57d10c0b1 100644
--- a/docs/architecture-decisions/adr008-default-catalog-file-name.md
+++ b/docs/architecture-decisions/adr008-default-catalog-file-name.md
@@ -1,7 +1,6 @@
---
id: adrs-adr008
title: ADR008: Default Catalog File Name
-sidebar_label: ADR008
description: Architecture Decision Record (ADR) log on Default Catalog File Name
---
diff --git a/docs/tutorials/quickstart-app-auth.md b/docs/tutorials/quickstart-app-auth.md
new file mode 100644
index 0000000000..fea1d7085a
--- /dev/null
+++ b/docs/tutorials/quickstart-app-auth.md
@@ -0,0 +1,199 @@
+---
+id: quickstart-app-auth
+title: Monorepo App Setup With Authentication
+---
+
+###### September 15th 2020 - @backstage/create-app - v0.1.1-alpha.21
+
+
+
+> This document takes you through setting up a backstage app that runs in your
+> own environment. It starts with a skeleton install and verifying of the
+> monorepo's functionality. Next, GitHub authentication is added and tested.
+>
+> This document assumes you have NodeJS 12 active along with Yarn. Please note,
+> that at the time of this writing, the current version is 0.1.1-alpha.21. This
+> guide can still be used with future versions, just, verify as you go. If you
+> run into issues, you can compare your setup with mine here >
+> [simple-backstage-app](https://github.com/johnson-jesse/simple-backstage-app).
+
+# The Skeleton Application
+
+From the terminal:
+
+1. Create a (monorepo) application: `npx @backstage/create-app`
+1. Enter an `id` for your new app like `mybiz-backstage` I went with
+ `simple-backstage-app`
+1. Choose `SQLite` as your database. This is the quickest way to get started as
+ PostgreSQL requires additional setup not covered here.
+1. Start your backend: `yarn --cwd packages/backend start`
+
+```zsh
+# You should see positive verbiage in your terminal output
+2020-09-11T22:20:26.712Z backstage info Listening on :7000
+```
+
+5. Finally, start the frontend. Open a new terminal window and from the root of
+ your project, run: `yarn start`
+
+```zsh
+# You should see positive verbiage in your terminal output
+ℹ 「wds」: Project is running at http://localhost:3000/
+```
+
+Once the app compiles, a browser window should have popped with your stand alone
+application loaded at `localhost:3000`. This could take a couple minutes.
+
+```zsh
+# You should see positive verbiage in your terminal output
+ℹℹ 「wdm」: Compiled successfully.
+```
+
+Since there is no auth currently configured, you are automatically entered as a
+guest. Let's fix that now and add auth.
+
+# The Auth Configuration
+
+1. Open `app-config.yaml` and change it as follows
+
+_from:_
+
+```yaml
+auth:
+ providers: {}
+```
+
+_to:_
+
+```yaml
+auth:
+ providers:
+ github:
+ development:
+ clientId:
+ $secret:
+ env: AUTH_GITHUB_CLIENT_ID
+ clientSecret:
+ $secret:
+ env: AUTH_GITHUB_CLIENT_SECRET
+ ## uncomment the following three lines if using enterprise
+ # enterpriseInstanceUrl:
+ # $secret:
+ # env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL
+```
+
+2. Set environment variables in whatever fashion is easiest for you. I chose to
+ add mine to my `.zshrc` profile.
+
+```zsh
+# For macOS Catalina & Z Shell
+# ------ simple-backstage-app GitHub
+export AUTH_GITHUB_CLIENT_ID=xxx
+export AUTH_GITHUB_CLIENT_SECRET=xxx
+# export AUTH_GITHUB_ENTERPRISE_INSTANCE_URL=https://github.{MY_BIZ}.com
+```
+
+3. And of course I need to source that file.
+
+```zsh
+# Loading the new variables
+% source ~/.zshrc
+
+# Any other currently opened terminals need to be restarted to pick up the new values
+# verify your setup by running env
+% env
+# should output something like
+> ...
+> AUTH_GITHUB_CLIENT_ID=xxx
+> AUTH_GITHUB_CLIENT_SECRET=xxx
+> ...
+```
+
+4. The values to replace `xxx` above come from your oauth app setup.
+
+```
+> Log into http://github.com
+> Navigate to (Settings > Developer Settings > OAuth Apps > New OAuth App)[https://github.com/settings/applications/new]
+> Set Homepage URL = http://localhost:3000
+> Set Callback URL = http://localhost:7000/auth/github
+> Click [Register application]
+> On the next page, copy and paste your new Client ID and Client Secret to the environment variables above, `AUTH_GITHUB_CLIENT_ID` & `AUTH_GITHUB_CLIENT_SECRET`
+> Don't forget to `source` that profile file again if necessary.
+```
+
+5. Open and change _root > packages > app > src >_`App.tsx` as follows
+
+```tsx
+// Add the following imports to the existing list from core
+import { githubAuthApiRef, SignInPage } from '@backstage/core';
+```
+
+6. In the same file, change the createApp function as follows
+
+```tsx
+const app = createApp({
+ apis,
+ plugins: Object.values(plugins),
+ components: {
+ SignInPage: props => {
+ return (
+
+ );
+ },
+ },
+});
+```
+
+6. Open and change _root > packages > app > src >_ `apis.ts` as follows
+
+```ts
+// Add the following imports to the existing list from core
+import { githubAuthApiRef, GithubAuth } from '@backstage/core';
+```
+
+7. In the same file, change the builder block for oauthRequestApiRef as follows
+
+_from:_
+
+```ts
+builder.add(oauthRequestApiRef, new OAuthRequestManager());
+```
+
+_to:_
+
+```ts
+const oauthRequestApi = builder.add(
+ oauthRequestApiRef,
+ new OAuthRequestManager(),
+);
+
+builder.add(
+ githubAuthApiRef,
+ GithubAuth.create({
+ discoveryApi,
+ oauthRequestApi,
+ }),
+);
+```
+
+> Start the backend and frontend as before. When the browser loads, you should
+> be presented with a login page for GitHub. Login as usual with your GitHub
+> account. If this is your first time, you will be asked to authorize and then
+> are redirected to the catalog page if all is well.
+
+# Where to go from here
+
+> You're probably eager to write your first custom plugin. Follow this next
+> tutorial for an in-depth look at a custom GitHub repository browser plugin.
+> [Adding Custom Plugin to Existing Monorepo App](quickstart-app-plugin.md).
diff --git a/docs/tutorials/quickstart-app-plugin.md b/docs/tutorials/quickstart-app-plugin.md
new file mode 100644
index 0000000000..28efb2a007
--- /dev/null
+++ b/docs/tutorials/quickstart-app-plugin.md
@@ -0,0 +1,487 @@
+---
+id: quickstart-app-plugin
+title: Adding Custom Plugin to Existing Monorepo App
+---
+
+###### September 15th 2020 - v0.1.1-alpha.21
+
+
+
+> This document takes you through setting up a new plugin for your existing
+> monorepo with a _GitHub provider already setup_. If you don't have either of
+> those, you can clone
+> [simple-backstage-app](https://github.com/johnson-jesse/simple-backstage-app)
+> which this document builds on.
+>
+> This document does not cover authoring a plugin for sharing with the Backstage
+> community. That will have to be a later discussion.
+>
+> We start with a skeleton plugin install. And after verifying its
+> functionality, extend the Sidebar to make our life easy. Finally, we add
+> custom code to display GitHub repository information.
+>
+> This document assumes you have NodeJS 12 active along with Yarn. Please note,
+> that at the time of this writing, the current version is 0.1.1-alpha.21. This
+> guide can still be used with future versions, just, verify as you go. If you
+> run into issues, you can compare your setup with mine here >
+> [simple-backstage-app-plugin](https://github.com/johnson-jesse/simple-backstage-app-plugin).
+
+# The Skeleton Plugin
+
+1. Start by using the built in creator. From the terminal and root of your
+ project run: `yarn create-plugin`
+1. Enter a plugin ID. I used `github-playground`
+1. When the process finishes, let's start the backend:
+ `yarn --cwd packages/backend start`
+1. If you see errors starting, refer to
+ [Auth Configuration](https://github.com/johnson-jesse/simple-backstage-app/blob/master/README.md#the-auth-configuration)
+ for more information on environment variables.
+1. And now the frontend, from a new terminal window and the root of your
+ project: `yarn start`
+1. As usual, a browser window should popup loading the App.
+1. Now manually navigate to our plugin page from your browser:
+ `http://localhost:3000/github-playground`
+1. You should see successful verbiage for this endpoint,
+ `Welcome to github-playground!`
+
+# The Shortcut
+
+Let's add a shortcut.
+
+1. Open and modify `root: packages > app > src > sidebar.tsx` with the
+ following:
+
+```tsx
+import GitHubIcon from '@material-ui/icons/GitHub';
+...
+
+```
+
+Simple! The App will reload with your changes automatically. You should now see
+a github icon displayed in the sidebar. Clicking that will link to our new
+plugin. And now, the API fun begins.
+
+# The Identity
+
+Our first modification will be to extract information from the Identity API.
+
+1. Start by opening
+ `root: plugins > github-playground > src > components > ExampleComponent > ExampleComponent.tsx`
+1. Add two new imports
+
+```tsx
+// Add identityApiRef to the list of imported from core
+import { identityApiRef } from '@backstage/core';
+import { useApi } from '@backstage/core-api';
+```
+
+3. Adjust the ExampleComponent from inline to block
+
+_from inline:_
+
+```tsx
+const ExampleComponent: FC<{}> = () => ( ... )
+```
+
+_to block:_
+
+```tsx
+const ExampleComponent: FC<{}> = () => {
+
+ return (
+ ...
+ )
+}
+```
+
+4. Now add our hook and const data before the return statement
+
+```tsx
+// our API hook
+const identityApi = useApi(identityApiRef);
+
+// data to use
+const userId = identityApi.getUserId();
+const profile = identityApi.getProfile();
+```
+
+5. Finally, update the InfoCard's jsx to use our new data
+
+```tsx
+
+
+ {`${profile.displayName} | ${profile.email}`}
+
+
+```
+
+If everything is saved, you should see your name, id, and email on the
+github-playground page. Our data accessed is synchronous. So we just grab and
+go.
+
+6. Here is the entire file for reference
+Complete ExampleComponent.tsx
+
+
+```tsx
+import React, { FC } from 'react';
+import { Typography, Grid } from '@material-ui/core';
+import {
+ InfoCard,
+ Header,
+ Page,
+ pageTheme,
+ Content,
+ ContentHeader,
+ HeaderLabel,
+ SupportButton,
+ identityApiRef,
+} from '@backstage/core';
+import { useApi } from '@backstage/core-api';
+import ExampleFetchComponent from '../ExampleFetchComponent';
+
+const ExampleComponent: FC<{}> = () => {
+ const identityApi = useApi(identityApiRef);
+ const userId = identityApi.getUserId();
+ const profile = identityApi.getProfile();
+
+ return (
+
+
+
+
+ A description of your plugin goes here.
+
+
+
+
+
+ {`${profile.displayName} | ${profile.email}`}
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default ExampleComponent;
+```
+
+
+
+
+# The Wipe
+
+The last file we will touch is ExampleFetchComponent. Because of the number of
+changes, let's start by wiping this component clean.
+
+1. Start by opening
+ `root: plugins > github-playground > src > components > ExampleFetchComponent > ExampleFetchComponent.tsx`
+1. Replace everyting in the file with the following:
+
+```tsx
+import React, { FC } from 'react';
+import { useAsync } from 'react-use';
+import Alert from '@material-ui/lab/Alert';
+import {
+ Table,
+ TableColumn,
+ Progress,
+ githubAuthApiRef,
+} from '@backstage/core';
+import { useApi } from '@backstage/core-api';
+import { graphql } from '@octokit/graphql';
+
+const ExampleFetchComponent: FC<{}> = () => {
+ return
Nothing to see yet
;
+};
+
+export default ExampleFetchComponent;
+```
+
+3. Save that and ensure you see no errors. Comment out the unused imports if
+ your linter gets in the way.
+
+###### We will add a lot to this file for the sake of ease. Please don't do this in productional code!
+
+# The Graph Model
+
+GitHub has a graphql API available for interacting. Let's start by adding our
+basic repository query
+
+1. Add the query const statement outside ExampleFetchComponent
+
+```tsx
+const query = `{
+ viewer {
+ repositories(first: 100) {
+ totalCount
+ nodes {
+ name
+ createdAt
+ description
+ diskUsage
+ isFork
+ }
+ pageInfo {
+ endCursor
+ hasNextPage
+ }
+ }
+ }
+}`;
+```
+
+2. Using this structure as a guide, we will break our query into type parts
+3. Add the following outside of ExampleFetchComponent
+
+```tsx
+type Node = {
+ name: string;
+ createdAt: string;
+ description: string;
+ diskUsage: number;
+ isFork: boolean;
+};
+
+type Viewer = {
+ repositories: {
+ totalCount: number;
+ nodes: Node[];
+ pageInfo: {
+ endCursor: string;
+ hasNextPage: boolean;
+ };
+ };
+};
+```
+
+# The Tabel Model
+
+Using Backstage's own component library, let's define a custom table. This
+component will get used if we have data to display.
+
+1. Add the following outside of ExampleFetchComponent
+
+```tsx
+type DenseTableProps = {
+ viewer: Viewer;
+};
+
+export const DenseTable: FC = ({ viewer }) => {
+ const columns: TableColumn[] = [
+ { title: 'Name', field: 'name' },
+ { title: 'Created', field: 'createdAt' },
+ { title: 'Description', field: 'description' },
+ { title: 'Disk Usage', field: 'diskUsage' },
+ { title: 'Fork', field: 'isFork' },
+ ];
+
+ return (
+
+ );
+};
+```
+
+# The Fetch
+
+We're ready to flush out our fetch component
+
+1. Add our api hook inside ExampleFetchComponent
+
+```tsx
+const auth = useApi(githubAuthApiRef);
+```
+
+2. The access token we need to make our GitHub request and the request itself is
+ obtained in an asynchronous manner.
+3. Add the useAsync block inside the ExampleFetchComponent
+
+```tsx
+const { value, loading, error } = useAsync(async (): Promise => {
+ const token = await auth.getAccessToken();
+
+ const gqlEndpoint = graphql.defaults({
+ // Uncomment baseUrl if using enterprise
+ // baseUrl: 'https://github.MY-BIZ.com/api',
+ headers: {
+ authorization: `token ${token}`,
+ },
+ });
+ const { viewer } = await gqlEndpoint(query);
+ return viewer;
+}, []);
+```
+
+4. The resolved data is conventiently destructured with value containing our
+ Viewer type. loading as a boolean, self explainatory. And error which is
+ present only if necessary. So let's use those as the first 3 of 4 multi
+ return statements.
+5. Add the _if return_ blocks below our async block
+
+```tsx
+if (loading) return ;
+if (error) return {error.message} ;
+if (value && value.repositories) return ;
+```
+
+6. The third line here utilizes our custom table accepting our Viewer type.
+7. Finally, we add our _else return_ block to catch any other scenarios.
+
+```tsx
+return (
+
+);
+```
+
+8. After saving that, and given we don't have any errors, you should see a table
+ with basic information on your repositories.
+9. Here is the entire file for reference
+Complete ExampleFetchComponent.tsx
+
+
+```tsx
+import React, { FC } from 'react';
+import { useAsync } from 'react-use';
+import Alert from '@material-ui/lab/Alert';
+import {
+ Table,
+ TableColumn,
+ Progress,
+ githubAuthApiRef,
+} from '@backstage/core';
+import { useApi } from '@backstage/core-api';
+import { graphql } from '@octokit/graphql';
+
+const query = `{
+viewer {
+ repositories(first: 100) {
+ totalCount
+ nodes {
+ name
+ createdAt
+ description
+ diskUsage
+ isFork
+ }
+ pageInfo {
+ endCursor
+ hasNextPage
+ }
+ }
+}
+}`;
+
+type Node = {
+ name: string;
+ createdAt: string;
+ description: string;
+ diskUsage: number;
+ isFork: boolean;
+};
+
+type Viewer = {
+ repositories: {
+ totalCount: number;
+ nodes: Node[];
+ pageInfo: {
+ endCursor: string;
+ hasNextPage: boolean;
+ };
+ };
+};
+
+type DenseTableProps = {
+ viewer: Viewer;
+};
+
+export const DenseTable: FC = ({ viewer }) => {
+ const columns: TableColumn[] = [
+ { title: 'Name', field: 'name' },
+ { title: 'Created', field: 'createdAt' },
+ { title: 'Description', field: 'description' },
+ { title: 'Disk Usage', field: 'diskUsage' },
+ { title: 'Fork', field: 'isFork' },
+ ];
+
+ return (
+
+ );
+};
+
+const ExampleFetchComponent: FC<{}> = () => {
+ const auth = useApi(githubAuthApiRef);
+
+ const { value, loading, error } = useAsync(async (): Promise => {
+ const token = await auth.getAccessToken();
+
+ const gqlEndpoint = graphql.defaults({
+ // Uncomment baseUrl if using enterprise
+ // baseUrl: 'https://github.MY-BIZ.com/api',
+ headers: {
+ authorization: `token ${token}`,
+ },
+ });
+ const { viewer } = await gqlEndpoint(query);
+ return viewer;
+ }, []);
+
+ if (loading) return ;
+ if (error) return {error.message} ;
+ if (value && value.repositories) return ;
+
+ return (
+
+ );
+};
+
+export default ExampleFetchComponent;
+```
+
+
+
+
+10. We finished! If there are no errors, you should see your own GitHub
+ repoistory information displayed in a basic table. If you run into issues,
+ you can compare the repo that backs this documdnt,
+ [simple-backstage-app-plugin](https://github.com/johnson-jesse/simple-backstage-app-plugin)
+
+# Where to go from here
+
+> Break apart ExampleFetchComponent into smaller logical parts contained in
+> their own files. Rename your components to something other than ExampleXxx.
+>
+> You might be real proud of a plugin you develop. Follow this next tutorial for
+> an in-depth look at publishing and including that for the entire Backstage
+> community. [TODO](#).
diff --git a/microsite/pages/en/index.js b/microsite/pages/en/index.js
index f595032bd0..37298ae8e2 100644
--- a/microsite/pages/en/index.js
+++ b/microsite/pages/en/index.js
@@ -462,6 +462,19 @@ class Index extends React.Component {
Contribute
+
+
+
+
+ Backstage is a{' '}
+
+ Cloud Native Computing Foundation
+ {' '}
+ sandbox project
+
+
+
+
);
}
diff --git a/microsite/sidebars.json b/microsite/sidebars.json
index f7aa0630ef..aac524c538 100644
--- a/microsite/sidebars.json
+++ b/microsite/sidebars.json
@@ -143,7 +143,11 @@
"ids": ["api/backend"]
}
],
- "Tutorials": ["tutorials/journey"],
+ "Tutorials": [
+ "tutorials/journey",
+ "tutorials/quickstart-app-auth",
+ "tutorials/quickstart-app-plugin"
+ ],
"Architecture Decision Records (ADRs)": [
"architecture-decisions/adrs-overview",
"architecture-decisions/adrs-adr001",
diff --git a/microsite/static/css/custom.css b/microsite/static/css/custom.css
index 111065a5d5..00a102ba00 100644
--- a/microsite/static/css/custom.css
+++ b/microsite/static/css/custom.css
@@ -1070,3 +1070,14 @@ code {
margin: auto;
}
}
+
+.cncf-block {
+ text-align: center;
+}
+
+.cncf-logo {
+ background: center no-repeat url(../img/cncf-white.svg);
+ width: 100%;
+ height: 100px;
+ margin-bottom: 40px;
+}
diff --git a/microsite/static/img/cncf-color.svg b/microsite/static/img/cncf-color.svg
new file mode 100644
index 0000000000..12f7d3e48a
--- /dev/null
+++ b/microsite/static/img/cncf-color.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/microsite/static/img/cncf-white.svg b/microsite/static/img/cncf-white.svg
new file mode 100644
index 0000000000..d94aaf3249
--- /dev/null
+++ b/microsite/static/img/cncf-white.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx
index e91504d619..03e24cf4ae 100644
--- a/packages/app/src/components/Root/Root.tsx
+++ b/packages/app/src/components/Root/Root.tsx
@@ -35,8 +35,6 @@ import {
SidebarSearchField,
SidebarSpace,
SidebarUserSettings,
- SidebarThemeToggle,
- SidebarPinButton,
DefaultProviderSettings,
} from '@backstage/core';
import { NavLink } from 'react-router-dom';
@@ -103,9 +101,7 @@ const Root: FC<{}> = ({ children }) => (
/>
-
} />
-
{children}
diff --git a/packages/backend/src/plugins/scaffolder.ts b/packages/backend/src/plugins/scaffolder.ts
index c4b7e22142..2793a38459 100644
--- a/packages/backend/src/plugins/scaffolder.ts
+++ b/packages/backend/src/plugins/scaffolder.ts
@@ -55,31 +55,60 @@ export default async function createPlugin({
const publishers = new Publishers();
- const githubToken = config.getString('scaffolder.github.token');
- const repoVisibility = config.getString(
- 'scaffolder.github.visibility',
- ) as RepoVisilityOptions;
+ const githubConfig = config.getOptionalConfig('scaffolder.github');
- const githubClient = new Octokit({ auth: githubToken });
- const githubPublisher = new GithubPublisher({
- client: githubClient,
- token: githubToken,
- repoVisibility,
- });
- publishers.register('file', githubPublisher);
- publishers.register('github', githubPublisher);
+ if (githubConfig) {
+ try {
+ const repoVisibility = githubConfig.getString(
+ 'visibility',
+ ) as RepoVisilityOptions;
+
+ const githubToken = githubConfig.getString('token');
+ const githubClient = new Octokit({ auth: githubToken });
+ const githubPublisher = new GithubPublisher({
+ client: githubClient,
+ token: githubToken,
+ repoVisibility,
+ });
+ publishers.register('file', githubPublisher);
+ publishers.register('github', githubPublisher);
+ } catch (e) {
+ const providerName = 'github';
+ if (process.env.NODE_ENV !== 'development') {
+ throw new Error(
+ `Failed to initialize ${providerName} scaffolding provider, ${e.message}`,
+ );
+ }
+
+ logger.warn(
+ `Skipping ${providerName} scaffolding provider, ${e.message}`,
+ );
+ }
+ }
const gitLabConfig = config.getOptionalConfig('scaffolder.gitlab.api');
-
if (gitLabConfig) {
- const gitLabToken = gitLabConfig.getString('token');
- const gitLabClient = new Gitlab({
- host: gitLabConfig.getOptionalString('baseUrl'),
- token: gitLabToken,
- });
- const gitLabPublisher = new GitlabPublisher(gitLabClient, gitLabToken);
- publishers.register('gitlab', gitLabPublisher);
- publishers.register('gitlab/api', gitLabPublisher);
+ try {
+ const gitLabToken = gitLabConfig.getString('token');
+ const gitLabClient = new Gitlab({
+ host: gitLabConfig.getOptionalString('baseUrl'),
+ token: gitLabToken,
+ });
+ const gitLabPublisher = new GitlabPublisher(gitLabClient, gitLabToken);
+ publishers.register('gitlab', gitLabPublisher);
+ publishers.register('gitlab/api', gitLabPublisher);
+ } catch (e) {
+ const providerName = 'gitlab';
+ if (process.env.NODE_ENV !== 'development') {
+ throw new Error(
+ `Failed to initialize ${providerName} scaffolding provider, ${e.message}`,
+ );
+ }
+
+ logger.warn(
+ `Skipping ${providerName} scaffolding provider, ${e.message}`,
+ );
+ }
}
const dockerClient = new Docker();
diff --git a/packages/core-api/src/app/FeatureFlags.tsx b/packages/core-api/src/app/FeatureFlags.tsx
index 11c084d3ed..3db2a18a02 100644
--- a/packages/core-api/src/app/FeatureFlags.tsx
+++ b/packages/core-api/src/app/FeatureFlags.tsx
@@ -80,6 +80,15 @@ export class UserFlags extends Map {
return output;
}
+ toggle(name: FeatureFlagName): FeatureFlagState {
+ if (super.get(name) === FeatureFlagState.On) {
+ super.set(name, FeatureFlagState.Off);
+ } else {
+ super.set(name, FeatureFlagState.On);
+ }
+ return super.get(name) || FeatureFlagState.Off;
+ }
+
delete(name: FeatureFlagName): boolean {
const output = super.delete(name);
this.save();
diff --git a/packages/core/src/layout/Sidebar/PinButton.tsx b/packages/core/src/layout/Sidebar/PinButton.tsx
deleted file mode 100644
index 8c52eeb24d..0000000000
--- a/packages/core/src/layout/Sidebar/PinButton.tsx
+++ /dev/null
@@ -1,75 +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, useContext } from 'react';
-import { makeStyles } from '@material-ui/core';
-import DoubleArrowIcon from '@material-ui/icons/DoubleArrow';
-import { SidebarContext } from './config';
-import { BackstageTheme } from '@backstage/theme';
-import { SidebarPinStateContext } from './Page';
-
-const ARROW_BUTTON_SIZE = 20;
-const useStyles = makeStyles(theme => {
- return {
- root: {
- position: 'relative',
- alignSelf: 'stretch',
- },
- arrowButtonWrapper: {
- position: 'absolute',
- right: 0,
- width: ARROW_BUTTON_SIZE,
- height: ARROW_BUTTON_SIZE,
- top: -(theme.spacing(6) + ARROW_BUTTON_SIZE) / 2,
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'center',
- borderRadius: '2px 0px 0px 2px',
- background: theme.palette.pinSidebarButton.background,
- color: theme.palette.pinSidebarButton.icon,
- border: 'none',
- outline: 'none',
- cursor: 'pointer',
- },
- arrowButtonIcon: {
- transform: ({ isPinned }) => (isPinned ? 'rotate(180deg)' : 'none'),
- },
- };
-});
-
-export const SidebarPinButton: FC<{}> = () => {
- const { isOpen } = useContext(SidebarContext);
- const { isPinned, toggleSidebarPinState } = useContext(
- SidebarPinStateContext,
- );
- const classes = useStyles({ isPinned });
-
- return (
-
- {isOpen && (
-
-
-
- )}
-
- );
-};
diff --git a/packages/core/src/layout/Sidebar/Settings/AppSettingsList.tsx b/packages/core/src/layout/Sidebar/Settings/AppSettingsList.tsx
new file mode 100644
index 0000000000..5dad178ccd
--- /dev/null
+++ b/packages/core/src/layout/Sidebar/Settings/AppSettingsList.tsx
@@ -0,0 +1,26 @@
+/*
+ * 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 { List, ListSubheader } from '@material-ui/core';
+import { SidebarThemeToggle } from './ThemeToggle';
+import { SidebarPinButton } from './PinButton';
+
+export const AppSettingsList = () => (
+ App Settings}>
+
+
+
+);
diff --git a/packages/core/src/layout/Sidebar/Settings/AuthProviderList.tsx b/packages/core/src/layout/Sidebar/Settings/AuthProviderList.tsx
new file mode 100644
index 0000000000..34cf6fb837
--- /dev/null
+++ b/packages/core/src/layout/Sidebar/Settings/AuthProviderList.tsx
@@ -0,0 +1,29 @@
+/*
+ * 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 List from '@material-ui/core/List';
+import ListSubheader from '@material-ui/core/ListSubheader';
+
+type Props = {
+ providerSettings: React.ReactNode;
+};
+
+export const AuthProvidersList = ({ providerSettings }: Props) => (
+ Available Auth Providers}>
+ {providerSettings}
+
+);
diff --git a/packages/core/src/layout/Sidebar/Settings/FeatureFlagsItem.tsx b/packages/core/src/layout/Sidebar/Settings/FeatureFlagsItem.tsx
new file mode 100644
index 0000000000..3e40ce69cc
--- /dev/null
+++ b/packages/core/src/layout/Sidebar/Settings/FeatureFlagsItem.tsx
@@ -0,0 +1,73 @@
+/*
+ * 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 {
+ FeatureFlagName,
+ useApi,
+ featureFlagsApiRef,
+} from '@backstage/core-api';
+import {
+ ListItem,
+ ListItemSecondaryAction,
+ ListItemText,
+ Tooltip,
+} from '@material-ui/core';
+import CheckIcon from '@material-ui/icons/CheckCircle';
+import { ToggleButton } from '@material-ui/lab';
+
+export type Item = {
+ name: FeatureFlagName;
+ pluginId: string;
+};
+
+type Props = {
+ featureFlag: Item;
+};
+
+export const FlagItem = ({ featureFlag }: Props) => {
+ const api = useApi(featureFlagsApiRef);
+
+ const [enabled, setEnabled] = React.useState(
+ Boolean(api.getFlags().get(featureFlag.name)),
+ );
+
+ const toggleFlag = () => {
+ const newState = api.getFlags().toggle(featureFlag.name);
+ setEnabled(Boolean(newState));
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/core/src/layout/Sidebar/Settings/FeatureFlagsList.tsx b/packages/core/src/layout/Sidebar/Settings/FeatureFlagsList.tsx
new file mode 100644
index 0000000000..5687446511
--- /dev/null
+++ b/packages/core/src/layout/Sidebar/Settings/FeatureFlagsList.tsx
@@ -0,0 +1,32 @@
+/*
+ * 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 List from '@material-ui/core/List';
+import ListSubheader from '@material-ui/core/ListSubheader';
+import { FlagItem, Item } from './FeatureFlagsItem';
+
+type Props = {
+ featureFlags: Item[];
+};
+
+export const FeatureFlagsList = ({ featureFlags }: Props) => (
+ Feature Flags}>
+ {featureFlags.map(featureFlag => (
+
+ ))}
+
+);
diff --git a/packages/core/src/layout/Sidebar/Settings/PinButton.tsx b/packages/core/src/layout/Sidebar/Settings/PinButton.tsx
new file mode 100644
index 0000000000..2727313ede
--- /dev/null
+++ b/packages/core/src/layout/Sidebar/Settings/PinButton.tsx
@@ -0,0 +1,64 @@
+/*
+ * 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, { useContext } from 'react';
+import {
+ ListItem,
+ ListItemSecondaryAction,
+ ListItemText,
+ Tooltip,
+} from '@material-ui/core';
+import LockIcon from '@material-ui/icons/Lock';
+import LockOpenIcon from '@material-ui/icons/LockOpen';
+import { ToggleButton } from '@material-ui/lab';
+import { SidebarPinStateContext } from '../Page';
+
+export const SidebarPinButton = () => {
+ const { isPinned, toggleSidebarPinState } = useContext(
+ SidebarPinStateContext,
+ );
+
+ const PinIcon = () => (
+
+ {isPinned ? : }
+
+ );
+
+ return (
+
+
+
+ {
+ toggleSidebarPinState();
+ }}
+ >
+
+
+
+
+ );
+};
diff --git a/packages/core/src/layout/Sidebar/Settings/ProviderSettingsItem.tsx b/packages/core/src/layout/Sidebar/Settings/ProviderSettingsItem.tsx
index 3d3d7439e1..653c64575b 100644
--- a/packages/core/src/layout/Sidebar/Settings/ProviderSettingsItem.tsx
+++ b/packages/core/src/layout/Sidebar/Settings/ProviderSettingsItem.tsx
@@ -14,31 +14,53 @@
* limitations under the License.
*/
-import React, { FC } from 'react';
-import { OAuthApi, OpenIdConnectApi, IconComponent } from '@backstage/core-api';
-import { SidebarItem } from '../Items';
-import { IconButton, Tooltip } from '@material-ui/core';
-import StarBorder from '@material-ui/icons/StarBorder';
+import React from 'react';
+import { IconComponent, OAuthApi, OpenIdConnectApi } from '@backstage/core-api';
+import {
+ ListItem,
+ ListItemIcon,
+ ListItemSecondaryAction,
+ ListItemText,
+ Tooltip,
+} from '@material-ui/core';
import PowerButton from '@material-ui/icons/PowerSettingsNew';
+import { ToggleButton } from '@material-ui/lab';
-export const ProviderSettingsItem: FC<{
+type Props = {
title: string;
icon: IconComponent;
signedIn: boolean;
api: OAuthApi | OpenIdConnectApi;
signInHandler: Function;
-}> = ({ title, icon, signedIn, api, signInHandler }) => {
- return (
-
- (signedIn ? api.logout() : signInHandler())}>
+};
+
+export const ProviderSettingsItem = ({
+ title,
+ icon: Icon,
+ signedIn,
+ api,
+ signInHandler,
+}: Props) => (
+
+
+
+
+
+
+ (signedIn ? api.logout() : signInHandler())}
+ >
-
+
-
-
- );
-};
+
+
+
+);
diff --git a/packages/core/src/layout/Sidebar/Settings/SettingsDialog.tsx b/packages/core/src/layout/Sidebar/Settings/SettingsDialog.tsx
new file mode 100644
index 0000000000..71c4899862
--- /dev/null
+++ b/packages/core/src/layout/Sidebar/Settings/SettingsDialog.tsx
@@ -0,0 +1,74 @@
+/*
+ * 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 {
+ Card,
+ CardContent,
+ CardHeader,
+ makeStyles,
+ Divider,
+} from '@material-ui/core';
+import { AppSettingsList } from './AppSettingsList';
+import { AuthProvidersList } from './AuthProviderList';
+import { FeatureFlagsList } from './FeatureFlagsList';
+import { SignInAvatar } from './SignInAvatar';
+import { UserSettingsMenu } from './UserSettingsMenu';
+import { useUserProfile } from './useUserProfileInfo';
+import { useApi, featureFlagsApiRef } from '@backstage/core-api';
+
+const useStyles = makeStyles({
+ root: {
+ minWidth: 400,
+ },
+});
+
+type Props = {
+ providerSettings?: React.ReactNode;
+};
+
+export const SettingsDialog = ({ providerSettings }: Props) => {
+ const classes = useStyles();
+ const { profile, displayName } = useUserProfile();
+ const featureFlagsApi = useApi(featureFlagsApiRef);
+ const featureFlags = featureFlagsApi.getRegisteredFlags();
+
+ return (
+
+ }
+ action={ }
+ title={displayName}
+ subheader={profile.email}
+ />
+
+
+ {providerSettings && (
+ <>
+
+
+ >
+ )}
+ {featureFlags.length > 0 && (
+ <>
+
+
+ >
+ )}
+
+
+ );
+};
diff --git a/packages/core/src/layout/Sidebar/Settings/SignInAvatar.tsx b/packages/core/src/layout/Sidebar/Settings/SignInAvatar.tsx
new file mode 100644
index 0000000000..f0430edfcf
--- /dev/null
+++ b/packages/core/src/layout/Sidebar/Settings/SignInAvatar.tsx
@@ -0,0 +1,42 @@
+/*
+ * 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 { BackstageTheme } from '@backstage/theme';
+import { makeStyles, Avatar } from '@material-ui/core';
+import { useUserProfile } from './useUserProfileInfo';
+import { sidebarConfig } from '../config';
+
+const useStyles = makeStyles({
+ avatar: {
+ width: ({ size }) => size,
+ height: ({ size }) => size,
+ },
+});
+
+type Props = { size?: number };
+
+export const SignInAvatar = ({ size }: Props) => {
+ const { iconSize } = sidebarConfig;
+ const classes = useStyles(size ? { size } : { size: iconSize });
+ const { profile, displayName } = useUserProfile();
+
+ return (
+
+ {displayName[0]}
+
+ );
+};
diff --git a/packages/core/src/layout/Sidebar/Settings/ThemeToggle.tsx b/packages/core/src/layout/Sidebar/Settings/ThemeToggle.tsx
new file mode 100644
index 0000000000..a5e703089c
--- /dev/null
+++ b/packages/core/src/layout/Sidebar/Settings/ThemeToggle.tsx
@@ -0,0 +1,87 @@
+/*
+ * 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 { useObservable } from 'react-use';
+import LightIcon from '@material-ui/icons/WbSunny';
+import DarkIcon from '@material-ui/icons/Brightness2';
+import AutoIcon from '@material-ui/icons/BrightnessAuto';
+import { appThemeApiRef, useApi } from '@backstage/core-api';
+import ToggleButton from '@material-ui/lab/ToggleButton';
+import ToggleButtonGroup from '@material-ui/lab/ToggleButtonGroup';
+import {
+ ListItem,
+ ListItemText,
+ ListItemSecondaryAction,
+ Tooltip,
+} from '@material-ui/core';
+
+export const SidebarThemeToggle = () => {
+ const appThemeApi = useApi(appThemeApiRef);
+ const themeId = useObservable(
+ appThemeApi.activeThemeId$(),
+ appThemeApi.getActiveThemeId(),
+ );
+
+ const themeIds = appThemeApi.getInstalledThemes();
+ // TODO(marcuseide): can these be put on the theme itself?
+ const themeIcons = {
+ dark: ,
+ light: ,
+ };
+
+ const handleSetTheme = (
+ _event: React.MouseEvent,
+ newThemeId: string | undefined,
+ ) => {
+ if (themeIds.some(t => t.id === newThemeId)) {
+ appThemeApi.setActiveThemeId(newThemeId);
+ } else {
+ appThemeApi.setActiveThemeId(undefined);
+ }
+ };
+
+ return (
+
+
+
+
+ {themeIds.map(theme => (
+
+
+ {themeIcons[theme.variant]}
+
+
+ ))}
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/core/src/layout/Sidebar/Settings/UserProfile.tsx b/packages/core/src/layout/Sidebar/Settings/UserProfile.tsx
deleted file mode 100644
index 3b854801c9..0000000000
--- a/packages/core/src/layout/Sidebar/Settings/UserProfile.tsx
+++ /dev/null
@@ -1,61 +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, useRef } from 'react';
-import { makeStyles, Avatar, Divider } from '@material-ui/core';
-import { useApi, identityApiRef } from '@backstage/core-api';
-import { SidebarItem } from '../Items';
-import ExpandLess from '@material-ui/icons/ExpandLess';
-import ExpandMore from '@material-ui/icons/ExpandMore';
-
-const useStyles = makeStyles({
- avatar: {
- width: 24,
- height: 24,
- },
-});
-
-export const UserProfile: FC<{ open: boolean; setOpen: Function }> = ({
- open,
- setOpen,
-}) => {
- const ref = useRef(); // for scrolling down when collapse item opens
- const classes = useStyles();
- const identityApi = useApi(identityApiRef);
-
- const handleClick = () => {
- setOpen(!open);
- setTimeout(() => ref.current?.scrollIntoView({ behavior: 'smooth' }), 300);
- };
-
- const userId = identityApi.getUserId();
- const profile = identityApi.getProfile();
- const displayName = profile.displayName ?? userId;
- const SignInAvatar = () => (
-
- {displayName[0]}
-
- );
-
- return (
- <>
-
-
- {open ? : }
-
- >
- );
-};
diff --git a/packages/core/src/layout/Sidebar/Settings/UserSettings.tsx b/packages/core/src/layout/Sidebar/Settings/UserSettings.tsx
new file mode 100644
index 0000000000..85dbefff9b
--- /dev/null
+++ b/packages/core/src/layout/Sidebar/Settings/UserSettings.tsx
@@ -0,0 +1,77 @@
+/*
+ * 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, { useEffect, useContext } from 'react';
+import { Popover } from '@material-ui/core';
+import { SignInAvatar } from './SignInAvatar';
+import { SettingsDialog } from './SettingsDialog';
+import { SidebarItem } from '../Items';
+import { useUserProfile } from './useUserProfileInfo';
+import { SidebarContext } from '../config';
+
+type Props = {
+ providerSettings?: React.ReactNode;
+};
+
+export const SidebarUserSettings = ({ providerSettings }: Props) => {
+ const { isOpen: sidebarOpen } = useContext(SidebarContext);
+ const { displayName } = useUserProfile();
+ const [open, setOpen] = React.useState(false);
+ const [anchorEl, setAnchorEl] = React.useState(
+ undefined,
+ );
+
+ const handleOpen = (event?: React.MouseEvent) => {
+ setAnchorEl(event?.currentTarget ?? undefined);
+ setOpen(true);
+ };
+
+ const handleClose = () => {
+ setAnchorEl(undefined);
+ setOpen(false);
+ };
+
+ useEffect(() => {
+ if (!sidebarOpen && open) setOpen(false);
+ }, [open, sidebarOpen]);
+
+ const SidebarAvatar = () => ;
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+};
diff --git a/packages/core/src/layout/Sidebar/Settings/UserSettingsMenu.tsx b/packages/core/src/layout/Sidebar/Settings/UserSettingsMenu.tsx
new file mode 100644
index 0000000000..9ac287c57e
--- /dev/null
+++ b/packages/core/src/layout/Sidebar/Settings/UserSettingsMenu.tsx
@@ -0,0 +1,55 @@
+/*
+ * 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 { identityApiRef, useApi } from '@backstage/core-api';
+import { IconButton, ListItemIcon, Menu, MenuItem } from '@material-ui/core';
+import SignOutIcon from '@material-ui/icons/MeetingRoom';
+import MoreVertIcon from '@material-ui/icons/MoreVert';
+
+export const UserSettingsMenu = () => {
+ const identityApi = useApi(identityApiRef);
+ const [open, setOpen] = React.useState(false);
+ const [anchorEl, setAnchorEl] = React.useState(
+ undefined,
+ );
+
+ const handleOpen = (event: React.MouseEvent) => {
+ setAnchorEl(event.currentTarget);
+ setOpen(true);
+ };
+
+ const handleClose = () => {
+ setAnchorEl(undefined);
+ setOpen(false);
+ };
+
+ return (
+ <>
+
+
+
+
+ identityApi.logout()}>
+
+
+
+ Sign Out
+
+
+ >
+ );
+};
diff --git a/packages/core/src/layout/Sidebar/Settings/index.ts b/packages/core/src/layout/Sidebar/Settings/index.ts
index 6557ace53a..7abf061620 100644
--- a/packages/core/src/layout/Sidebar/Settings/index.ts
+++ b/packages/core/src/layout/Sidebar/Settings/index.ts
@@ -17,4 +17,4 @@
export { ProviderSettingsItem } from './ProviderSettingsItem';
export { OAuthProviderSettings } from './OAuthProviderSettings';
export { OIDCProviderSettings } from './OIDCProviderSettings';
-export { UserProfile } from './UserProfile';
+export { SidebarUserSettings } from './UserSettings';
diff --git a/packages/core/src/layout/Sidebar/Settings/useUserProfileInfo.ts b/packages/core/src/layout/Sidebar/Settings/useUserProfileInfo.ts
new file mode 100644
index 0000000000..60dae294a5
--- /dev/null
+++ b/packages/core/src/layout/Sidebar/Settings/useUserProfileInfo.ts
@@ -0,0 +1,26 @@
+/*
+ * 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 { useApi, identityApiRef } from '@backstage/core-api';
+
+export const useUserProfile = () => {
+ const identityApi = useApi(identityApiRef);
+ const userId = identityApi.getUserId();
+ const profile = identityApi.getProfile();
+ const displayName = profile.displayName ?? userId;
+
+ return { profile, displayName };
+};
diff --git a/packages/core/src/layout/Sidebar/SidebarThemeToggle.tsx b/packages/core/src/layout/Sidebar/SidebarThemeToggle.tsx
deleted file mode 100644
index 32bc9d9632..0000000000
--- a/packages/core/src/layout/Sidebar/SidebarThemeToggle.tsx
+++ /dev/null
@@ -1,58 +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 { useObservable } from 'react-use';
-import LightIcon from '@material-ui/icons/WbSunny';
-import DarkIcon from '@material-ui/icons/Brightness2';
-import AutoIcon from '@material-ui/icons/BrightnessAuto';
-import { appThemeApiRef, useApi } from '@backstage/core-api';
-import { SidebarItem } from './Items';
-
-export const SidebarThemeToggle: FC<{}> = () => {
- const appThemeApi = useApi(appThemeApiRef);
- const themeId = useObservable(
- appThemeApi.activeThemeId$(),
- appThemeApi.getActiveThemeId(),
- );
-
- let text = 'Auto';
- let icon = AutoIcon;
- switch (themeId) {
- case 'dark':
- text = 'Dark mode';
- icon = DarkIcon;
- break;
- case 'light':
- text = 'Light mode';
- icon = LightIcon;
- break;
- default:
- break;
- }
-
- const handleToggle = () => {
- if (!themeId) {
- appThemeApi.setActiveThemeId('light');
- } else if (themeId === 'light') {
- appThemeApi.setActiveThemeId('dark');
- } else {
- appThemeApi.setActiveThemeId(undefined);
- }
- };
-
- return ;
-};
diff --git a/packages/core/src/layout/Sidebar/UserSettings.tsx b/packages/core/src/layout/Sidebar/UserSettings.tsx
deleted file mode 100644
index f586f6d077..0000000000
--- a/packages/core/src/layout/Sidebar/UserSettings.tsx
+++ /dev/null
@@ -1,53 +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 { identityApiRef, useApi } from '@backstage/core-api';
-import Collapse from '@material-ui/core/Collapse';
-import SignOutIcon from '@material-ui/icons/MeetingRoom';
-import React, { useContext, useEffect } from 'react';
-import { SidebarContext } from './config';
-import { SidebarItem } from './Items';
-import { UserProfile as SidebarUserProfile } from './Settings';
-
-type SidebarUserSettingsProps = { providerSettings?: React.ReactNode };
-
-export function SidebarUserSettings({
- providerSettings,
-}: SidebarUserSettingsProps) {
- const { isOpen: sidebarOpen } = useContext(SidebarContext);
- const [open, setOpen] = React.useState(false);
- const identityApi = useApi(identityApiRef);
-
- // Close the provider list when sidebar collapse
- useEffect(() => {
- if (!sidebarOpen && open) setOpen(false);
- }, [open, sidebarOpen]);
-
- return (
- <>
-
-
- {providerSettings}
-
- identityApi.logout()}
- />
-
- >
- );
-}
diff --git a/packages/core/src/layout/Sidebar/index.ts b/packages/core/src/layout/Sidebar/index.ts
index fdca7e4eea..a644c06e14 100644
--- a/packages/core/src/layout/Sidebar/index.ts
+++ b/packages/core/src/layout/Sidebar/index.ts
@@ -25,14 +25,11 @@ export {
SidebarSpacer,
} from './Items';
export { IntroCard, SidebarIntro } from './Intro';
-export { SidebarPinButton } from './PinButton';
export {
SIDEBAR_INTRO_LOCAL_STORAGE,
SidebarContext,
sidebarConfig,
} from './config';
export type { SidebarContextType } from './config';
-export { SidebarThemeToggle } from './SidebarThemeToggle';
-export { SidebarUserSettings } from './UserSettings';
export { DefaultProviderSettings } from './DefaultProviderSettings';
export * from './Settings';
diff --git a/packages/create-app/templates/default-app/packages/app/src/sidebar.tsx b/packages/create-app/templates/default-app/packages/app/src/sidebar.tsx
index 96d18965ec..9dd9ea64c3 100644
--- a/packages/create-app/templates/default-app/packages/app/src/sidebar.tsx
+++ b/packages/create-app/templates/default-app/packages/app/src/sidebar.tsx
@@ -18,8 +18,6 @@ import {
SidebarContext,
SidebarSpace,
SidebarUserSettings,
- SidebarThemeToggle,
- SidebarPinButton,
DefaultProviderSettings,
} from '@backstage/core';
@@ -39,9 +37,7 @@ export const AppSidebar = () => (
-
} />
-
);
diff --git a/packages/e2e-test/src/e2e-test.ts b/packages/e2e-test/src/e2e-test.ts
index 83f92ea360..146a3ec644 100644
--- a/packages/e2e-test/src/e2e-test.ts
+++ b/packages/e2e-test/src/e2e-test.ts
@@ -403,5 +403,15 @@ async function testBackendStart(appDir: string, isPostgres: boolean) {
}
}
-process.on('unhandledRejection', handleError);
+process.on('unhandledRejection', (error: Error) => {
+ // Try to avoid exiting if the unhandled error is coming from jsdom, i.e. zombie.
+ // Those are typically errors on the page that should be benign, at least in the
+ // context of this test. We have other ways of asserting that the page is being
+ // rendered correctly.
+ if (error?.stack?.includes('node_modules/jsdom/lib')) {
+ console.log(`Ignored error inside jsdom, ${error}`);
+ } else {
+ handleError(error);
+ }
+});
main().catch(handleError);
diff --git a/plugins/api-docs/src/catalog/EntityPageApi/EntityPageApi.tsx b/plugins/api-docs/src/catalog/EntityPageApi/EntityPageApi.tsx
index 5383fc0570..e2148184b7 100644
--- a/plugins/api-docs/src/catalog/EntityPageApi/EntityPageApi.tsx
+++ b/plugins/api-docs/src/catalog/EntityPageApi/EntityPageApi.tsx
@@ -16,8 +16,8 @@
import { ComponentEntity, Entity } from '@backstage/catalog-model';
import { Progress } from '@backstage/core';
-import React from 'react';
import { Grid } from '@material-ui/core';
+import React from 'react';
import {
ApiDefinitionCard,
useComponentApiEntities,
diff --git a/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.tsx b/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.tsx
index 055363ae1f..49b8a69318 100644
--- a/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.tsx
+++ b/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.tsx
@@ -15,15 +15,16 @@
*/
import { ApiEntity } from '@backstage/catalog-model';
-import { CardTab, TabbedCard } from '@backstage/core';
+import { CardTab, useApi, TabbedCard } from '@backstage/core';
import { Alert } from '@material-ui/lab';
import React from 'react';
+import { apiDocsConfigRef } from '../../config';
import { PlainApiDefinitionWidget } from '../PlainApiDefinitionWidget';
import { OpenApiDefinitionWidget } from '../OpenApiDefinitionWidget';
import { AsyncApiDefinitionWidget } from '../AsyncApiDefinitionWidget';
import { GraphQlDefinitionWidget } from '../GraphQlDefinitionWidget';
-type ApiDefinitionWidget = {
+export type ApiDefinitionWidget = {
type: string;
title: string;
component: (definition: string) => React.ReactElement;
@@ -61,34 +62,25 @@ export function defaultDefinitionWidgets(): ApiDefinitionWidget[] {
type Props = {
apiEntity?: ApiEntity;
- definitionWidgets?: ApiDefinitionWidget[];
};
-const defaultProps = {
- definitionWidgets: defaultDefinitionWidgets(),
-};
-
-export const ApiDefinitionCard = (props: Props) => {
- const { apiEntity, definitionWidgets } = {
- ...defaultProps,
- ...props,
- };
+export const ApiDefinitionCard = ({ apiEntity }: Props) => {
+ const config = useApi(apiDocsConfigRef);
+ const { getApiDefinitionWidget } = config;
if (!apiEntity) {
return Could not fetch the API ;
}
- const definitionWidget = definitionWidgets.find(
- d => d.type === apiEntity.spec.type,
- );
+ const definitionWidget = getApiDefinitionWidget(apiEntity);
if (definitionWidget) {
return (
-
+
{definitionWidget.component(apiEntity.spec.definition)}
-
+
{
title={apiEntity.metadata.name}
children={[
// Has to be an array, otherwise typescript doesn't like that this has only a single child
-
+
{
@@ -32,6 +33,7 @@ describe('ApiCatalogPage', () => {
metadata: {
name: 'Entity1',
},
+ spec: { type: 'openapi' },
},
{
apiVersion: 'backstage.io/v1alpha1',
@@ -39,12 +41,17 @@ describe('ApiCatalogPage', () => {
metadata: {
name: 'Entity2',
},
+ spec: { type: 'openapi' },
},
] as Entity[]),
getLocationByEntity: () =>
Promise.resolve({ id: 'id', type: 'github', target: 'url' }),
};
+ const apiDocsConfig = {
+ getApiDefinitionWidget: () => undefined,
+ };
+
const renderWrapped = (children: React.ReactNode) =>
render(
wrapInTestApp(
@@ -52,6 +59,7 @@ describe('ApiCatalogPage', () => {
apis={ApiRegistry.from([
[catalogApiRef, catalogApi],
[storageApiRef, MockStorageApi.create()],
+ [apiDocsConfigRef, apiDocsConfig],
])}
>
{children}
diff --git a/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.test.tsx b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.test.tsx
index 9cd7b01945..c679c2201f 100644
--- a/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.test.tsx
+++ b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.test.tsx
@@ -15,9 +15,11 @@
*/
import { Entity } from '@backstage/catalog-model';
+import { ApiProvider, ApiRegistry } from '@backstage/core';
import { wrapInTestApp } from '@backstage/test-utils';
import { render } from '@testing-library/react';
import * as React from 'react';
+import { apiDocsConfigRef } from '../../config';
import { ApiExplorerTable } from './ApiExplorerTable';
const entites: Entity[] = [
@@ -25,29 +27,38 @@ const entites: Entity[] = [
apiVersion: 'backstage.io/v1alpha1',
kind: 'API',
metadata: { name: 'api1' },
+ spec: { type: 'openapi' },
},
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'API',
metadata: { name: 'api2' },
+ spec: { type: 'openapi' },
},
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'API',
metadata: { name: 'api3' },
+ spec: { type: 'grpc' },
},
];
+const apiRegistry = ApiRegistry.with(apiDocsConfigRef, {
+ getApiDefinitionWidget: () => undefined,
+});
+
describe('ApiCatalogTable component', () => {
it('should render error message when error is passed in props', async () => {
const rendered = render(
wrapInTestApp(
- ,
+
+
+ ,
),
);
const errorMessage = await rendered.findByText(
@@ -59,11 +70,13 @@ describe('ApiCatalogTable component', () => {
it('should display entity names when loading has finished and no error occurred', async () => {
const rendered = render(
wrapInTestApp(
- ,
+
+
+ ,
),
);
expect(rendered.getByText(/APIs \(3\)/)).toBeInTheDocument();
diff --git a/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx
index fd1e9c3518..485fcc216d 100644
--- a/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx
+++ b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx
@@ -14,14 +14,23 @@
* limitations under the License.
*/
-import { Entity } from '@backstage/catalog-model';
-import { Table, TableColumn } from '@backstage/core';
-import { Link, Chip } from '@material-ui/core';
+import { ApiEntityV1alpha1, Entity } from '@backstage/catalog-model';
+import { Table, TableColumn, useApi } from '@backstage/core';
+import { Chip, Link } from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import React from 'react';
import { generatePath, Link as RouterLink } from 'react-router-dom';
+import { apiDocsConfigRef } from '../../config';
import { entityRoute } from '../../routes';
+const ApiTypeTitle = ({ apiEntity }: { apiEntity: ApiEntityV1alpha1 }) => {
+ const config = useApi(apiDocsConfigRef);
+ const definition = config.getApiDefinitionWidget(apiEntity);
+ const type = definition ? definition.title : apiEntity.spec.type;
+
+ return {type} ;
+};
+
const columns: TableColumn[] = [
{
title: 'Name',
@@ -54,8 +63,11 @@ const columns: TableColumn[] = [
field: 'spec.lifecycle',
},
{
- title: 'Type', // TODO: Resolve the type display name using the API from https://github.com/spotify/backstage/pull/2451
+ title: 'Type',
field: 'spec.type',
+ render: (entity: Entity) => (
+
+ ),
},
{
title: 'Description',
diff --git a/plugins/api-docs/src/components/index.ts b/plugins/api-docs/src/components/index.ts
index 55b9517f9c..cf9b189091 100644
--- a/plugins/api-docs/src/components/index.ts
+++ b/plugins/api-docs/src/components/index.ts
@@ -14,7 +14,11 @@
* limitations under the License.
*/
-export { ApiDefinitionCard } from './ApiDefinitionCard';
+export type { ApiDefinitionWidget } from './ApiDefinitionCard';
+export {
+ ApiDefinitionCard,
+ defaultDefinitionWidgets,
+} from './ApiDefinitionCard';
export { AsyncApiDefinitionWidget } from './AsyncApiDefinitionWidget';
export { OpenApiDefinitionWidget } from './OpenApiDefinitionWidget';
export { PlainApiDefinitionWidget } from './PlainApiDefinitionWidget';
diff --git a/plugins/api-docs/src/config.ts b/plugins/api-docs/src/config.ts
new file mode 100644
index 0000000000..a90189a037
--- /dev/null
+++ b/plugins/api-docs/src/config.ts
@@ -0,0 +1,30 @@
+/*
+ * 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 { ApiEntity } from '@backstage/catalog-model';
+import { createApiRef } from '@backstage/core';
+import { ApiDefinitionWidget } from './components';
+
+export const apiDocsConfigRef = createApiRef({
+ id: 'plugin.api-docs.config',
+ description: 'Used to configure api-docs widgets',
+});
+
+export interface ApiDocsConfig {
+ getApiDefinitionWidget: (
+ apiEntity: ApiEntity,
+ ) => ApiDefinitionWidget | undefined;
+}
diff --git a/plugins/api-docs/src/plugin.ts b/plugins/api-docs/src/plugin.ts
index bd9c968d5b..3fcbe8a1f4 100644
--- a/plugins/api-docs/src/plugin.ts
+++ b/plugins/api-docs/src/plugin.ts
@@ -14,13 +14,30 @@
* limitations under the License.
*/
-import { createPlugin } from '@backstage/core';
+import { ApiEntity } from '@backstage/catalog-model';
+import { createApiFactory, createPlugin } from '@backstage/core';
import { ApiExplorerPage } from './components/ApiExplorerPage/ApiExplorerPage';
+import { defaultDefinitionWidgets } from './components/ApiDefinitionCard';
import { ApiEntityPage } from './components/ApiEntityPage/ApiEntityPage';
import { entityRoute, rootRoute } from './routes';
+import { apiDocsConfigRef } from './config';
export const plugin = createPlugin({
id: 'api-docs',
+ apis: [
+ createApiFactory({
+ api: apiDocsConfigRef,
+ deps: {},
+ factory: () => {
+ const definitionWidgets = defaultDefinitionWidgets();
+ return {
+ getApiDefinitionWidget: (apiEntity: ApiEntity) => {
+ return definitionWidgets.find(d => d.type === apiEntity.spec.type);
+ },
+ };
+ },
+ }),
+ ],
register({ router }) {
router.addRoute(rootRoute, ApiExplorerPage);
router.addRoute(entityRoute, ApiEntityPage);
diff --git a/plugins/api-docs/src/routes.ts b/plugins/api-docs/src/routes.ts
index eea911dd62..d2d8ef10c5 100644
--- a/plugins/api-docs/src/routes.ts
+++ b/plugins/api-docs/src/routes.ts
@@ -23,11 +23,13 @@ export const rootRoute = createRouteRef({
path: '/api-docs',
title: 'APIs',
});
+
export const entityRoute = createRouteRef({
icon: NoIcon,
path: '/api-docs/:optionalNamespaceAndName/',
title: 'API',
});
+
export const catalogRoute = createRouteRef({
icon: NoIcon,
path: '',
diff --git a/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml b/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml
index b4bd45d163..31b57135ad 100644
--- a/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml
+++ b/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml
@@ -17,6 +17,7 @@ spec:
schema:
required:
- component_id
+ - description
properties:
component_id:
title: Name
diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts
index c06a553771..e68b0d0447 100644
--- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts
@@ -147,6 +147,7 @@ describe('GitHub Publisher', () => {
storePath: 'blam/test',
owner: 'bob',
access: 'bob',
+ description: 'description',
},
directory: '/tmp/test',
});
@@ -154,6 +155,7 @@ describe('GitHub Publisher', () => {
expect(
mockGithubClient.repos.createForAuthenticatedUser,
).toHaveBeenCalledWith({
+ description: 'description',
name: 'test',
private: false,
});
diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts
index 1d5c9e1947..d3aafb1e07 100644
--- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts
@@ -61,6 +61,7 @@ export class GithubPublisher implements PublisherBase {
values: RequiredTemplateValues & Record,
) {
const [owner, name] = values.storePath.split('/');
+ const description = values.description as string;
const user = await this.client.users.getByUsername({ username: owner });
@@ -71,10 +72,12 @@ export class GithubPublisher implements PublisherBase {
org: owner,
private: this.repoVisibility !== 'public',
visibility: this.repoVisibility,
+ description,
})
: this.client.repos.createForAuthenticatedUser({
name,
private: this.repoVisibility === 'private',
+ description,
});
const { data } = await repoCreationPromise;
diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json
index 476b321e1e..b9c2953d28 100644
--- a/plugins/techdocs-backend/package.json
+++ b/plugins/techdocs-backend/package.json
@@ -25,6 +25,7 @@
"@backstage/config": "^0.1.1-alpha.21",
"@types/dockerode": "^2.5.34",
"@types/express": "^4.17.6",
+ "command-exists-promise": "^2.0.2",
"default-branch": "^1.0.8",
"dockerode": "^3.2.1",
"express": "^4.17.1",
diff --git a/plugins/techdocs-backend/src/techdocs/stages/generate/helpers.test.ts b/plugins/techdocs-backend/src/techdocs/stages/generate/helpers.test.ts
index 799e79ef67..283560f0ce 100644
--- a/plugins/techdocs-backend/src/techdocs/stages/generate/helpers.test.ts
+++ b/plugins/techdocs-backend/src/techdocs/stages/generate/helpers.test.ts
@@ -51,6 +51,10 @@ describe('helpers', () => {
jest
.spyOn(mockDocker, 'run')
.mockResolvedValue([{ Error: null, StatusCode: 0 }]);
+
+ jest
+ .spyOn(mockDocker, 'ping')
+ .mockResolvedValue(Buffer.from('OK', 'utf-8'));
});
const imageName = 'spotify/techdocs';
@@ -99,5 +103,39 @@ describe('helpers', () => {
},
);
});
+
+ it('should ping docker to test availability', async () => {
+ await runDockerContainer({
+ imageName,
+ args,
+ docsDir,
+ resultDir,
+ dockerClient: mockDocker,
+ });
+
+ expect(mockDocker.ping).toHaveBeenCalled();
+ });
+
+ describe('where docker is unavailable', () => {
+ const dockerError = 'a docker error';
+
+ beforeEach(() => {
+ jest.spyOn(mockDocker, 'ping').mockImplementationOnce(() => {
+ throw new Error(dockerError);
+ });
+ });
+
+ it('should throw with a descriptive error message including the docker error message', async () => {
+ await expect(
+ runDockerContainer({
+ imageName,
+ args,
+ docsDir,
+ resultDir,
+ dockerClient: mockDocker,
+ }),
+ ).rejects.toThrow(new RegExp(`.+: ${dockerError}`));
+ });
+ });
});
});
diff --git a/plugins/techdocs-backend/src/techdocs/stages/generate/helpers.ts b/plugins/techdocs-backend/src/techdocs/stages/generate/helpers.ts
index 88667f9903..d68bb74aa6 100644
--- a/plugins/techdocs-backend/src/techdocs/stages/generate/helpers.ts
+++ b/plugins/techdocs-backend/src/techdocs/stages/generate/helpers.ts
@@ -18,6 +18,7 @@ import { Entity } from '@backstage/catalog-model';
import { Writable, PassThrough } from 'stream';
import Docker from 'dockerode';
import { SupportedGeneratorKey } from './types';
+import { spawn } from 'child_process';
// TODO: Implement proper support for more generators.
export function getGeneratorKey(entity: Entity): SupportedGeneratorKey {
@@ -38,6 +39,13 @@ type RunDockerContainerOptions = {
createOptions?: Docker.ContainerCreateOptions;
};
+export type RunCommandOptions = {
+ command: string;
+ args: string[];
+ options: object;
+ logStream?: Writable;
+};
+
export async function runDockerContainer({
imageName,
args,
@@ -47,6 +55,14 @@ export async function runDockerContainer({
dockerClient,
createOptions,
}: RunDockerContainerOptions) {
+ try {
+ await dockerClient.ping();
+ } catch (e) {
+ throw new Error(
+ `This operation requires Docker. Docker does not appear to be available. Docker.ping() failed with: ${e.message}`,
+ );
+ }
+
await new Promise((resolve, reject) => {
dockerClient.pull(imageName, {}, (err, stream) => {
if (err) return reject(err);
@@ -88,3 +104,41 @@ export async function runDockerContainer({
return { error, statusCode };
}
+
+/**
+ *
+ * @param options the options object
+ * @param options.command the command to run
+ * @param options.args the arguments to pass the command
+ * @param options.options options used in spawn
+ * @param options.logStream the log streamer to capture log messages
+ */
+export const runCommand = async ({
+ command,
+ args,
+ options,
+ logStream = new PassThrough(),
+}: RunCommandOptions) => {
+ await new Promise((resolve, reject) => {
+ const process = spawn(command, args, options);
+
+ process.stdout.on('data', stream => {
+ logStream.write(stream);
+ });
+
+ process.stderr.on('data', stream => {
+ logStream.write(stream);
+ });
+
+ process.on('error', error => {
+ return reject(error);
+ });
+
+ process.on('close', code => {
+ if (code !== 0) {
+ return reject(`Command ${command} failed, exit code: ${code}`);
+ }
+ return resolve();
+ });
+ });
+};
diff --git a/plugins/techdocs-backend/src/techdocs/stages/generate/techdocs.ts b/plugins/techdocs-backend/src/techdocs/stages/generate/techdocs.ts
index 40cd7590b6..cb89b42e38 100644
--- a/plugins/techdocs-backend/src/techdocs/stages/generate/techdocs.ts
+++ b/plugins/techdocs-backend/src/techdocs/stages/generate/techdocs.ts
@@ -24,7 +24,9 @@ import {
GeneratorRunOptions,
GeneratorRunResult,
} from './types';
-import { runDockerContainer } from './helpers';
+import { runDockerContainer, runCommand } from './helpers';
+
+const commandExists = require('command-exists-promise');
export class TechdocsGenerator implements GeneratorBase {
private readonly logger: Logger;
@@ -46,17 +48,29 @@ export class TechdocsGenerator implements GeneratorBase {
);
try {
- await runDockerContainer({
- imageName: 'spotify/techdocs',
- args: ['build', '-d', '/result'],
- logStream,
- docsDir: directory,
- resultDir,
- dockerClient,
- });
- this.logger.info(
- `[TechDocs]: Successfully generated docs from ${directory} into ${resultDir}`,
- );
+ const mkdocsInstalled = await commandExists('mkdocs');
+ if (mkdocsInstalled) {
+ await runCommand({
+ command: 'mkdocs',
+ args: ['build', '-d', resultDir, '-v'],
+ options: {
+ cwd: directory,
+ },
+ logStream,
+ });
+ } else {
+ await runDockerContainer({
+ imageName: 'spotify/techdocs',
+ args: ['build', '-d', '/result'],
+ logStream,
+ docsDir: directory,
+ resultDir,
+ dockerClient,
+ });
+ this.logger.info(
+ `[TechDocs]: Successfully generated docs from ${directory} into ${resultDir}`,
+ );
+ }
} catch (error) {
this.logger.debug(
`[TechDocs]: Failed to generate docs from ${directory} into ${resultDir}`,