Merge branch 'master' of github.com:spotify/backstage into mob/union-types
* 'master' of github.com:spotify/backstage: (22 commits) [microsite] Add CNCF logo (#2511) feat: use title for API type in table new tutorials (#2435) e2e-test: ignore unhandled errors from jsdom Techdocs without docker-in-docker (#2438) Test for docker before attempting to run a container (#2497) scaffolder: Fix backend fails to start feat: add api-docs config ref for widgets Mark docs template description as required Send component description to GitHub Pass props to lists and control rendering from outside wrapper Remove commented out code Remove unnecessary boolean conversion docs: improve navigation around ADRs Update default-app template Handle empty sections Change logic of FeatureFlags.toggle() Update sidepar Add featureflags components and toggle method on api Cleanup and breaking out into more components ...
This commit is contained in:
@@ -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
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
<br />
|
||||
|
||||
> 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 (
|
||||
<SignInPage
|
||||
{...props}
|
||||
providers={[
|
||||
{
|
||||
id: 'github-auth-provider',
|
||||
title: 'GitHub',
|
||||
message: 'Simple Backstage Application Login',
|
||||
apiRef: githubAuthApiRef,
|
||||
},
|
||||
]}
|
||||
align="center"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
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).
|
||||
@@ -0,0 +1,487 @@
|
||||
---
|
||||
id: quickstart-app-plugin
|
||||
title: Adding Custom Plugin to Existing Monorepo App
|
||||
---
|
||||
|
||||
###### September 15th 2020 - v0.1.1-alpha.21
|
||||
|
||||
<br />
|
||||
|
||||
> 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';
|
||||
...
|
||||
<SidebarItem icon={GitHubIcon} to="github-playground" text="GitHub Repository" />
|
||||
```
|
||||
|
||||
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
|
||||
<InfoCard title={userId}>
|
||||
<Typography variant="body1">
|
||||
{`${profile.displayName} | ${profile.email}`}
|
||||
</Typography>
|
||||
</InfoCard>
|
||||
```
|
||||
|
||||
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
|
||||
<details><summary>Complete ExampleComponent.tsx</summary>
|
||||
<p>
|
||||
|
||||
```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 (
|
||||
<Page theme={pageTheme.tool}>
|
||||
<Header
|
||||
title="Welcome to github-playground!"
|
||||
subtitle="Optional subtitle"
|
||||
>
|
||||
<HeaderLabel label="Owner" value="Team X" />
|
||||
<HeaderLabel label="Lifecycle" value="Alpha" />
|
||||
</Header>
|
||||
<Content>
|
||||
<ContentHeader title="Plugin title">
|
||||
<SupportButton>A description of your plugin goes here.</SupportButton>
|
||||
</ContentHeader>
|
||||
<Grid container spacing={3} direction="column">
|
||||
<Grid item>
|
||||
<InfoCard title={userId}>
|
||||
<Typography variant="body1">
|
||||
{`${profile.displayName} | ${profile.email}`}
|
||||
</Typography>
|
||||
</InfoCard>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<ExampleFetchComponent />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExampleComponent;
|
||||
```
|
||||
|
||||
</p>
|
||||
</details>
|
||||
|
||||
# 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 <div>Nothing to see yet</div>;
|
||||
};
|
||||
|
||||
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<DenseTableProps> = ({ 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 (
|
||||
<Table
|
||||
title="List Of User's Repositories"
|
||||
options={{ search: false, paging: false }}
|
||||
columns={columns}
|
||||
data={viewer.repositories.nodes}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
# 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<any> => {
|
||||
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 <Progress />;
|
||||
if (error) return <Alert severity="error">{error.message}</Alert>;
|
||||
if (value && value.repositories) return <DenseTable viewer={value} />;
|
||||
```
|
||||
|
||||
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 (
|
||||
<Table
|
||||
title="List Of User's Repositories"
|
||||
options={{ search: false, paging: false }}
|
||||
columns={[]}
|
||||
data={[]}
|
||||
/>
|
||||
);
|
||||
```
|
||||
|
||||
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
|
||||
<details><summary>Complete ExampleFetchComponent.tsx</summary>
|
||||
<p>
|
||||
|
||||
```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<DenseTableProps> = ({ 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 (
|
||||
<Table
|
||||
title="List Of User's Repositories"
|
||||
options={{ search: false, paging: false }}
|
||||
columns={columns}
|
||||
data={viewer.repositories.nodes}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ExampleFetchComponent: FC<{}> = () => {
|
||||
const auth = useApi(githubAuthApiRef);
|
||||
|
||||
const { value, loading, error } = useAsync(async (): Promise<any> => {
|
||||
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 <Progress />;
|
||||
if (error) return <Alert severity="error">{error.message}</Alert>;
|
||||
if (value && value.repositories) return <DenseTable viewer={value} />;
|
||||
|
||||
return (
|
||||
<Table
|
||||
title="List Of User's Repositories"
|
||||
options={{ search: false, paging: false }}
|
||||
columns={[]}
|
||||
data={[]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExampleFetchComponent;
|
||||
```
|
||||
|
||||
</p>
|
||||
</details>
|
||||
|
||||
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](#).
|
||||
@@ -462,6 +462,19 @@ class Index extends React.Component {
|
||||
Contribute
|
||||
</ActionBlock.Link>
|
||||
</ActionBlock>
|
||||
|
||||
<Block small className="bg-black-grey cncf-block">
|
||||
<Block.Container center>
|
||||
<Block.SmallTitle small>
|
||||
Backstage is a{' '}
|
||||
<a href="https://www.cncf.io">
|
||||
Cloud Native Computing Foundation
|
||||
</a>{' '}
|
||||
sandbox project
|
||||
</Block.SmallTitle>
|
||||
<div className="cncf-logo" />
|
||||
</Block.Container>
|
||||
</Block>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" role="img" viewBox="-2.82 -4.07 419.64 80.14"><style>svg {enable-background:new 0 0 414 73}</style><path d="M101.2 32c3.3 0 5.9-1.3 8-3.9l4.2 4.4c-3.4 3.8-7.3 5.7-11.9 5.7-4.6 0-8.3-1.4-11.3-4.3s-4.4-6.5-4.4-10.9 1.5-8.1 4.5-11c3-2.9 6.7-4.4 11.1-4.4 4.9 0 9 1.9 12.2 5.6l-4.1 4.7c-2.1-2.6-4.7-3.9-7.8-3.9-2.5 0-4.6.8-6.4 2.4-1.8 1.6-2.7 3.8-2.7 6.6s.8 5 2.5 6.7c1.7 1.4 3.7 2.3 6.1 2.3zm17.5 5.8V8.1h6.6v23.8H138v5.9zm48.3-4.1c-3 2.9-6.7 4.4-11.1 4.4-4.4 0-8.1-1.5-11.1-4.4-3-2.9-4.5-6.6-4.5-10.9s1.5-8 4.5-10.9c3-2.9 6.7-4.4 11.1-4.4 4.4 0 8.1 1.5 11.1 4.4 3 2.9 4.5 6.6 4.5 10.9s-1.5 8-4.5 10.9zm-2.3-10.9c0-2.6-.9-4.9-2.5-6.8-1.7-1.9-3.8-2.8-6.3-2.8s-4.6.9-6.3 2.8c-1.7 1.9-2.6 4.1-2.6 6.8s.9 4.9 2.6 6.8c1.7 1.9 3.8 2.8 6.3 2.8s4.6-.9 6.3-2.8 2.5-4.1 2.5-6.8zm20.3 7.4c1.1 1.4 2.6 2.1 4.5 2.1s3.4-.7 4.4-2.1c1.1-1.4 1.6-3.3 1.6-5.6V8.1h6.6v16.6c0 4.3-1.2 7.6-3.6 9.9-2.4 2.3-5.4 3.5-9.1 3.5-3.7 0-6.8-1.2-9.2-3.5-2.4-2.3-3.6-5.6-3.6-9.9V8.1h6.6v16.4c.1 2.4.7 4.3 1.8 5.7zM231.7 12c2.8 2.6 4.3 6.2 4.3 10.8s-1.4 8.3-4.1 11c-2.8 2.7-7 4-12.6 4H209V8.1h10.5c5.3 0 9.4 1.3 12.2 3.9zm-4.8 17.7c1.6-1.5 2.4-3.8 2.4-6.7 0-2.9-.8-5.2-2.4-6.7-1.6-1.6-4.1-2.4-7.5-2.4h-3.7V32h4.2c3 0 5.3-.8 7-2.3zm46.8-21.6h6.7v29.7h-6.7l-14.1-18.6v18.6h-6.7V8.1h6.3l14.5 19.2zm35.1 29.7l-2.8-6.4h-12.5l-2.8 6.4h-7.1l12.8-29.7h6.4l12.8 29.7h-6.8zM299.9 17l-3.7 8.6h7.4l-3.7-8.6zm29.9-3.1v23.9h-6.7V13.9h-8.4V8.1h23.5v5.8zM343 8.1h6.6v29.7H343zM367.9 27l7.6-18.9h7.2l-12 29.7h-5.6L353.2 8.1h7.2zm39.7-18.9V14h-14.8v6.2h13.3v5.6h-13.3V32h15.3v5.8h-21.9V8.1zm-314 55c1.8 0 3.3-.7 4.5-2.2l2.4 2.5c-1.9 2.1-4.1 3.2-6.7 3.2s-4.7-.8-6.3-2.4c-1.7-1.6-2.5-3.7-2.5-6.1 0-2.5.8-4.5 2.5-6.2s3.8-2.5 6.2-2.5c2.7 0 5 1 6.9 3.1l-2.3 2.6c-1.2-1.5-2.6-2.2-4.4-2.2-1.4 0-2.6.5-3.6 1.4-1 .9-1.5 2.1-1.5 3.7s.5 2.8 1.4 3.7c.9 1 2.1 1.4 3.4 1.4zm23.4 1c-1.7 1.6-3.8 2.5-6.2 2.5s-4.5-.8-6.2-2.5c-1.7-1.6-2.5-3.7-2.5-6.1s.8-4.5 2.5-6.1c1.7-1.6 3.8-2.5 6.2-2.5s4.5.8 6.2 2.5c1.7 1.6 2.5 3.7 2.5 6.1s-.8 4.5-2.5 6.1zm-1.2-6.1c0-1.5-.5-2.8-1.4-3.8-1-1-2.1-1.6-3.5-1.6s-2.6.5-3.5 1.6c-1 1-1.4 2.3-1.4 3.8s.5 2.7 1.4 3.8c1 1 2.1 1.6 3.5 1.6s2.6-.5 3.5-1.6c.9-1 1.4-2.3 1.4-3.8zm21.8-2.1l-4.5 9.2h-2.2l-4.5-9.2v10.5h-3.7V49.8h5l4.3 9.1 4.3-9.1h5v16.6h-3.7zm19.3-4.6c1.2 1 1.8 2.5 1.8 4.6s-.6 3.6-1.8 4.5c-1.2 1-3 1.4-5.5 1.4h-2.2v4.6h-3.7V49.8h5.9c2.5 0 4.3.5 5.5 1.5zm-2.7 6.5c.4-.5.7-1.2.7-2.2s-.3-1.6-.9-2c-.6-.4-1.5-.6-2.7-.6h-2.1v5.6h2.5c1.2 0 2-.3 2.5-.8zm11.8 4.3c.6.8 1.5 1.2 2.5 1.2s1.9-.4 2.5-1.2c.6-.8.9-1.8.9-3.2v-9.2h3.7V59c0 2.4-.7 4.3-2 5.6-1.3 1.3-3 1.9-5.1 1.9s-3.8-.7-5.1-2-2-3.2-2-5.6v-9.3h3.7V59c0 1.3.3 2.4.9 3.1zm20.5-9.1v13.4h-3.7V53h-4.7v-3.2h13.1V53zm7.5-3.2h3.7v16.7H194zm19.4 0h3.7v16.6h-3.7L205.5 56v10.4h-3.7V49.8h3.4l8.2 10.7zm18.7 8.2h3.7v5.9c-1.7 1.8-3.9 2.8-6.9 2.8-2.4 0-4.5-.8-6.2-2.4-1.7-1.6-2.5-3.7-2.5-6.1 0-2.5.8-4.5 2.5-6.2s3.7-2.5 6.1-2.5c2.4 0 4.5.8 6.2 2.4l-1.9 2.8c-.7-.7-1.4-1.1-2.1-1.3-.6-.2-1.3-.4-2-.4-1.4 0-2.6.5-3.6 1.4-1 1-1.5 2.2-1.5 3.8 0 1.6.5 2.8 1.4 3.8.9.9 2 1.4 3.3 1.4 1.3 0 2.4-.2 3.2-.7l.3-4.7zm24.7-8.2V53h-7.7v3.6h7.4v3.3h-7.4v6.5h-3.7V49.8zM274 64.1c-1.7 1.6-3.8 2.5-6.2 2.5-2.5 0-4.5-.8-6.2-2.5-1.7-1.6-2.5-3.7-2.5-6.1s.8-4.5 2.5-6.1c1.7-1.6 3.8-2.5 6.2-2.5 2.5 0 4.5.8 6.2 2.5 1.7 1.6 2.5 3.7 2.5 6.1s-.8 4.5-2.5 6.1zm-1.3-6.1c0-1.5-.5-2.8-1.4-3.8-1-1-2.1-1.6-3.5-1.6s-2.6.5-3.5 1.6c-1 1-1.4 2.3-1.4 3.8s.5 2.7 1.4 3.8c1 1 2.1 1.6 3.5 1.6s2.6-.5 3.5-1.6c1-1 1.4-2.3 1.4-3.8zm11.4 4.1c.6.8 1.5 1.2 2.5 1.2s1.9-.4 2.5-1.2c.6-.8.9-1.8.9-3.2v-9.2h3.7V59c0 2.4-.7 4.3-2 5.6-1.3 1.3-3 1.9-5.1 1.9s-3.8-.7-5.1-2c-1.3-1.3-2-3.2-2-5.6v-9.3h3.7V59c-.1 1.3.3 2.4.9 3.1zm25.1-12.3h3.7v16.6h-3.7L301.3 56v10.4h-3.7V49.8h3.4l8.2 10.7zm20.5 2.2c1.6 1.5 2.4 3.5 2.4 6.1 0 2.6-.8 4.6-2.3 6.1-1.5 1.5-3.9 2.3-7.1 2.3H317V49.8h5.9c3 0 5.3.7 6.8 2.2zm-2.7 9.9c.9-.9 1.4-2.1 1.4-3.7s-.5-2.9-1.4-3.8c-.9-.9-2.3-1.3-4.2-1.3h-2.1v10.1h2.4c1.7-.1 3-.5 3.9-1.3zm19.7 4.5l-1.5-3.6h-7l-1.5 3.6h-4l7.2-16.7h3.6l7.2 16.7h-4zm-5-11.7l-2.1 4.8h4.2l-2.1-4.8zm16.8-1.7v13.4h-3.8V53H350v-3.2h13.2V53zm7.4-3.2h3.7v16.7h-3.7zm21.8 14.3c-1.7 1.6-3.8 2.5-6.2 2.5-2.5 0-4.5-.8-6.2-2.5-1.7-1.6-2.5-3.7-2.5-6.1s.8-4.5 2.5-6.1c1.7-1.6 3.8-2.5 6.2-2.5 2.5 0 4.5.8 6.2 2.5 1.7 1.6 2.5 3.7 2.5 6.1s-.8 4.5-2.5 6.1zm-1.2-6.1c0-1.5-.5-2.8-1.4-3.8-1-1-2.1-1.6-3.5-1.6s-2.6.5-3.5 1.6c-1 1-1.4 2.3-1.4 3.8s.5 2.7 1.4 3.8c1 1 2.1 1.6 3.5 1.6s2.6-.5 3.5-1.6c.9-1 1.4-2.3 1.4-3.8zm18.5-8.2h3.8v16.6H405L397.1 56v10.4h-3.7V49.8h3.5l8.1 10.7z"/><path fill="#446ca9" d="M14.5 46.7H5.4v21.4h21.3v-9.2H14.5zm45.8.1v12.1H48.1v-.1 9.3h21.3V46.7h-9.2zM5.4 25.4h9.2l-.1-.1V13.2h12.2V4H5.4zM48.1 4v9.2h12.2v12.2h9.1V4z"/><path fill="#76c4d5" d="M46.9 25.4L34.7 13.2h13.4V4H26.7v9.2l12.2 12.2zm-11 21.3h-8L38 56.8l2 2.1H26.7v9.2h21.4v-9.3l-6.1-6zm24.4-21.3v13.3l-2.1-2.1-10.1-10.1v8.1l6 6 6.1 6.1h9.2V25.4zM26.7 37.5L14.6 25.4H5.4v21.3h9.1V33.4l12.2 12.2z"/></svg>
|
||||
|
After Width: | Height: | Size: 4.7 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 6.3 KiB |
@@ -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 }) => (
|
||||
/>
|
||||
<SidebarSpace />
|
||||
<SidebarDivider />
|
||||
<SidebarThemeToggle />
|
||||
<SidebarUserSettings providerSettings={<DefaultProviderSettings />} />
|
||||
<SidebarPinButton />
|
||||
</Sidebar>
|
||||
{children}
|
||||
</SidebarPage>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -80,6 +80,15 @@ export class UserFlags extends Map<FeatureFlagName, FeatureFlagState> {
|
||||
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();
|
||||
|
||||
@@ -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<BackstageTheme, { isPinned: boolean }>(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 (
|
||||
<div className={classes.root}>
|
||||
{isOpen && (
|
||||
<button
|
||||
className={classes.arrowButtonWrapper}
|
||||
onClick={toggleSidebarPinState}
|
||||
>
|
||||
<DoubleArrowIcon
|
||||
className={classes.arrowButtonIcon}
|
||||
style={{ fontSize: 14 }}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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 = () => (
|
||||
<List dense subheader={<ListSubheader>App Settings</ListSubheader>}>
|
||||
<SidebarThemeToggle />
|
||||
<SidebarPinButton />
|
||||
</List>
|
||||
);
|
||||
@@ -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) => (
|
||||
<List subheader={<ListSubheader>Available Auth Providers</ListSubheader>}>
|
||||
{providerSettings}
|
||||
</List>
|
||||
);
|
||||
@@ -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 (
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={featureFlag.name}
|
||||
secondary={`Registered in ${featureFlag.pluginId} plugin`}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<ToggleButton
|
||||
size="small"
|
||||
value="flag"
|
||||
selected={enabled}
|
||||
onChange={toggleFlag}
|
||||
>
|
||||
<Tooltip placement="top" arrow title={enabled ? 'Disable' : 'Enable'}>
|
||||
<CheckIcon />
|
||||
</Tooltip>
|
||||
</ToggleButton>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
@@ -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) => (
|
||||
<List dense subheader={<ListSubheader>Feature Flags</ListSubheader>}>
|
||||
{featureFlags.map(featureFlag => (
|
||||
<FlagItem key={featureFlag.name} featureFlag={featureFlag} />
|
||||
))}
|
||||
</List>
|
||||
);
|
||||
@@ -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 = () => (
|
||||
<Tooltip
|
||||
placement="top"
|
||||
arrow
|
||||
title={`${isPinned ? 'Unpin' : 'Pin'} Sidebar`}
|
||||
>
|
||||
{isPinned ? <LockIcon /> : <LockOpenIcon />}
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
return (
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary="Pin Sidebar"
|
||||
secondary="Prevent the sidebar from collapsing"
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<ToggleButton
|
||||
size="small"
|
||||
value="pin"
|
||||
selected={isPinned}
|
||||
onChange={() => {
|
||||
toggleSidebarPinState();
|
||||
}}
|
||||
>
|
||||
<PinIcon />
|
||||
</ToggleButton>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
@@ -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 (
|
||||
<SidebarItem key={title} text={title} icon={icon ?? StarBorder}>
|
||||
<IconButton onClick={() => (signedIn ? api.logout() : signInHandler())}>
|
||||
};
|
||||
|
||||
export const ProviderSettingsItem = ({
|
||||
title,
|
||||
icon: Icon,
|
||||
signedIn,
|
||||
api,
|
||||
signInHandler,
|
||||
}: Props) => (
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Icon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={title} />
|
||||
<ListItemSecondaryAction>
|
||||
<ToggleButton
|
||||
size="small"
|
||||
value={title}
|
||||
selected={signedIn}
|
||||
onChange={() => (signedIn ? api.logout() : signInHandler())}
|
||||
>
|
||||
<Tooltip
|
||||
placement="top"
|
||||
arrow
|
||||
title={signedIn ? `Sign out from ${title}` : `Sign in to ${title}`}
|
||||
>
|
||||
<PowerButton color={signedIn ? 'secondary' : 'primary'} />
|
||||
<PowerButton />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
</SidebarItem>
|
||||
);
|
||||
};
|
||||
</ToggleButton>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<Card className={classes.root}>
|
||||
<CardHeader
|
||||
avatar={<SignInAvatar size={48} />}
|
||||
action={<UserSettingsMenu />}
|
||||
title={displayName}
|
||||
subheader={profile.email}
|
||||
/>
|
||||
<CardContent>
|
||||
<AppSettingsList />
|
||||
{providerSettings && (
|
||||
<>
|
||||
<Divider />
|
||||
<AuthProvidersList providerSettings={providerSettings} />
|
||||
</>
|
||||
)}
|
||||
{featureFlags.length > 0 && (
|
||||
<>
|
||||
<Divider />
|
||||
<FeatureFlagsList featureFlags={featureFlags} />
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -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<BackstageTheme, { size: number }>({
|
||||
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 (
|
||||
<Avatar src={profile.picture} className={classes.avatar}>
|
||||
{displayName[0]}
|
||||
</Avatar>
|
||||
);
|
||||
};
|
||||
@@ -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: <DarkIcon />,
|
||||
light: <LightIcon />,
|
||||
};
|
||||
|
||||
const handleSetTheme = (
|
||||
_event: React.MouseEvent<HTMLElement>,
|
||||
newThemeId: string | undefined,
|
||||
) => {
|
||||
if (themeIds.some(t => t.id === newThemeId)) {
|
||||
appThemeApi.setActiveThemeId(newThemeId);
|
||||
} else {
|
||||
appThemeApi.setActiveThemeId(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ListItem>
|
||||
<ListItemText primary="Theme" secondary="Change the theme mode" />
|
||||
<ListItemSecondaryAction>
|
||||
<ToggleButtonGroup
|
||||
exclusive
|
||||
size="small"
|
||||
value={themeId ?? 'auto'}
|
||||
onChange={handleSetTheme}
|
||||
>
|
||||
{themeIds.map(theme => (
|
||||
<ToggleButton key={theme.id} value={theme.variant}>
|
||||
<Tooltip
|
||||
placement="top"
|
||||
arrow
|
||||
title={`Select ${theme.variant} theme`}
|
||||
>
|
||||
{themeIcons[theme.variant]}
|
||||
</Tooltip>
|
||||
</ToggleButton>
|
||||
))}
|
||||
<ToggleButton value="auto">
|
||||
<Tooltip placement="top" arrow title="Select auto theme">
|
||||
<AutoIcon />
|
||||
</Tooltip>
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
@@ -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<Element>(); // 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 = () => (
|
||||
<Avatar src={profile.picture} className={classes.avatar}>
|
||||
{displayName[0]}
|
||||
</Avatar>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Divider innerRef={ref} />
|
||||
<SidebarItem text={displayName} onClick={handleClick} icon={SignInAvatar}>
|
||||
{open ? <ExpandMore /> : <ExpandLess />}
|
||||
</SidebarItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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<HTMLButtonElement | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
const handleOpen = (event?: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event?.currentTarget ?? undefined);
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(undefined);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!sidebarOpen && open) setOpen(false);
|
||||
}, [open, sidebarOpen]);
|
||||
|
||||
const SidebarAvatar = () => <SignInAvatar />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidebarItem
|
||||
text={displayName}
|
||||
onClick={handleOpen}
|
||||
icon={SidebarAvatar}
|
||||
/>
|
||||
<Popover
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={{
|
||||
vertical: 'center',
|
||||
horizontal: 'center',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
>
|
||||
<SettingsDialog providerSettings={providerSettings} />
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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 | HTMLElement>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
const handleOpen = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(undefined);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton onClick={handleOpen}>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
<Menu anchorEl={anchorEl} open={open} onClose={handleClose}>
|
||||
<MenuItem onClick={() => identityApi.logout()}>
|
||||
<ListItemIcon>
|
||||
<SignOutIcon />
|
||||
</ListItemIcon>
|
||||
Sign Out
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -17,4 +17,4 @@
|
||||
export { ProviderSettingsItem } from './ProviderSettingsItem';
|
||||
export { OAuthProviderSettings } from './OAuthProviderSettings';
|
||||
export { OIDCProviderSettings } from './OIDCProviderSettings';
|
||||
export { UserProfile } from './UserProfile';
|
||||
export { SidebarUserSettings } from './UserSettings';
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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 <SidebarItem text={text} onClick={handleToggle} icon={icon} />;
|
||||
};
|
||||
@@ -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 (
|
||||
<>
|
||||
<SidebarUserProfile open={open} setOpen={setOpen} />
|
||||
<Collapse in={open} timeout="auto">
|
||||
{providerSettings}
|
||||
|
||||
<SidebarItem
|
||||
icon={SignOutIcon}
|
||||
text="Sign Out"
|
||||
onClick={() => identityApi.logout()}
|
||||
/>
|
||||
</Collapse>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -18,8 +18,6 @@ import {
|
||||
SidebarContext,
|
||||
SidebarSpace,
|
||||
SidebarUserSettings,
|
||||
SidebarThemeToggle,
|
||||
SidebarPinButton,
|
||||
DefaultProviderSettings,
|
||||
} from '@backstage/core';
|
||||
|
||||
@@ -39,9 +37,7 @@ export const AppSidebar = () => (
|
||||
<SidebarDivider />
|
||||
<SidebarSpace />
|
||||
<SidebarDivider />
|
||||
<SidebarThemeToggle />
|
||||
<SidebarUserSettings providerSettings={<DefaultProviderSettings />} />
|
||||
<SidebarPinButton />
|
||||
</Sidebar>
|
||||
);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 <Alert severity="error">Could not fetch the API</Alert>;
|
||||
}
|
||||
|
||||
const definitionWidget = definitionWidgets.find(
|
||||
d => d.type === apiEntity.spec.type,
|
||||
);
|
||||
const definitionWidget = getApiDefinitionWidget(apiEntity);
|
||||
|
||||
if (definitionWidget) {
|
||||
return (
|
||||
<TabbedCard title={apiEntity.metadata.name}>
|
||||
<CardTab label={definitionWidget.title}>
|
||||
<CardTab label={definitionWidget.title} key="widget">
|
||||
{definitionWidget.component(apiEntity.spec.definition)}
|
||||
</CardTab>
|
||||
<CardTab label="Raw">
|
||||
<CardTab label="Raw" key="raw">
|
||||
<PlainApiDefinitionWidget
|
||||
definition={apiEntity.spec.definition}
|
||||
language={definitionWidget.rawLanguage || apiEntity.spec.type}
|
||||
@@ -103,7 +95,7 @@ export const ApiDefinitionCard = (props: Props) => {
|
||||
title={apiEntity.metadata.name}
|
||||
children={[
|
||||
// Has to be an array, otherwise typescript doesn't like that this has only a single child
|
||||
<CardTab label={apiEntity.spec.type}>
|
||||
<CardTab label={apiEntity.spec.type} key="raw">
|
||||
<PlainApiDefinitionWidget
|
||||
definition={apiEntity.spec.definition}
|
||||
language={apiEntity.spec.type}
|
||||
|
||||
@@ -14,4 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { ApiDefinitionCard } from './ApiDefinitionCard';
|
||||
export type { ApiDefinitionWidget } from './ApiDefinitionCard';
|
||||
export {
|
||||
ApiDefinitionCard,
|
||||
defaultDefinitionWidgets,
|
||||
} from './ApiDefinitionCard';
|
||||
|
||||
@@ -20,6 +20,7 @@ import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog';
|
||||
import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils';
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { apiDocsConfigRef } from '../../config';
|
||||
import { ApiExplorerPage } from './ApiExplorerPage';
|
||||
|
||||
describe('ApiCatalogPage', () => {
|
||||
@@ -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}
|
||||
|
||||
@@ -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(
|
||||
<ApiExplorerTable
|
||||
titlePreamble="APIs"
|
||||
entities={[]}
|
||||
loading={false}
|
||||
error={{ code: 'error' }}
|
||||
/>,
|
||||
<ApiProvider apis={apiRegistry}>
|
||||
<ApiExplorerTable
|
||||
titlePreamble="APIs"
|
||||
entities={[]}
|
||||
loading={false}
|
||||
error={{ code: 'error' }}
|
||||
/>
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
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(
|
||||
<ApiExplorerTable
|
||||
titlePreamble="APIs"
|
||||
entities={entites}
|
||||
loading={false}
|
||||
/>,
|
||||
<ApiProvider apis={apiRegistry}>
|
||||
<ApiExplorerTable
|
||||
titlePreamble="APIs"
|
||||
entities={entites}
|
||||
loading={false}
|
||||
/>
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
expect(rendered.getByText(/APIs \(3\)/)).toBeInTheDocument();
|
||||
|
||||
@@ -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 <span>{type}</span>;
|
||||
};
|
||||
|
||||
const columns: TableColumn<Entity>[] = [
|
||||
{
|
||||
title: 'Name',
|
||||
@@ -54,8 +63,11 @@ const columns: TableColumn<Entity>[] = [
|
||||
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) => (
|
||||
<ApiTypeTitle apiEntity={entity as ApiEntityV1alpha1} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Description',
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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<ApiDocsConfig>({
|
||||
id: 'plugin.api-docs.config',
|
||||
description: 'Used to configure api-docs widgets',
|
||||
});
|
||||
|
||||
export interface ApiDocsConfig {
|
||||
getApiDefinitionWidget: (
|
||||
apiEntity: ApiEntity,
|
||||
) => ApiDefinitionWidget | undefined;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
@@ -17,6 +17,7 @@ spec:
|
||||
schema:
|
||||
required:
|
||||
- component_id
|
||||
- description
|
||||
properties:
|
||||
component_id:
|
||||
title: Name
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -61,6 +61,7 @@ export class GithubPublisher implements PublisherBase {
|
||||
values: RequiredTemplateValues & Record<string, JsonValue>,
|
||||
) {
|
||||
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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}`));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
Reference in New Issue
Block a user