Merge branch 'master' of github.com:spotify/backstage into shmidt-i/proxy-plugin

This commit is contained in:
Ivan Shmidt
2020-07-13 13:38:57 +02:00
47 changed files with 711 additions and 286 deletions
+2
View File
@@ -21,7 +21,9 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/catalog-model": "^0.1.1-alpha.13",
"@backstage/core": "^0.1.1-alpha.13",
"@backstage/core-api": "^0.1.1-alpha.13",
"@backstage/theme": "^0.1.1-alpha.13",
"@material-ui/core": "^4.9.1",
"@material-ui/icons": "^4.9.1",
@@ -14,25 +14,23 @@
* limitations under the License.
*/
export enum BuildStatus {
Null,
Success,
Failure,
Pending,
Running,
}
import { createApiRef } from '@backstage/core';
import { Build, BuildDetails } from './types';
export type Build = {
commitId: string;
message: string;
branch: string;
status: BuildStatus;
uri: string;
};
export const githubActionsApiRef = createApiRef<GithubActionsApi>({
id: 'plugin.githubactions.service',
description: 'Used by the Github Actions plugin to make requests',
});
export type BuildDetails = {
build: Build;
author: string;
logUrl: string;
overviewUrl: string;
export type GithubActionsApi = {
listBuilds: ({
owner,
repo,
token,
}: {
owner: string;
repo: string;
token: string;
}) => Promise<Build[]>;
getBuild: (buildUri: string, token: Promise<string>) => Promise<BuildDetails>;
};
@@ -0,0 +1,131 @@
/*
* 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 { GithubActionsApi } from './GithubActionsApi';
import { Build, BuildDetails, BuildStatus, WorkflowRun } from './types';
const statusToBuildStatus: { [status: string]: BuildStatus } = {
success: BuildStatus.Success,
failure: BuildStatus.Failure,
pending: BuildStatus.Pending,
running: BuildStatus.Running,
in_progress: BuildStatus.Running,
completed: BuildStatus.Success,
};
const conclusionToStatus = (conslusion: string): BuildStatus =>
statusToBuildStatus[conslusion] ?? BuildStatus.Null;
export class GithubActionsClient implements GithubActionsApi {
async listBuilds({
owner,
repo,
token,
}: {
owner: string;
repo: string;
token: string;
}): Promise<Build[]> {
const url = `https://api.github.com/repos/${owner}/${repo}/actions/runs`;
const response = await fetch(url, {
headers: new Headers({
Authorization: `Bearer ${token}`,
}),
});
if (!response.ok) {
return [
{
commitId: 'Error',
message: 'Response status is not OK',
branch: 'Error',
status: BuildStatus.Failure,
uri: 'Error',
},
];
}
const data = await response.json();
const newData: WorkflowRun[] = data.workflow_runs;
const endData: Build[] = [];
newData.forEach((element, index) => {
const transData: Build = {
commitId: '',
message: '',
branch: '',
status: BuildStatus.Null,
uri: '',
};
transData.commitId = String(element.head_commit.id);
transData.branch = element.head_branch;
transData.status = conclusionToStatus(element.conclusion);
transData.message = element.head_commit.message;
transData.uri = element.url;
endData[index] = transData;
});
return endData;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async getBuild(
buildUri: string,
token: Promise<string>,
): Promise<BuildDetails> {
const response = await fetch(buildUri, {
headers: new Headers({
Authorization: `Bearer ${await token}`,
}),
});
const buildBlank: Build = {
commitId: '',
message: '',
branch: '',
status: BuildStatus.Null,
uri: '',
};
const dataBlank: BuildDetails = {
build: buildBlank,
author: '',
logUrl: '',
overviewUrl: '',
};
if (!response.ok) {
return dataBlank;
}
const data = await response.json();
const newData: WorkflowRun = data;
dataBlank.author = newData.head_commit.author.name;
dataBlank.build.branch = newData.head_branch;
dataBlank.build.commitId = newData.head_commit.id;
dataBlank.build.message = newData.head_commit.message;
dataBlank.build.status = conclusionToStatus(newData.status);
dataBlank.build.uri = newData.url;
dataBlank.logUrl = newData.logs_url;
dataBlank.overviewUrl = newData.html_url;
return dataBlank;
}
}
@@ -14,5 +14,6 @@
* limitations under the License.
*/
export { BuildsClient } from './BuildsClient';
export * from './GithubActionsApi';
export * from './GithubActionsClient';
export * from './types';
+224
View File
@@ -0,0 +1,224 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export enum BuildStatus {
Null,
Success,
Failure,
Pending,
Running,
}
export type Build = {
commitId: string;
message: string;
branch: string;
status: BuildStatus;
uri: string;
};
export type BuildDetails = {
build: Build;
author: string;
logUrl: string;
overviewUrl: string;
};
export interface Author {
name: string;
email: string;
}
export interface Committer {
name: string;
email: string;
}
export interface HeadCommit {
id: string;
tree_id: string;
message: string;
timestamp: Date;
author: Author;
committer: Committer;
}
export interface Owner {
login: string;
id: number;
node_id: string;
avatar_url: string;
gravatar_id: string;
url: string;
html_url: string;
followers_url: string;
following_url: string;
gists_url: string;
starred_url: string;
subscriptions_url: string;
organizations_url: string;
repos_url: string;
events_url: string;
received_events_url: string;
type: string;
site_admin: boolean;
}
export interface Repository {
id: number;
node_id: string;
name: string;
full_name: string;
private: boolean;
owner: Owner;
html_url: string;
description?: any;
fork: boolean;
url: string;
forks_url: string;
keys_url: string;
collaborators_url: string;
teams_url: string;
hooks_url: string;
issue_events_url: string;
events_url: string;
assignees_url: string;
branches_url: string;
tags_url: string;
blobs_url: string;
git_tags_url: string;
git_refs_url: string;
trees_url: string;
statuses_url: string;
languages_url: string;
stargazers_url: string;
contributors_url: string;
subscribers_url: string;
subscription_url: string;
commits_url: string;
git_commits_url: string;
comments_url: string;
issue_comment_url: string;
contents_url: string;
compare_url: string;
merges_url: string;
archive_url: string;
downloads_url: string;
issues_url: string;
pulls_url: string;
milestones_url: string;
notifications_url: string;
labels_url: string;
releases_url: string;
deployments_url: string;
}
export interface Owner2 {
login: string;
id: number;
node_id: string;
avatar_url: string;
gravatar_id: string;
url: string;
html_url: string;
followers_url: string;
following_url: string;
gists_url: string;
starred_url: string;
subscriptions_url: string;
organizations_url: string;
repos_url: string;
events_url: string;
received_events_url: string;
type: string;
site_admin: boolean;
}
export interface HeadRepository {
id: number;
node_id: string;
name: string;
full_name: string;
private: boolean;
owner: Owner2;
html_url: string;
description?: any;
fork: boolean;
url: string;
forks_url: string;
keys_url: string;
collaborators_url: string;
teams_url: string;
hooks_url: string;
issue_events_url: string;
events_url: string;
assignees_url: string;
branches_url: string;
tags_url: string;
blobs_url: string;
git_tags_url: string;
git_refs_url: string;
trees_url: string;
statuses_url: string;
languages_url: string;
stargazers_url: string;
contributors_url: string;
subscribers_url: string;
subscription_url: string;
commits_url: string;
git_commits_url: string;
comments_url: string;
issue_comment_url: string;
contents_url: string;
compare_url: string;
merges_url: string;
archive_url: string;
downloads_url: string;
issues_url: string;
pulls_url: string;
milestones_url: string;
notifications_url: string;
labels_url: string;
releases_url: string;
deployments_url: string;
}
export interface WorkflowRun {
id: number;
node_id: string;
head_branch: string;
head_sha: string;
run_number: number;
event: string;
status: string;
conclusion: string;
workflow_id: number;
url: string;
html_url: string;
pull_requests: any[];
created_at: Date;
updated_at: Date;
jobs_url: string;
logs_url: string;
check_suite_url: string;
artifacts_url: string;
cancel_url: string;
rerun_url: string;
workflow_url: string;
head_commit: HeadCommit;
repository: Repository;
head_repository: HeadRepository;
}
@@ -1,44 +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 { Build, BuildDetails, BuildStatus } from './types';
export class BuildsClient {
static create(): BuildsClient {
return new BuildsClient();
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async listBuilds(_entityUri: string): Promise<Build[]> {
return [];
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async getBuild(_buildUri: string): Promise<BuildDetails> {
return {
build: {
commitId: 'TODO',
branch: 'TODO',
uri: 'TODO',
status: BuildStatus.Running,
message: 'TODO',
},
author: 'TODO',
logUrl: 'TODO',
overviewUrl: 'TODO',
};
}
}
@@ -14,7 +14,6 @@
* limitations under the License.
*/
import { Link } from '@backstage/core';
import {
Button,
ButtonGroup,
@@ -30,10 +29,11 @@ import {
Typography,
} from '@material-ui/core';
import React from 'react';
import { useParams } from 'react-router-dom';
import { useLocation } from 'react-router-dom';
import { useAsync } from 'react-use';
import { BuildsClient } from '../../apis/builds';
import { BuildStatusIndicator } from '../BuildStatusIndicator';
import { Link, useApi, githubAuthApiRef } from '@backstage/core';
import { githubActionsApiRef } from '../../api';
const useStyles = makeStyles<Theme>(theme => ({
root: {
@@ -48,12 +48,18 @@ const useStyles = makeStyles<Theme>(theme => ({
},
}));
const client = BuildsClient.create();
export const BuildDetailsPage = () => {
const api = useApi(githubActionsApiRef);
const githubApi = useApi(githubAuthApiRef);
const token = githubApi.getAccessToken('repo');
const classes = useStyles();
const { buildUri } = useParams();
const status = useAsync(() => client.getBuild(buildUri), [buildUri]);
const location = useLocation();
const status = useAsync(
() =>
api.getBuild(decodeURIComponent(location.search.split('uri=')[1]), token),
[location.search],
);
if (status.loading) {
return <LinearProgress />;
@@ -70,7 +76,7 @@ export const BuildDetailsPage = () => {
return (
<div className={classes.root}>
<Typography className={classes.title} variant="h3">
<Link to="/builds">
<Link to="/github-actions">
<Typography component="span" variant="h3" color="primary">
&lt;
</Typography>
@@ -124,12 +130,12 @@ export const BuildDetailsPage = () => {
>
{details?.overviewUrl && (
<Button>
<Link to={details.overviewUrl}>GitHub</Link>
<a href={details.overviewUrl}>GitHub</a>
</Button>
)}
{details?.logUrl && (
<Button>
<Link to={details.logUrl}>Logs</Link>
<a href={details.logUrl}>Logs</a>
</Button>
)}
</ButtonGroup>
@@ -14,7 +14,6 @@
* limitations under the License.
*/
import { Link } from '@backstage/core';
import {
LinearProgress,
makeStyles,
@@ -27,10 +26,9 @@ import {
} from '@material-ui/core';
import React from 'react';
import { useAsync } from 'react-use';
import { BuildsClient } from '../../apis/builds';
import { BuildStatusIndicator } from '../BuildStatusIndicator';
const client = BuildsClient.create();
import { githubActionsApiRef } from '../../api';
import { Link, useApi, githubAuthApiRef } from '@backstage/core';
const useStyles = makeStyles<Theme>(theme => ({
root: {
@@ -41,62 +39,69 @@ const useStyles = makeStyles<Theme>(theme => ({
},
}));
export const BuildInfoCard = () => {
const classes = useStyles();
const status = useAsync(() => client.listBuilds('entity:spotify:backstage'));
const BuildInfoCardContent = () => {
const api = useApi(githubActionsApiRef);
const githubApi = useApi(githubAuthApiRef);
let content: JSX.Element;
const status = useAsync(async () => {
const token = await githubApi.getAccessToken('repo');
return api.listBuilds({ owner: 'spotify', repo: 'backstage', token });
});
if (status.loading) {
content = <LinearProgress />;
return <LinearProgress />;
} else if (status.error) {
content = (
return (
<Typography variant="h2" color="error">
Failed to load builds, {status.error.message}
</Typography>
);
} else {
const [build] =
status.value?.filter(({ branch }) => branch === 'master') ?? [];
content = (
<Table>
<TableBody>
<TableRow>
<TableCell>
<Typography noWrap>Message</Typography>
</TableCell>
<TableCell>
<Link to={`builds/${encodeURIComponent(build?.uri || '')}`}>
<Typography color="primary">{build?.message}</Typography>
</Link>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Typography noWrap>Commit ID</Typography>
</TableCell>
<TableCell>{build?.commitId}</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Typography noWrap>Status</Typography>
</TableCell>
<TableCell>
<BuildStatusIndicator status={build?.status} />
</TableCell>
</TableRow>
</TableBody>
</Table>
);
}
const [build] =
status.value?.filter(({ branch }) => branch === 'master') ?? [];
return (
<Table>
<TableBody>
<TableRow>
<TableCell>
<Typography noWrap>Message</Typography>
</TableCell>
<TableCell>
<Link to={`builds/${encodeURIComponent(build?.uri || '')}`}>
<Typography color="primary">{build?.message}</Typography>
</Link>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Typography noWrap>Commit ID</Typography>
</TableCell>
<TableCell>{build?.commitId}</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Typography noWrap>Status</Typography>
</TableCell>
<TableCell>
<BuildStatusIndicator status={build?.status} />
</TableCell>
</TableRow>
</TableBody>
</Table>
);
};
export const BuildInfoCard = () => {
const classes = useStyles();
return (
<div className={classes.root}>
<Typography variant="h2" className={classes.title}>
Master Build
</Typography>
{content}
<BuildInfoCardContent />
</div>
);
};
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { Link } from '@backstage/core';
import { Link, useApi, githubAuthApiRef } from '@backstage/core';
import {
LinearProgress,
makeStyles,
@@ -31,10 +31,8 @@ import {
} from '@material-ui/core';
import React from 'react';
import { useAsync } from 'react-use';
import { BuildsClient } from '../../apis/builds';
import { BuildStatusIndicator } from '../BuildStatusIndicator';
const client = BuildsClient.create();
import { githubActionsApiRef, Build } from '../../api';
const LongText = ({ text, max }: { text: string; max: number }) => {
if (text.length < max) {
@@ -56,10 +54,15 @@ const useStyles = makeStyles<Theme>(theme => ({
},
}));
const PageContents = () => {
const { loading, error, value } = useAsync(() =>
client.listBuilds('entity:spotify:backstage'),
);
const PageContents = ({ owner, repo }: { owner: string; repo: string }) => {
const api = useApi(githubActionsApiRef);
const githubApi = useApi(githubAuthApiRef);
const { loading, error, value } = useAsync(async () => {
const token = await githubApi.getAccessToken('repo');
return api.listBuilds({ owner, repo, token });
}, [githubApi, owner, repo]);
if (loading) {
return <LinearProgress />;
@@ -85,7 +88,7 @@ const PageContents = () => {
</TableRow>
</TableHead>
<TableBody>
{value!.map(build => (
{value?.map((build: Build) => (
<TableRow key={build.uri}>
<TableCell>
<BuildStatusIndicator status={build.status} />
@@ -96,7 +99,7 @@ const PageContents = () => {
</Typography>
</TableCell>
<TableCell>
<Link to={`builds/${encodeURIComponent(build.uri)}`}>
<Link to={`builds?uri=${encodeURIComponent(build.uri)}`}>
<Typography color="primary">
<LongText text={build.message} max={60} />
</Typography>
@@ -117,12 +120,13 @@ const PageContents = () => {
export const BuildListPage = () => {
const classes = useStyles();
return (
<div className={classes.root}>
<Typography variant="h3" className={classes.title}>
CI/CD Builds
</Typography>
<PageContents />
<PageContents owner="spotify" repo="backstage" />
</div>
);
};
@@ -21,7 +21,7 @@ import SuccessIcon from '@material-ui/icons/CheckCircle';
import FailureIcon from '@material-ui/icons/Error';
import UnknownIcon from '@material-ui/icons/Help';
import React from 'react';
import { BuildStatus } from '../../apis/builds';
import { BuildStatus } from '../../api/types';
type Props = {
status?: BuildStatus;
+1
View File
@@ -15,3 +15,4 @@
*/
export { plugin } from './plugin';
export * from './api';
+1 -1
View File
@@ -24,7 +24,7 @@ export const rootRouteRef = createRouteRef({
title: 'GitHub Actions',
});
export const buildRouteRef = createRouteRef({
path: '/github-actions/builds/:buildUri',
path: '/github-actions/builds',
title: 'GitHub Actions Build',
});
@@ -30,20 +30,6 @@ describe('ComponentIdValidators', () => {
expect(ComponentIdValidators.httpsValidator(arg)).toBe(expected);
});
});
describe('masterValidator', () => {
const errorMessage = 'Must reference a file on the master branch.';
test.each([
[true, '/blob/master/'],
[true, 'http://example.com/blob/master/'],
[errorMessage, 'blob/master/'],
[errorMessage, '/blob/master'],
[errorMessage, '/master/'],
[errorMessage, ''],
[errorMessage, undefined],
])('should return %p for %s', (expected: string | boolean, arg: any) => {
expect(ComponentIdValidators.masterValidator(arg)).toBe(expected);
});
});
describe('yamlValidator', () => {
const errorMessage = "Must end with '.yaml'.";
test.each([
@@ -18,9 +18,6 @@ export const ComponentIdValidators = {
httpsValidator: (value: any) =>
(typeof value === 'string' && value.match(/^https:\/\//) !== null) ||
'Must start with https://.',
masterValidator: (value: any) =>
(typeof value === 'string' && value.match(/\/blob\/master\//) !== null) ||
'Must reference a file on the master branch.',
yamlValidator: (value: any) =>
(typeof value === 'string' && value.match(/.yaml$/) !== null) ||
"Must end with '.yaml'.",
+32
View File
@@ -15,3 +15,35 @@ Your plugin has been added to the example app in this repository, meaning you'll
You can also serve the plugin in isolation by running `yarn start` in the plugin directory.
This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads.
It is only meant for local development, and the setup for it can be found inside the [/dev](/dev) directory.
## Configuration
### Custom Storage URL
TechDocs currently reads a static HTML file, generated by Mkdocs (see our `packages/techdocs-container` folder for more documentation) and stored on an external server, and loads that into Backstage. By default, we have set up a mock server with some example documentation sites over in Google Cloud Storage:
```md
# Base URL
https://techdocs-mock-sites.storage.googleapis.com
# Home Page for the "mkdocs" docs
https://techdocs-mock-sites.storage.googleapis.com/mkdocs/index.html
# Home Page for the "backstage-microsite" docs
https://techdocs-mock-sites.storage.googleapis.com/backstage-microsite/index.html
```
Using your own setup (or ours which is being worked on as of Q3 2020), you can point it to your own server with your own hosted documentation sites. The only requirement is that it the output is from [Mkdocs](https://mkdocs.org) with the Material theme. You can always use our documentation generation tool located at `packages/techdocs-container` for easy setup.
To point TechDocs to your own server, simply update the `techdocs.storageUrl` value in your `app-config.yaml` file or set the environment variable `APP_CONFIG_techdocs_storageUrl` in your application:
```bash
git clone git@github.com:spotify/backstage.git
cd backstage/
yarn install
export APP_CONFIG_techdocs_storageUrl='"http://example-docs-site-server.com"'
yarn start
```
-15
View File
@@ -1,15 +0,0 @@
# MkDocs
Welcome to MkDocs. This is the TechDocs implementation of MkDocs.
**WIP: This is a work in progress. It is not ready for use yet. Follow our progress on [the Backstage Discord](https://discord.gg/MUpMjP2) under #docs-like-code or on [our GitHub Milestone](https://github.com/spotify/backstage/milestone/15).**
## Getting started
```bash
docker build ./container -t mkdocs-container
docker run -w /content -v $(pwd)/mock-docs:/content -p 8000:8000 -it mkdocs-container serve -a 0.0.0.0:8000
```
Then open up `http://localhost:8000` on your local machine.
@@ -1,24 +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.
FROM python:3.7.7-alpine3.12
RUN apk update && apk --no-cache add gcc musl-dev
RUN pip install --upgrade pip && pip install mkdocs==1.1.2 mkdocs-material==5.3.2 mkdocs-monorepo-plugin==0.4.5 pymdown-extensions==7.1
ADD ./techdocs-core /techdocs-core
RUN pip install --no-index /techdocs-core
ENTRYPOINT [ "mkdocs" ]
@@ -1,2 +0,0 @@
.tox
*.egg-info
@@ -1,45 +0,0 @@
# techdocs-core
This is the base [Mkdocs](https://mkdocs.org) plugin used when using Mkdocs with Spotify's TechDocs. It is written in Python and packages all of our Mkdocs defaults, such as theming, plugins, etc in a single plugin.
## Usage
**Installation instructions TBD.** We haven't published it to a Python registry yet.
Once you have installed the `mkdocs-techdocs-core` plugin, you'll need to add it to your `mkdocs.yml`.
```yaml
site_name: Backstage Docs
nav:
- Home: index.md
- Developing a Plugin: developing-a-plugin.md
plugins:
- techdocs-core
```
## Running Locally
You can install this package locally using `pip` and the `--editable` flag used for making developing Python packages.
```bash
pip install --editable .
```
You'll then have the `techdocs-core` package available to use in Mkdocs and `pip` will point the dependency to this folder.
## Running with Docker
In the parent `Dockerfile` we add this folder to the build and install the package locally in the container. In the future, we'll probably move away from this approach and have it download directly from a Python registry (and this folder will publish to one).
See the `README.md` located in the `mkdocs/` folder for more details on how to build and run the Docker container.
## Linting
```bash
pip install -r requirements.txt
python -m black src/
```
**Note:** This will write to all Python files in `src/` with the formatted code. If you would like to only check to see if it passes, simply append the `--check` flag.
@@ -1,9 +0,0 @@
# The "base" version of the Mkdocs project.
# Note: if you update this, also update `install_requires` in setup.py
# https://github.com/mkdocs/mkdocs
mkdocs==1.1.2
# The linter using for Python
# Note: This requires Python 3.6+ to run, but can format Python 2 code too.
# https://github.com/psf/black
black==19.10b0
@@ -1,48 +0,0 @@
"""
Copyright 2020 Spotify AB
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from setuptools import setup, find_packages
setup(
name='mkdocs-techdocs-core',
version='0.0.1',
description='A Mkdocs package that contains TechDocs defaults',
long_description='',
keywords='mkdocs',
url='https://github.com/spotify/backstage',
author='Spotify',
author_email='fossboard@spotify.com',
license='Apache-2.0',
python_requires='>=3.7',
install_requires=[
'mkdocs>=1.1.2'
],
classifiers=[
'Development Status :: 1 - Planning',
'Intended Audience :: Developers',
'Intended Audience :: Information Technology',
'License :: OSI Approved :: Apache Software License',
'Programming Language :: Python',
'Programming Language :: Python :: 3 :: Only',
'Programming Language :: Python :: 3.7'
],
packages=find_packages(),
entry_points={
'mkdocs.plugins': [
'techdocs-core = src.core:TechDocsCore'
]
}
)
@@ -1,85 +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.
"""
from mkdocs.plugins import BasePlugin, PluginCollection
from mkdocs.theme import Theme
from mkdocs.contrib.search import SearchPlugin
from mkdocs_monorepo_plugin.plugin import MonorepoPlugin
class TechDocsCore(BasePlugin):
def on_config(self, config):
# Theme
config["theme"] = Theme(name="material")
# Plugins
del config["plugins"]["techdocs-core"]
search_plugin = SearchPlugin()
search_plugin.load_config({})
monorepo_plugin = MonorepoPlugin()
monorepo_plugin.load_config({})
config["plugins"]["search"] = search_plugin
config["plugins"]["monorepo"] = monorepo_plugin
search_plugin = SearchPlugin()
search_plugin.load_config({})
config["plugins"]["search"] = search_plugin
# Markdown Extensions
config["markdown_extensions"].append("admonition")
config["markdown_extensions"].append("abbr")
config["markdown_extensions"].append("attr_list")
config["markdown_extensions"].append("def_list")
config["markdown_extensions"].append("codehilite")
config["mdx_configs"]["codehilite"] = {
"linenums": True,
"guess_lang": False,
"pygments_style": "friendly",
}
config["markdown_extensions"].append("toc")
config["mdx_configs"]["toc"] = {
"permalink": True,
}
config["markdown_extensions"].append("footnotes")
config["markdown_extensions"].append("markdown.extensions.tables")
config["markdown_extensions"].append("pymdownx.betterem")
config["mdx_configs"]["pymdownx.betterem"] = {
"smart_enable": "all",
}
config["markdown_extensions"].append("pymdownx.caret")
config["markdown_extensions"].append("pymdownx.critic")
config["markdown_extensions"].append("pymdownx.details")
config["markdown_extensions"].append("pymdownx.emoji")
config["mdx_configs"]["pymdownx.emoji"] = {
"emoji_generator": "!!python/name:pymdownx.emoji.to_svg",
}
config["markdown_extensions"].append("pymdownx.inlinehilite")
config["markdown_extensions"].append("pymdownx.magiclink")
config["markdown_extensions"].append("pymdownx.mark")
config["markdown_extensions"].append("pymdownx.smartsymbols")
config["markdown_extensions"].append("pymdownx.superfences")
config["markdown_extensions"].append("pymdownx.tasklist")
config["mdx_configs"]["pymdownx.tasklist"] = {
"custom_checkbox": True,
}
config["markdown_extensions"].append("pymdownx.tilde")
return config
@@ -1 +0,0 @@
site/
@@ -1,32 +0,0 @@
## hello mock docs
!!! test
Testing somethin
Some text about MOCDOC
\*[MOCDOC]: Mock Documentation
This is a paragraph.
{: #test_id .test_class }
Apple
: Pomaceous fruit of plants of the genus Malus in
the family Rosaceae.
```javascript
import { test } from 'something';
const addThingToThing = (a, b) a + b;
```
- [abc](#abc)
- [xyz](#xyz)
## abc
This is a b c.
## xyz
This is x y z.
@@ -1,8 +0,0 @@
site_name: 'mock-docs'
nav:
- Home: index.md
- SubDocs: '!include ./sub-docs/mkdocs.yml'
plugins:
- techdocs-core
@@ -1 +0,0 @@
### This is an md file in another docs folder using the [MkDocs Monorepo Plugin](https://github.com/spotify/mkdocs-monorepo-plugin)
@@ -1,4 +0,0 @@
site_name: subdocs
nav:
- Home 2: "index.md"
+1 -1
View File
@@ -4,7 +4,7 @@
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"private": true,
"private": false,
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
-17
View File
@@ -1,17 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const docStorageURL =
'https://techdocs-mock-sites.storage.googleapis.com';
@@ -15,6 +15,7 @@
*/
import React from 'react';
import { useApi, configApiRef } from '@backstage/core';
import { useShadowDom } from '..';
import { useAsync } from 'react-use';
import { AsyncState } from 'react-use/lib/useAsync';
@@ -30,7 +31,6 @@ import transformer, {
onCssReady,
sanitizeDOM,
} from '../transformers';
import { docStorageURL } from '../../config';
import URLFormatter from '../urlFormatter';
import { TechDocsNotFound } from './TechDocsNotFound';
import { TechDocsPageWrapper } from './TechDocsPageWrapper';
@@ -69,12 +69,16 @@ const useEnforcedTrailingSlash = (): void => {
export const Reader = () => {
useEnforcedTrailingSlash();
const docStorageUrl =
useApi(configApiRef).getOptionalString('techdocs.storageUrl') ??
'https://techdocs-mock-sites.storage.googleapis.com';
const location = useLocation();
const { componentId, '*': path } = useParams();
const [shadowDomRef, shadowRoot] = useShadowDom();
const navigate = useNavigate();
const normalizedUrl = new URLFormatter(
`${docStorageURL}${location.pathname.replace('/docs', '')}`,
`${docStorageUrl}${location.pathname.replace('/docs', '')}`,
).formatBaseURL();
const state = useFetch(`${normalizedUrl}index.html`);
@@ -91,7 +95,7 @@ export const Reader = () => {
const transformedElement = transformer(state.value as string, [
sanitizeDOM(),
addBaseUrl({
docStorageURL,
docStorageUrl,
componentId,
path,
}),
@@ -137,7 +141,7 @@ export const Reader = () => {
},
}),
onCssReady({
docStorageURL,
docStorageUrl,
onLoading: (dom: Element) => {
(dom as HTMLElement).style.setProperty('opacity', '0');
},
@@ -41,7 +41,7 @@ describe('addBaseUrl', () => {
const shadowDom = createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE, {
preTransformers: [
addBaseUrl({
docStorageURL: DOC_STORAGE_URL,
docStorageUrl: DOC_STORAGE_URL,
componentId: 'example-docs',
path: '',
}),
@@ -76,7 +76,7 @@ describe('addBaseUrl', () => {
{
preTransformers: [
addBaseUrl({
docStorageURL: DOC_STORAGE_URL,
docStorageUrl: DOC_STORAGE_URL,
componentId: 'example-docs',
path: 'examplepath',
}),
@@ -112,7 +112,7 @@ describe('addBaseUrl', () => {
{
preTransformers: [
addBaseUrl({
docStorageURL: DOC_STORAGE_URL,
docStorageUrl: DOC_STORAGE_URL,
componentId: 'example-docs',
path: 'examplepath/',
}),
@@ -18,13 +18,13 @@ import URLFormatter from '../urlFormatter';
import type { Transformer } from './index';
type AddBaseUrlOptions = {
docStorageURL: string;
docStorageUrl: string;
componentId: string;
path: string;
};
export const addBaseUrl = ({
docStorageURL,
docStorageUrl,
componentId,
path,
}: AddBaseUrlOptions): Transformer => {
@@ -38,8 +38,8 @@ export const addBaseUrl = ({
.forEach((elem: T) => {
const urlFormatter = new URLFormatter(
path.length < 1 || path.endsWith('/')
? `${docStorageURL}/${componentId}/${path}`
: `${docStorageURL}/${componentId}/${path}/`,
? `${docStorageUrl}/${componentId}/${path}`
: `${docStorageUrl}/${componentId}/${path}/`,
);
elem.setAttribute(
@@ -23,7 +23,7 @@ import {
} from '../../test-utils';
import { addBaseUrl, onCssReady } from '../transformers';
const docStorageURL: string =
const docStorageUrl: string =
'https://techdocs-mock-sites.storage.googleapis.com';
jest.useFakeTimers();
@@ -45,7 +45,7 @@ describe('onCssReady', () => {
preTransformers: [],
postTransformers: [
onCssReady({
docStorageURL,
docStorageUrl,
onLoading,
onLoaded,
}),
@@ -65,12 +65,12 @@ describe('onCssReady', () => {
preTransformers: [],
postTransformers: [
addBaseUrl({
docStorageURL,
docStorageUrl,
componentId: 'mkdocs',
path: '',
}),
onCssReady({
docStorageURL,
docStorageUrl,
onLoading,
onLoaded,
}),
@@ -17,20 +17,20 @@
import type { Transformer } from './index';
type OnCssReadyOptions = {
docStorageURL: string;
docStorageUrl: string;
onLoading: (dom: Element) => void;
onLoaded: (dom: Element) => void;
};
export const onCssReady = ({
docStorageURL,
docStorageUrl,
onLoading,
onLoaded,
}: OnCssReadyOptions): Transformer => {
return dom => {
const cssPages = Array.from(
dom.querySelectorAll('head > link[rel="stylesheet"]'),
).filter(elem => elem.getAttribute('href')?.startsWith(docStorageURL));
).filter(elem => elem.getAttribute('href')?.startsWith(docStorageUrl));
let count = cssPages.length;