Merge branch 'master' of https://github.com/spotify/backstage into add-okta-sso
This commit is contained in:
@@ -10,29 +10,39 @@
|
||||
|
||||
## What is Backstage?
|
||||
|
||||
Backstage is an open platform for building developer portals.
|
||||
|
||||
The philosophy behind Backstage is simple: Don't expose your engineers to the full complexity of your infrastructure tooling. Engineers should be shipping code — not figuring out a whole new toolset every time they want to implement the basics. Backstage allows you add "stuff" (tooling, services, features, etc.) by adding a plugin, instead of building a new tool. This saves you work and avoids the need of your team to learn how to use and support yet another tool.
|
||||
[Backstage](https://backstage.io/) is an open platform for building developer portals. It’s based on the developer portal we’ve been using internally at Spotify for over four years. Backstage can be as simple as a services catalog or as powerful as the UX layer for your entire tech infrastructure.
|
||||
|
||||
For more information go to [backstage.io](https://backstage.io) or join our [Discord chatroom](https://discord.gg/EBHEGzX).
|
||||
|
||||
## What problem does Backstage solve?
|
||||
### Features
|
||||
* Create and manage all of your organization’s software and microservices in one place
|
||||
* Services catalog keeps track of all software and its ownership
|
||||
* Visualizations provide information about your backend services and tooling, and help you monitor them
|
||||
* A unified method for managing microservices offers both visibility and control
|
||||
* Preset templates allow engineers to quickly create microservices in a standardized way ([coming soon](https://github.com/spotify/backstage/milestone/11))
|
||||
* Centralized, full-featured technical documentation with integrated tooling that makes it easy for developers to set up, publish, and maintain alongside their code ([coming soon](https://github.com/spotify/backstage/milestone/15))
|
||||
|
||||
As companies grow, their infrastructure systems get messier. Backstage unifies all your infrastructure tooling, services, and documentation with a single, consistent UI.
|
||||
### Benefits
|
||||
* For engineering managers, it allows you to maintain standards and best practices across the organization, and can help you manage your whole tech ecosystem, from migrations to test certification.
|
||||
* For end users (developers), it makes it fast and simple to build software components in a standardized way, and it provides a central place to manage all projects and documentation.
|
||||
* For platform engineers, it enables extensibility and scalability by letting you easily integrate new tools and services (via plugins), as well as extending the functionality of existing ones.
|
||||
* For everyone, it’s a single, consistent experience that ties all your infrastructure tooling, resources, standards, owners, contributors, and administrators together in one place.
|
||||
|
||||
This blog post provides more examples of how Backstage is used inside Spotify:
|
||||
## Backstage Service Catalog (alpha)
|
||||
|
||||
https://labs.spotify.com/2020/03/17/what-the-heck-is-backstage-anyway/
|
||||
The Backstage Service Catalog — actually, a software catalog, since it includes more than just services — is a centralized system that keeps track of ownership and metadata for all the software in your ecosystem (services, websites, libraries, data pipelines, etc). The catalog is built around the concept of [metadata yaml files](https://github.com/spotify/backstage/blob/master/docs/architecture-decisions/adr002-default-catalog-file-format.md#format) stored together with the code, which are then harvested and visualized in Backstage.
|
||||
|
||||
https://backstage.io/demos
|
||||

|
||||
|
||||
We have also found that the service catalog is a great way to organise the infrastructure tools you use to manage the software as well. This is how Backstage creates one developer portal for all your tools. Rather than asking teams to jump between different infrastructure UI’s (and incurring additional cognitive overhead each time they make a context switch), most of these tools can be organised around the entities in the catalog.
|
||||
|
||||
## Project roadmap
|
||||
|
||||
We created Backstage about 4 years ago. While our internal version of Backstage has had the benefit of time to mature and evolve, the first iteration of our open source version is still nascent. We are envisioning three phases of the project and we have already begun work on various aspects of these phases:
|
||||
|
||||
- 🐣 **Phase 1:** Extensible frontend platform (Done ✅) - You will be able to easily create a single consistent UI layer for your internal infrastructure and tools. A set of reusable UX patterns and components help ensure a consistent experience between tools.
|
||||
- 🐣 **Phase 1:** Extensible frontend platform (Done ✅) - You will be able to easily create a single consistent UI layer for your internal infrastructure and tools. A set of reusable [UX patterns and components](http://storybook.backstage.io) help ensure a consistent experience between tools.
|
||||
|
||||
- 🐢 **Phase 2:** Manage your stuff ([current focus](https://backstage.io/blog/2020/05/22/phase-2-service-catalog)) - Manage anything from microservices to software components to infrastructure and your service catalog. Regardless of whether you want to create a new library, view service deployment status in Kubernetes, or check the test coverage for a website -- Backstage will provide all of those tools - and many more - in a single developer portal.
|
||||
- 🐢 **Phase 2:** Service Catalog ([alpha released](https://backstage.io/blog/2020/06/22/backstage-service-catalog-alpha)) - With a single catalog, Backstage makes it easy for a team to manage ten services — and makes it possible for your company to manage thousands of them. Developers can get a uniform overview of all their software and related resources, regardless of how and where they are running, as well as an easy way to onboard and manage those resources.
|
||||
|
||||
- 🐇 **Phase 3:** Ecosystem (later) - Everyone's infrastructure stack is different. By fostering a vibrant community of contributors we hope to provide an ecosystem of Open Source plugins/integrations that allows you to pick the tools that match your stack.
|
||||
|
||||
@@ -46,14 +56,12 @@ The Backstage platform consists of a number of different components:
|
||||
|
||||
- **app** - Main web application that users interact with. It's built up by a number of different _Plugins_. This repo contains an example implementation of an app (located in `packages/example-app`) and you can easily get started with your own app by [creating one](docs/create-an-app.md).
|
||||
- [**plugins**](https://github.com/spotify/backstage/tree/master/plugins) - Each plugin is treated as a self-contained web app and can include almost any type of content. Plugins all use a common set of platform API's and reusable UI components. Plugins can fetch data either from the _backend_ or through any RESTful API exposed through the _proxy_.
|
||||
- [**backend**](https://github.com/spotify/backstage/tree/master/packages/backend) - GraphQL aggregation service that holds the model of your software ecosystem, including organisational information and what team owns what software. The backend also has a Plugin model for extending its graph.
|
||||
- [**service catalog**](https://github.com/spotify/backstage/tree/master/packages/backend) - Service that holds the model of your software ecosystem, including organisational information and what team owns what software. The backend also has a Plugin model for extending its graph.
|
||||
- **proxy** \* - Terminates HTTPS and exposes any RESTful API to Plugins.
|
||||
- **identity** \* - A backend service that holds your organisation's metadata.
|
||||
- **identity** - A backend service that holds your organisation's metadata.
|
||||
|
||||
_\* not yet released_
|
||||
|
||||

|
||||
|
||||
## Getting started
|
||||
|
||||
To run a Backstage app, you will need to have the following installed:
|
||||
@@ -100,6 +108,7 @@ We would love your help in building Backstage! See [CONTRIBUTING](CONTRIBUTING.m
|
||||
- [FAQ](docs/FAQ.md) - Frequently Asked Questions
|
||||
- [Code of Conduct](CODE_OF_CONDUCT.md) - This is how we roll
|
||||
- [Blog](https://backstage.io/blog/) - Announcements and updates
|
||||
- [Newsletter](https://mailchi.mp/spotify/backstage-community)
|
||||
- Give us a star ⭐️ - If you are using Backstage or think it is an interesting project, we would love a star ❤️
|
||||
|
||||
Or, if you are an open source developer and are interested in joining our team, please reach out to [foss-opportunities@spotify.com ](mailto:foss-opportunities@spotify.com)
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"@backstage/plugin-identity-backend": "^0.1.1-alpha.12",
|
||||
"@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.12",
|
||||
"@backstage/plugin-sentry-backend": "^0.1.1-alpha.12",
|
||||
"dockerode": "^3.2.0",
|
||||
"express": "^4.17.1",
|
||||
"knex": "^0.21.1",
|
||||
"sqlite3": "^4.2.0",
|
||||
@@ -35,6 +36,7 @@
|
||||
"@backstage/cli": "^0.1.1-alpha.12",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/express-serve-static-core": "^4.17.5",
|
||||
"@types/helmet": "^0.0.47"
|
||||
"@types/helmet": "^0.0.47",
|
||||
"@types/dockerode": "^2.5.32"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,15 +22,17 @@ import {
|
||||
Preparers,
|
||||
} from '@backstage/plugin-scaffolder-backend';
|
||||
import type { PluginEnvironment } from '../types';
|
||||
import Docker from 'dockerode';
|
||||
|
||||
export default async function createPlugin({ logger }: PluginEnvironment) {
|
||||
const templater = new CookieCutter();
|
||||
const filePreparer = new FilePreparer();
|
||||
const githubPreparer = new GithubPreparer();
|
||||
const preparers = new Preparers();
|
||||
const dockerClient = new Docker();
|
||||
|
||||
preparers.register('file', filePreparer);
|
||||
preparers.register('github', githubPreparer);
|
||||
|
||||
return await createRouter({ preparers, templater, logger });
|
||||
return await createRouter({ preparers, templater, logger, dockerClient });
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import { AnnotateLocationEntityProcessor } from './processors/AnnotateLocationEn
|
||||
import { EntityPolicyProcessor } from './processors/EntityPolicyProcessor';
|
||||
import { FileReaderProcessor } from './processors/FileReaderProcessor';
|
||||
import { GithubReaderProcessor } from './processors/GithubReaderProcessor';
|
||||
import { GitlabReaderProcessor } from './processors/GitlabReaderProcessor';
|
||||
import { LocationRefProcessor } from './processors/LocationEntityProcessor';
|
||||
import * as result from './processors/results';
|
||||
import {
|
||||
@@ -56,6 +57,7 @@ export class LocationReaders implements LocationReader {
|
||||
return [
|
||||
new FileReaderProcessor(),
|
||||
new GithubReaderProcessor(),
|
||||
new GitlabReaderProcessor(),
|
||||
new YamlProcessor(),
|
||||
new EntityPolicyProcessor(entityPolicy),
|
||||
new LocationRefProcessor(),
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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 { LocationSpec } from '@backstage/catalog-model';
|
||||
import fetch from 'node-fetch';
|
||||
import * as result from './results';
|
||||
import { LocationProcessor, LocationProcessorEmit } from './types';
|
||||
|
||||
export class GitlabReaderProcessor implements LocationProcessor {
|
||||
async readLocation(
|
||||
location: LocationSpec,
|
||||
optional: boolean,
|
||||
emit: LocationProcessorEmit,
|
||||
): Promise<boolean> {
|
||||
if (location.type !== 'gitlab') {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = this.buildRawUrl(location.target);
|
||||
|
||||
const response = await fetch(url.toString());
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.buffer();
|
||||
emit(result.data(location, data));
|
||||
} else {
|
||||
const message = `${location.target} could not be read as ${url}, ${response.status} ${response.statusText}`;
|
||||
if (response.status === 404) {
|
||||
if (!optional) {
|
||||
throw result.notFoundError(location, message);
|
||||
}
|
||||
} else {
|
||||
throw result.generalError(location, message);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
const message = `Unable to read ${location.type} ${location.target}, ${e}`;
|
||||
emit(result.generalError(location, message));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Converts
|
||||
// from: https://gitlab.example.com/a/b/blob/master/c.yaml
|
||||
// to: https://gitlab.example.com/a/b/raw/master/c.yaml
|
||||
private buildRawUrl(target: string): URL {
|
||||
try {
|
||||
const url = new URL(target);
|
||||
|
||||
const [
|
||||
empty,
|
||||
userOrOrg,
|
||||
repoName,
|
||||
blobKeyword,
|
||||
...restOfPath
|
||||
] = url.pathname.split('/');
|
||||
|
||||
if (
|
||||
empty !== '' ||
|
||||
userOrOrg === '' ||
|
||||
repoName === '' ||
|
||||
blobKeyword !== 'blob' ||
|
||||
!restOfPath.join('/').match(/\.yaml$/)
|
||||
) {
|
||||
throw new Error('Wrong Gitlab URL');
|
||||
}
|
||||
|
||||
// Replace 'blob' with 'raw'
|
||||
url.pathname = [empty, userOrOrg, repoName, 'raw', ...restOfPath].join(
|
||||
'/',
|
||||
);
|
||||
|
||||
return url;
|
||||
} catch (e) {
|
||||
throw new Error(`Incorrect url: ${target}, ${e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,9 @@
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.1.1-alpha.12",
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.12",
|
||||
"@backstage/config": "^0.1.1-alpha.12",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/dockerode": "^2.5.32",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"dockerode": "^3.2.0",
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"_copy_without_render": [".github/workflows/*"]
|
||||
}
|
||||
+6
-11
@@ -1,12 +1,7 @@
|
||||
#!/bin/bash
|
||||
#!/bin/sh
|
||||
|
||||
# package name is "__component_id__" so that yarn doesn't throw an error
|
||||
# about invalid characters when running yarn commands. here we replace it with the actual name
|
||||
sed -i -e "s/__component_id__/{{ cookiecutter.component_id }}/g" package.json
|
||||
|
||||
# node_modules was moved out of the template folder, during the pre_gen hook,
|
||||
# to avoid cookie_cutter from copying all of them. time to move it back
|
||||
mv ../../node_modules.tmp ../../\{\{cookiecutter.component_id\}\}/node_modules 2>/dev/null ||:
|
||||
|
||||
# move back the build directory that was moved out in the pre_gen hook (if it exists)
|
||||
mv ../../build.tmp ../../\{\{cookiecutter.component_id\}\}/build 2>/dev/null ||:
|
||||
# Move all template files to the root folder
|
||||
mv ./* ../
|
||||
cd ..
|
||||
rm -rf {{cookiecutter.component_id}}
|
||||
# # # # # # # #
|
||||
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# no way to ignore files in cookiecutter, so move node_modules out while building
|
||||
# to avoid cookiecutter from copying all of them
|
||||
mv ../../\{\{cookiecutter.component_id\}\}/node_modules ../../node_modules.tmp 2>/dev/null ||:
|
||||
|
||||
# cookicutter really doesn't like the next.js build directory, so if the app has
|
||||
# been built from inside the template folder, that folders needs to be moved out as well
|
||||
mv ../../\{\{cookiecutter.component_id\}\}/build ../../build.tmp 2>/dev/null ||:
|
||||
@@ -7,4 +7,3 @@ metadata:
|
||||
spec:
|
||||
type: cookiecutter
|
||||
path: '.'
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "__component_id__",
|
||||
"name": "{{ cookiecutter.component_id }}",
|
||||
"version": "0.0.0",
|
||||
"description": "{{ cookiecutter.description }}",
|
||||
"license": "UNLICENSED",
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
FROM alpine:3.7
|
||||
|
||||
RUN apk add --update \
|
||||
git \
|
||||
python \
|
||||
python-dev \
|
||||
py-pip \
|
||||
g++ && \
|
||||
pip install cookiecutter && \
|
||||
apk del g++ py-pip python-dev && \
|
||||
rm -rf /var/cache/apk/*
|
||||
@@ -16,3 +16,4 @@
|
||||
|
||||
export * from './scaffolder';
|
||||
export * from './service/router';
|
||||
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
jest.mock('./helpers', () => ({ runDockerContainer: jest.fn() }));
|
||||
|
||||
import { CookieCutter } from './cookiecutter';
|
||||
import fs from 'fs-extra';
|
||||
import os from 'os';
|
||||
import { RunDockerContainerOptions } from './helpers';
|
||||
import { PassThrough } from 'stream';
|
||||
import Docker from 'dockerode';
|
||||
|
||||
describe('CookieCutter Templater', () => {
|
||||
const cookie = new CookieCutter();
|
||||
const mockDocker = {} as Docker;
|
||||
const {
|
||||
runDockerContainer,
|
||||
}: {
|
||||
runDockerContainer: jest.Mock<RunDockerContainerOptions>;
|
||||
} = require('./helpers');
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
await fs.remove(`${os.tmpdir()}/cookiecutter.json`);
|
||||
});
|
||||
|
||||
it('should write a cookiecutter.json file with the values from the entitiy', async () => {
|
||||
const tempdir = os.tmpdir();
|
||||
|
||||
const values = {
|
||||
component_id: 'test',
|
||||
description: 'description',
|
||||
};
|
||||
|
||||
await cookie.run({ directory: tempdir, values, dockerClient: mockDocker });
|
||||
|
||||
const cookieCutterJson = await fs.readJSON(`${tempdir}/cookiecutter.json`);
|
||||
|
||||
expect(cookieCutterJson).toEqual(expect.objectContaining(values));
|
||||
});
|
||||
|
||||
it('should merge any value that is in the cookiecutter.json path already', async () => {
|
||||
const tempdir = os.tmpdir();
|
||||
const existingJson = {
|
||||
_copy_without_render: ['./github/workflows/*'],
|
||||
};
|
||||
await fs.writeJSON(`${tempdir}/cookiecutter.json`, existingJson);
|
||||
|
||||
const values = {
|
||||
component_id: 'hello',
|
||||
description: 'im something cool',
|
||||
};
|
||||
|
||||
await cookie.run({ directory: tempdir, values, dockerClient: mockDocker });
|
||||
|
||||
const cookieCutterJson = await fs.readJSON(`${tempdir}/cookiecutter.json`);
|
||||
|
||||
expect(cookieCutterJson).toEqual({ ...existingJson, ...values });
|
||||
});
|
||||
|
||||
it('should throw an error if the cookiecutter json is malformed and not missing', async () => {
|
||||
const tempdir = os.tmpdir();
|
||||
|
||||
await fs.writeFile(`${tempdir}/cookiecutter.json`, "{'");
|
||||
|
||||
const values = {
|
||||
component_id: 'hello',
|
||||
description: 'im something cool',
|
||||
};
|
||||
|
||||
await expect(
|
||||
cookie.run({ directory: tempdir, values, dockerClient: mockDocker }),
|
||||
).rejects.toThrow(/Unexpected token ' in JSON at position 1/);
|
||||
});
|
||||
|
||||
it('should run the correct docker container with the correct bindings for the volumes', async () => {
|
||||
const tempdir = os.tmpdir();
|
||||
|
||||
const values = {
|
||||
component_id: 'test',
|
||||
description: 'description',
|
||||
};
|
||||
|
||||
await cookie.run({ directory: tempdir, values, dockerClient: mockDocker });
|
||||
|
||||
expect(runDockerContainer).toHaveBeenCalledWith({
|
||||
imageName: 'backstage/cookiecutter',
|
||||
args: ['cookiecutter', '--no-input', '-o', '/result', '/template'],
|
||||
templateDir: tempdir,
|
||||
resultDir: `${tempdir}/result`,
|
||||
logStream: undefined,
|
||||
dockerClient: mockDocker,
|
||||
});
|
||||
});
|
||||
it('should return the result path to the end templated folder', async () => {
|
||||
const tempdir = os.tmpdir();
|
||||
|
||||
const values = {
|
||||
component_id: 'test',
|
||||
description: 'description',
|
||||
};
|
||||
|
||||
const path = await cookie.run({
|
||||
directory: tempdir,
|
||||
values,
|
||||
dockerClient: mockDocker,
|
||||
});
|
||||
|
||||
expect(path).toBe(`${tempdir}/result`);
|
||||
});
|
||||
|
||||
it('should pass through the streamer to the run docker helper', async () => {
|
||||
const stream = new PassThrough();
|
||||
|
||||
const tempdir = os.tmpdir();
|
||||
|
||||
const values = {
|
||||
component_id: 'test',
|
||||
description: 'description',
|
||||
};
|
||||
|
||||
await cookie.run({
|
||||
directory: tempdir,
|
||||
values,
|
||||
logStream: stream,
|
||||
dockerClient: mockDocker,
|
||||
});
|
||||
|
||||
expect(runDockerContainer).toHaveBeenCalledWith({
|
||||
imageName: 'backstage/cookiecutter',
|
||||
args: ['cookiecutter', '--no-input', '-o', '/result', '/template'],
|
||||
templateDir: tempdir,
|
||||
resultDir: `${tempdir}/result`,
|
||||
logStream: stream,
|
||||
dockerClient: mockDocker,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -16,17 +16,52 @@ import { TemplaterBase, TemplaterRunOptions } from '.';
|
||||
* limitations under the License.
|
||||
*/
|
||||
import fs from 'fs-extra';
|
||||
import { JsonValue } from '@backstage/config';
|
||||
import { runDockerContainer } from './helpers';
|
||||
|
||||
export class CookieCutter implements TemplaterBase {
|
||||
private async fetchTemplateCookieCutter(
|
||||
directory: string,
|
||||
): Promise<Record<string, JsonValue>> {
|
||||
try {
|
||||
return await fs.readJSON(`${directory}/cookiecutter.json`);
|
||||
} catch (ex) {
|
||||
if (ex.code !== 'ENOENT') {
|
||||
throw ex;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
public async run(options: TemplaterRunOptions): Promise<string> {
|
||||
// first we need to make cookiecutter.json in the directory provided with the input values.
|
||||
// First lets grab the default cookiecutter.json file
|
||||
const cookieCutterJson = await this.fetchTemplateCookieCutter(
|
||||
options.directory,
|
||||
);
|
||||
|
||||
const cookieInfo = {
|
||||
_copy_without_render: ['.github/workflows/*'],
|
||||
...cookieCutterJson,
|
||||
...options.values,
|
||||
};
|
||||
|
||||
await fs.writeJSON(`${options.directory}/cookiecutter.json`, cookieInfo);
|
||||
return '';
|
||||
// run cookie cutter with new json
|
||||
|
||||
const templateDir = options.directory;
|
||||
|
||||
// TODO(blam): This should be an entirely different directory on the host machine
|
||||
// not in the template directory
|
||||
const resultDir = `${templateDir}/result`;
|
||||
|
||||
await runDockerContainer({
|
||||
imageName: 'backstage/cookiecutter',
|
||||
args: ['cookiecutter', '--no-input', '-o', '/result', '/template'],
|
||||
templateDir,
|
||||
resultDir,
|
||||
logStream: options.logStream,
|
||||
dockerClient: options.dockerClient,
|
||||
});
|
||||
|
||||
return resultDir;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* 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 Stream, { PassThrough } from 'stream';
|
||||
import os from 'os';
|
||||
import fs from 'fs';
|
||||
import Docker from 'dockerode';
|
||||
import { runDockerContainer } from './helpers';
|
||||
|
||||
describe('helpers', () => {
|
||||
const mockDocker = new Docker() as jest.Mocked<Docker>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.spyOn(mockDocker, 'run')
|
||||
.mockResolvedValue([{ Error: null, StatusCode: 0 }]);
|
||||
});
|
||||
|
||||
describe('runDockerContainer', () => {
|
||||
const imageName = 'blam/github:ben';
|
||||
const args = ['bash', '-c', 'echo lol'];
|
||||
const templateDir = os.tmpdir();
|
||||
const resultDir = os.tmpdir();
|
||||
|
||||
it('should call the dockerClient run command with the correct arguments passed through', async () => {
|
||||
await runDockerContainer({
|
||||
imageName,
|
||||
args,
|
||||
templateDir,
|
||||
resultDir,
|
||||
dockerClient: mockDocker,
|
||||
});
|
||||
|
||||
expect(mockDocker.run).toHaveBeenCalledWith(
|
||||
imageName,
|
||||
args,
|
||||
expect.any(Stream),
|
||||
expect.objectContaining({
|
||||
HostConfig: {
|
||||
Binds: expect.arrayContaining([
|
||||
`${await fs.promises.realpath(templateDir)}:/template`,
|
||||
`${await fs.promises.realpath(resultDir)}:/result`,
|
||||
]),
|
||||
},
|
||||
Volumes: {
|
||||
'/template': {},
|
||||
'/result': {},
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('throws a correct error if the templating fails in docker', async () => {
|
||||
mockDocker.run.mockResolvedValueOnce([
|
||||
{
|
||||
Error: new Error('Something went wrong with docker'),
|
||||
StatusCode: 0,
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(
|
||||
runDockerContainer({
|
||||
imageName,
|
||||
args,
|
||||
templateDir,
|
||||
resultDir,
|
||||
dockerClient: mockDocker,
|
||||
}),
|
||||
).rejects.toThrow(/Something went wrong with docker/);
|
||||
});
|
||||
|
||||
it('throws a correct error when the response code of the container is non-zero', async () => {
|
||||
mockDocker.run.mockResolvedValueOnce([
|
||||
{
|
||||
Error: null,
|
||||
StatusCode: 123,
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(
|
||||
runDockerContainer({
|
||||
imageName,
|
||||
args,
|
||||
templateDir,
|
||||
resultDir,
|
||||
dockerClient: mockDocker,
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
/Docker container returned a non-zero exit code \(123\)/,
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass through the log stream to the docker client', async () => {
|
||||
const logStream = new PassThrough();
|
||||
await runDockerContainer({
|
||||
imageName,
|
||||
args,
|
||||
templateDir,
|
||||
resultDir,
|
||||
logStream,
|
||||
dockerClient: mockDocker,
|
||||
});
|
||||
|
||||
expect(mockDocker.run).toHaveBeenCalledWith(
|
||||
imageName,
|
||||
args,
|
||||
logStream,
|
||||
expect.objectContaining({
|
||||
HostConfig: {
|
||||
Binds: expect.arrayContaining([
|
||||
`${await fs.promises.realpath(templateDir)}:/template`,
|
||||
`${await fs.promises.realpath(resultDir)}:/result`,
|
||||
]),
|
||||
},
|
||||
Volumes: {
|
||||
'/template': {},
|
||||
'/result': {},
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 { Writable, PassThrough } from 'stream';
|
||||
import Docker from 'dockerode';
|
||||
import fs from 'fs';
|
||||
|
||||
export type RunDockerContainerOptions = {
|
||||
imageName: string;
|
||||
args: string[];
|
||||
logStream?: Writable;
|
||||
resultDir: string;
|
||||
templateDir: string;
|
||||
dockerClient: Docker;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param options the options object
|
||||
* @param options.imageName the image to run
|
||||
* @param options.args the arguments to pass the container
|
||||
* @param options.logStream the log streamer to capture log messages
|
||||
* @param options.resultDir the /result path inside the container
|
||||
* @param options.templateDir the /template path inside the container
|
||||
* @param options.dockerClient the dockerClient to use
|
||||
*/
|
||||
export const runDockerContainer = async ({
|
||||
imageName,
|
||||
args,
|
||||
logStream = new PassThrough(),
|
||||
resultDir,
|
||||
templateDir,
|
||||
dockerClient,
|
||||
}: RunDockerContainerOptions) => {
|
||||
const [{ Error: error, StatusCode: statusCode }] = await dockerClient.run(
|
||||
imageName,
|
||||
args,
|
||||
logStream,
|
||||
{
|
||||
Volumes: { '/result': {}, '/template': {} },
|
||||
HostConfig: {
|
||||
Binds: [
|
||||
// Need to use realpath here as Docker mounting does not like
|
||||
// symlinks for binding volumes
|
||||
`${await fs.promises.realpath(resultDir)}:/result`,
|
||||
`${await fs.promises.realpath(templateDir)}:/template`,
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (error) {
|
||||
throw new Error(
|
||||
`Docker failed to run with the following error message: ${error}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (statusCode !== 0) {
|
||||
throw new Error(
|
||||
`Docker container returned a non-zero exit code (${statusCode})`,
|
||||
);
|
||||
}
|
||||
|
||||
return { error, statusCode };
|
||||
};
|
||||
@@ -14,6 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Writable } from 'stream';
|
||||
import Docker from 'dockerode';
|
||||
|
||||
export interface RequiredTemplateValues {
|
||||
component_id: string;
|
||||
}
|
||||
@@ -21,12 +24,14 @@ export interface RequiredTemplateValues {
|
||||
export interface TemplaterRunOptions {
|
||||
directory: string;
|
||||
values: RequiredTemplateValues & object;
|
||||
logStream?: Writable;
|
||||
dockerClient: Docker;
|
||||
}
|
||||
|
||||
export abstract class TemplaterBase {
|
||||
export type TemplaterBase = {
|
||||
// runs the templating with the values and returns the directory to push the VCS
|
||||
abstract async run(opts: TemplaterRunOptions): Promise<string>;
|
||||
}
|
||||
run(opts: TemplaterRunOptions): Promise<string>;
|
||||
};
|
||||
|
||||
export interface TemplaterConfig {
|
||||
templater?: TemplaterBase;
|
||||
|
||||
@@ -19,18 +19,20 @@ import Router from 'express-promise-router';
|
||||
import express from 'express';
|
||||
import { PreparerBuilder, TemplaterBase } from '../scaffolder';
|
||||
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
|
||||
import Docker from 'dockerode';
|
||||
|
||||
export interface RouterOptions {
|
||||
preparers: PreparerBuilder;
|
||||
templater: TemplaterBase;
|
||||
logger: Logger;
|
||||
dockerClient: Docker;
|
||||
}
|
||||
|
||||
export async function createRouter(
|
||||
options: RouterOptions,
|
||||
): Promise<express.Router> {
|
||||
const router = Router();
|
||||
const { preparers, templater, logger: parentLogger } = options;
|
||||
const { preparers, templater, logger: parentLogger, dockerClient } = options;
|
||||
const logger = parentLogger.child({ plugin: 'scaffolder' });
|
||||
|
||||
router.post('/v1/jobs', async (_, res) => {
|
||||
@@ -66,12 +68,16 @@ export async function createRouter(
|
||||
const preparer = preparers.get(mockEntity);
|
||||
|
||||
// Run the preparer for the mock entity to produce a temporary directory with template in
|
||||
const path = await preparer.prepare(mockEntity);
|
||||
const skeletonPath = await preparer.prepare(mockEntity);
|
||||
|
||||
// Run the templater on the mock directory with values from the post body
|
||||
await templater.run({ directory: path, values: { component_id: 'test' } });
|
||||
const templatedPath = await templater.run({
|
||||
directory: skeletonPath,
|
||||
values: { component_id: 'test' },
|
||||
dockerClient,
|
||||
});
|
||||
|
||||
console.warn(path);
|
||||
console.warn(templatedPath);
|
||||
});
|
||||
|
||||
const app = express();
|
||||
|
||||
@@ -44,42 +44,42 @@ class TechDocsCore(BasePlugin):
|
||||
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("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("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("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.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.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')
|
||||
config["markdown_extensions"].append("pymdownx.tilde")
|
||||
|
||||
return config
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/core": "^0.1.1-alpha.12",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.12",
|
||||
"@backstage/theme": "^0.1.1-alpha.12",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import mockFetch from 'jest-fetch-mock';
|
||||
import ExampleComponent from './ExampleComponent';
|
||||
import { ThemeProvider } from '@material-ui/core';
|
||||
import { lightTheme } from '@backstage/theme';
|
||||
|
||||
describe('ExampleComponent', () => {
|
||||
it('should render', () => {
|
||||
mockFetch.mockResponse(() => new Promise(() => {}));
|
||||
const rendered = render(
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<ExampleComponent />
|
||||
</ThemeProvider>,
|
||||
);
|
||||
expect(rendered.getByText('Welcome to techdocs!')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,57 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import { Typography, Grid } from '@material-ui/core';
|
||||
import {
|
||||
InfoCard,
|
||||
Header,
|
||||
Page,
|
||||
pageTheme,
|
||||
Content,
|
||||
ContentHeader,
|
||||
HeaderLabel,
|
||||
SupportButton,
|
||||
} from '@backstage/core';
|
||||
import ExampleFetchComponent from '../ExampleFetchComponent';
|
||||
|
||||
const ExampleComponent: FC<{}> = () => (
|
||||
<Page theme={pageTheme.tool}>
|
||||
<Header title="Welcome to techdocs!" 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="Information card">
|
||||
<Typography variant="body1">
|
||||
All content should be wrapped in a card like this.
|
||||
</Typography>
|
||||
</InfoCard>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<ExampleFetchComponent />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
|
||||
export default ExampleComponent;
|
||||
@@ -1,108 +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 { makeStyles } from '@material-ui/core/styles';
|
||||
import { Table, TableColumn, Progress } from '@backstage/core';
|
||||
import Alert from '@material-ui/lab/Alert';
|
||||
import { useAsync } from 'react-use';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
avatar: {
|
||||
height: 32,
|
||||
width: 32,
|
||||
borderRadius: '50%',
|
||||
},
|
||||
});
|
||||
|
||||
type User = {
|
||||
gender: string; // "male"
|
||||
name: {
|
||||
title: string; // "Mr",
|
||||
first: string; // "Duane",
|
||||
last: string; // "Reed"
|
||||
};
|
||||
location: object; // {street: {number: 5060, name: "Hickory Creek Dr"}, city: "Albany", state: "New South Wales",…}
|
||||
email: string; // "duane.reed@example.com"
|
||||
login: object; // {uuid: "4b785022-9a23-4ab9-8a23-cb3fb43969a9", username: "blackdog796", password: "patch",…}
|
||||
dob: object; // {date: "1983-06-22T12:30:23.016Z", age: 37}
|
||||
registered: object; // {date: "2006-06-13T18:48:28.037Z", age: 14}
|
||||
phone: string; // "07-2154-5651"
|
||||
cell: string; // "0405-592-879"
|
||||
id: {
|
||||
name: string; // "TFN",
|
||||
value: string; // "796260432"
|
||||
};
|
||||
picture: { medium: string }; // {medium: "https://randomuser.me/api/portraits/men/95.jpg",…}
|
||||
nat: string; // "AU"
|
||||
};
|
||||
|
||||
type DenseTableProps = {
|
||||
users: User[];
|
||||
};
|
||||
|
||||
export const DenseTable: FC<DenseTableProps> = ({ users }) => {
|
||||
const classes = useStyles();
|
||||
|
||||
const columns: TableColumn[] = [
|
||||
{ title: 'Avatar', field: 'avatar' },
|
||||
{ title: 'Name', field: 'name' },
|
||||
{ title: 'Email', field: 'email' },
|
||||
{ title: 'Nationality', field: 'nationality' },
|
||||
];
|
||||
|
||||
const data = users.map(user => {
|
||||
return {
|
||||
avatar: (
|
||||
<img
|
||||
src={user.picture.medium}
|
||||
className={classes.avatar}
|
||||
alt={user.name.first}
|
||||
/>
|
||||
),
|
||||
name: `${user.name.first} ${user.name.last}`,
|
||||
email: user.email,
|
||||
nationality: user.nat,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Table
|
||||
title="Example User List (fetching data from randomuser.me)"
|
||||
options={{ search: false, paging: false }}
|
||||
columns={columns}
|
||||
data={data}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ExampleFetchComponent: FC<{}> = () => {
|
||||
const { value, loading, error } = useAsync(async (): Promise<User[]> => {
|
||||
const response = await fetch('https://randomuser.me/api/?results=20');
|
||||
const data = await response.json();
|
||||
return data.results;
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <Progress />;
|
||||
} else if (error) {
|
||||
return <Alert severity="error">{error.message}</Alert>;
|
||||
}
|
||||
|
||||
return <DenseTable users={value || []} />;
|
||||
};
|
||||
|
||||
export default ExampleFetchComponent;
|
||||
@@ -30,16 +30,16 @@
|
||||
*/
|
||||
|
||||
import { createPlugin, createRouteRef } from '@backstage/core';
|
||||
import ExampleComponent from './components/ExampleComponent';
|
||||
import { Reader } from './reader/components/Reader';
|
||||
|
||||
export const rootRouteRef = createRouteRef({
|
||||
path: '/techdocs',
|
||||
title: 'techdocs',
|
||||
path: '/docs',
|
||||
title: 'Docs',
|
||||
});
|
||||
|
||||
export const plugin = createPlugin({
|
||||
id: 'techdocs',
|
||||
register({ router }) {
|
||||
router.addRoute(rootRouteRef, ExampleComponent);
|
||||
router.addRoute(rootRouteRef, Reader);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,816 @@
|
||||
/*
|
||||
* 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 { useShadowDom } from '..';
|
||||
|
||||
const mockHtml: string = `
|
||||
|
||||
|
||||
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en" class="no-js">
|
||||
<head>
|
||||
<base href="https://mkdocs.peaceiris.com/getting-started/download-boilerplate/" />
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
|
||||
<meta name="description" content="MkDocs Material Boilerplate (Starter Kit) - Deploy documentation to hosting platforms (Netlify, GitHub Pages, GitLab Pages, and AWS Amplify Console) with GitHub Actions, CircleCI, Docker, pipenv, and CI/CD">
|
||||
|
||||
|
||||
<link rel="canonical" href="https://mkdocs.peaceiris.com/getting-started/download-boilerplate/">
|
||||
|
||||
|
||||
<meta name="author" content="peaceiris">
|
||||
|
||||
<link rel="shortcut icon" href="../../assets/images/favicon.png">
|
||||
<meta name="generator" content="mkdocs-1.1.2, mkdocs-material-5.3.2">
|
||||
|
||||
|
||||
|
||||
<title>Download boilerplate - MkDocs Material Boilerplate</title>
|
||||
|
||||
|
||||
|
||||
<link rel="stylesheet" href="https://mkdocs.peaceiris.com/assets/stylesheets/main.fe0cca5b.min.css">
|
||||
|
||||
<link rel="stylesheet" href="https://mkdocs.peaceiris.com/assets/stylesheets/palette.a46bcfb3.min.css">
|
||||
|
||||
|
||||
|
||||
|
||||
<meta name="theme-color" content="#2196f3">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<link href="https://fonts.gstatic.com" rel="preconnect" crossorigin>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,400i,700%7CRoboto+Mono&display=fallback">
|
||||
<style>body,input{font-family:"Roboto",-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif}code,kbd,pre{font-family:"Roboto Mono",SFMono-Regular,Consolas,Menlo,monospace}</style>
|
||||
|
||||
|
||||
|
||||
<link rel="manifest" href="../../manifest.json" crossorigin="use-credentials">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="../../assets/css/custom.css">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</head>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<body dir="ltr" data-md-color-scheme="" data-md-color-primary="blue" data-md-color-accent="blue">
|
||||
|
||||
|
||||
<input class="md-toggle" data-md-toggle="drawer" type="checkbox" id="__drawer" autocomplete="off">
|
||||
<input class="md-toggle" data-md-toggle="search" type="checkbox" id="__search" autocomplete="off">
|
||||
<label class="md-overlay" for="__drawer"></label>
|
||||
<div data-md-component="skip">
|
||||
|
||||
|
||||
<a href="#download_boilerplate" class="md-skip">
|
||||
Skip to content
|
||||
</a>
|
||||
|
||||
</div>
|
||||
<div data-md-component="announce">
|
||||
|
||||
</div>
|
||||
|
||||
<header class="md-header" data-md-component="header">
|
||||
<nav class="md-header-nav md-grid" aria-label="Header">
|
||||
<a href="https://mkdocs.peaceiris.com/" title="MkDocs Material Boilerplate" class="md-header-nav__button md-logo" aria-label="MkDocs Material Boilerplate">
|
||||
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 8a3 3 0 003-3 3 3 0 00-3-3 3 3 0 00-3 3 3 3 0 003 3m0 3.54C9.64 9.35 6.5 8 3 8v11c3.5 0 6.64 1.35 9 3.54 2.36-2.19 5.5-3.54 9-3.54V8c-3.5 0-6.64 1.35-9 3.54z"/></svg>
|
||||
|
||||
</a>
|
||||
<label class="md-header-nav__button md-icon" for="__drawer">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 6h18v2H3V6m0 5h18v2H3v-2m0 5h18v2H3v-2z"/></svg>
|
||||
</label>
|
||||
<div class="md-header-nav__title" data-md-component="header-title">
|
||||
|
||||
<div class="md-header-nav__ellipsis">
|
||||
<span class="md-header-nav__topic md-ellipsis">
|
||||
MkDocs Material Boilerplate
|
||||
</span>
|
||||
<span class="md-header-nav__topic md-ellipsis">
|
||||
|
||||
Download boilerplate
|
||||
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<label class="md-header-nav__button md-icon" for="__search">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0116 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.516 6.516 0 019.5 16 6.5 6.5 0 013 9.5 6.5 6.5 0 019.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5z"/></svg>
|
||||
</label>
|
||||
|
||||
<div class="md-search" data-md-component="search" role="dialog">
|
||||
<label class="md-search__overlay" for="__search"></label>
|
||||
<div class="md-search__inner" role="search">
|
||||
<form class="md-search__form" name="search">
|
||||
<input type="text" class="md-search__input" name="query" aria-label="Search" placeholder="Search" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false" data-md-component="search-query" data-md-state="active">
|
||||
<label class="md-search__icon md-icon" for="__search">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0116 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.516 6.516 0 019.5 16 6.5 6.5 0 013 9.5 6.5 6.5 0 019.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11h12z"/></svg>
|
||||
</label>
|
||||
<button type="reset" class="md-search__icon md-icon" aria-label="Clear" data-md-component="search-reset" tabindex="-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/></svg>
|
||||
</button>
|
||||
</form>
|
||||
<div class="md-search__output">
|
||||
<div class="md-search__scrollwrap" data-md-scrollfix>
|
||||
<div class="md-search-result" data-md-component="search-result">
|
||||
<div class="md-search-result__meta">
|
||||
Initializing search
|
||||
</div>
|
||||
<ol class="md-search-result__list"></ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="md-header-nav__source">
|
||||
|
||||
<a href="https://github.com/peaceiris/mkdocs-material-boilerplate/" title="Go to repository" class="md-source">
|
||||
<div class="md-source__icon md-icon">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M439.55 236.05L244 40.45a28.87 28.87 0 00-40.81 0l-40.66 40.63 51.52 51.52c27.06-9.14 52.68 16.77 43.39 43.68l49.66 49.66c34.23-11.8 61.18 31 35.47 56.69-26.49 26.49-70.21-2.87-56-37.34L240.22 199v121.85c25.3 12.54 22.26 41.85 9.08 55a34.34 34.34 0 01-48.55 0c-17.57-17.6-11.07-46.91 11.25-56v-123c-20.8-8.51-24.6-30.74-18.64-45L142.57 101 8.45 235.14a28.86 28.86 0 000 40.81l195.61 195.6a28.86 28.86 0 0040.8 0l194.69-194.69a28.86 28.86 0 000-40.81z"/></svg>
|
||||
</div>
|
||||
<div class="md-source__repository">
|
||||
GitHub
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="md-container" data-md-component="container">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<nav class="md-tabs md-tabs--active" aria-label="Tabs" data-md-component="tabs">
|
||||
<div class="md-tabs__inner md-grid">
|
||||
<ul class="md-tabs__list">
|
||||
|
||||
|
||||
<li class="md-tabs__item">
|
||||
|
||||
<a href="../.." class="md-tabs__link">
|
||||
Home
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-tabs__item">
|
||||
|
||||
<a href="./" class="md-tabs__link md-tabs__link--active">
|
||||
Getting started
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-tabs__item">
|
||||
|
||||
<a href="../../hosting-and-deployment/combinations/" class="md-tabs__link">
|
||||
Hosting and Deployment
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-tabs__item">
|
||||
|
||||
<a href="../../extensions/mathjax/" class="md-tabs__link">
|
||||
Extensions
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<main class="md-main" data-md-component="main">
|
||||
<div class="md-main__inner md-grid">
|
||||
|
||||
|
||||
<div class="md-sidebar md-sidebar--primary" data-md-component="navigation">
|
||||
<div class="md-sidebar__scrollwrap">
|
||||
<div class="md-sidebar__inner">
|
||||
<nav class="md-nav md-nav--primary" aria-label="Navigation" data-md-level="0">
|
||||
<label class="md-nav__title" for="__drawer">
|
||||
<a href="https://mkdocs.peaceiris.com/" title="MkDocs Material Boilerplate" class="md-nav__button md-logo" aria-label="MkDocs Material Boilerplate">
|
||||
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 8a3 3 0 003-3 3 3 0 00-3-3 3 3 0 00-3 3 3 3 0 003 3m0 3.54C9.64 9.35 6.5 8 3 8v11c3.5 0 6.64 1.35 9 3.54 2.36-2.19 5.5-3.54 9-3.54V8c-3.5 0-6.64 1.35-9 3.54z"/></svg>
|
||||
|
||||
</a>
|
||||
MkDocs Material Boilerplate
|
||||
</label>
|
||||
|
||||
<div class="md-nav__source">
|
||||
|
||||
<a href="https://github.com/peaceiris/mkdocs-material-boilerplate/" title="Go to repository" class="md-source">
|
||||
<div class="md-source__icon md-icon">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M439.55 236.05L244 40.45a28.87 28.87 0 00-40.81 0l-40.66 40.63 51.52 51.52c27.06-9.14 52.68 16.77 43.39 43.68l49.66 49.66c34.23-11.8 61.18 31 35.47 56.69-26.49 26.49-70.21-2.87-56-37.34L240.22 199v121.85c25.3 12.54 22.26 41.85 9.08 55a34.34 34.34 0 01-48.55 0c-17.57-17.6-11.07-46.91 11.25-56v-123c-20.8-8.51-24.6-30.74-18.64-45L142.57 101 8.45 235.14a28.86 28.86 0 000 40.81l195.61 195.6a28.86 28.86 0 0040.8 0l194.69-194.69a28.86 28.86 0 000-40.81z"/></svg>
|
||||
</div>
|
||||
<div class="md-source__repository">
|
||||
GitHub
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<ul class="md-nav__list" data-md-scrollfix>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../.." title="Home" class="md-nav__link">
|
||||
Home
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../../material-for-mkdocs/" title="Material for MkDocs" class="md-nav__link">
|
||||
Material for MkDocs
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item md-nav__item--active md-nav__item--nested">
|
||||
|
||||
<input class="md-nav__toggle md-toggle" data-md-toggle="nav-3" type="checkbox" id="nav-3" checked>
|
||||
|
||||
<label class="md-nav__link" for="nav-3">
|
||||
Getting started
|
||||
<span class="md-nav__icon md-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8.59 16.58L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.42z"/></svg>
|
||||
</span>
|
||||
</label>
|
||||
<nav class="md-nav" aria-label="Getting started" data-md-level="1">
|
||||
<label class="md-nav__title" for="nav-3">
|
||||
<span class="md-nav__icon md-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11h12z"/></svg>
|
||||
</span>
|
||||
Getting started
|
||||
</label>
|
||||
<ul class="md-nav__list" data-md-scrollfix>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item md-nav__item--active">
|
||||
|
||||
<input class="md-nav__toggle md-toggle" data-md-toggle="toc" type="checkbox" id="__toc">
|
||||
|
||||
|
||||
|
||||
|
||||
<label class="md-nav__link md-nav__link--active" for="__toc">
|
||||
Download boilerplate
|
||||
<span class="md-nav__icon md-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 9h14V7H3v2m0 4h14v-2H3v2m0 4h14v-2H3v2m16 0h2v-2h-2v2m0-10v2h2V7h-2m0 6h2v-2h-2v2z"/></svg>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<a href="./" title="Download boilerplate" class="md-nav__link md-nav__link--active">
|
||||
Download boilerplate
|
||||
</a>
|
||||
|
||||
|
||||
<nav class="md-nav md-nav--secondary" aria-label="Table of contents">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<label class="md-nav__title" for="__toc">
|
||||
<span class="md-nav__icon md-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11h12z"/></svg>
|
||||
</span>
|
||||
Table of contents
|
||||
</label>
|
||||
<ul class="md-nav__list" data-md-scrollfix>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#git_clone" class="md-nav__link">
|
||||
Git clone
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#download_zip" class="md-nav__link">
|
||||
Download zip
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
</nav>
|
||||
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../pipenv/" title="pipenv" class="md-nav__link">
|
||||
pipenv
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../invoke/" title="invoke" class="md-nav__link">
|
||||
invoke
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../docker/" title="Docker" class="md-nav__link">
|
||||
Docker
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../pip/" title="pip" class="md-nav__link">
|
||||
pip
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item md-nav__item--nested">
|
||||
|
||||
<input class="md-nav__toggle md-toggle" data-md-toggle="nav-4" type="checkbox" id="nav-4">
|
||||
|
||||
<label class="md-nav__link" for="nav-4">
|
||||
Hosting and Deployment
|
||||
<span class="md-nav__icon md-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8.59 16.58L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.42z"/></svg>
|
||||
</span>
|
||||
</label>
|
||||
<nav class="md-nav" aria-label="Hosting and Deployment" data-md-level="1">
|
||||
<label class="md-nav__title" for="nav-4">
|
||||
<span class="md-nav__icon md-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11h12z"/></svg>
|
||||
</span>
|
||||
Hosting and Deployment
|
||||
</label>
|
||||
<ul class="md-nav__list" data-md-scrollfix>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../../hosting-and-deployment/combinations/" title="Combinations" class="md-nav__link">
|
||||
Combinations
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../../hosting-and-deployment/netlify/" title="Netlify" class="md-nav__link">
|
||||
Netlify
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../../hosting-and-deployment/github-pages/" title="GitHub Pages" class="md-nav__link">
|
||||
GitHub Pages
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../../hosting-and-deployment/gitlab-pages/" title="GitLab Pages" class="md-nav__link">
|
||||
GitLab Pages
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../../hosting-and-deployment/aws-amplify-console/" title="AWS Amplify Console" class="md-nav__link">
|
||||
AWS Amplify Console
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item md-nav__item--nested">
|
||||
|
||||
<input class="md-nav__toggle md-toggle" data-md-toggle="nav-5" type="checkbox" id="nav-5">
|
||||
|
||||
<label class="md-nav__link" for="nav-5">
|
||||
Extensions
|
||||
<span class="md-nav__icon md-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8.59 16.58L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.42z"/></svg>
|
||||
</span>
|
||||
</label>
|
||||
<nav class="md-nav" aria-label="Extensions" data-md-level="1">
|
||||
<label class="md-nav__title" for="nav-5">
|
||||
<span class="md-nav__icon md-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11h12z"/></svg>
|
||||
</span>
|
||||
Extensions
|
||||
</label>
|
||||
<ul class="md-nav__list" data-md-scrollfix>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../../extensions/mathjax/" title="MathJax" class="md-nav__link">
|
||||
MathJax
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../../extensions/code-hilite/" title="Code Hilite" class="md-nav__link">
|
||||
Code Hilite
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../../extensions/footnote/" title="Footnote" class="md-nav__link">
|
||||
Footnote
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../../license/" title="License" class="md-nav__link">
|
||||
License
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="md-sidebar md-sidebar--secondary" data-md-component="toc">
|
||||
<div class="md-sidebar__scrollwrap">
|
||||
<div class="md-sidebar__inner">
|
||||
|
||||
<nav class="md-nav md-nav--secondary" aria-label="Table of contents">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<label class="md-nav__title" for="__toc">
|
||||
<span class="md-nav__icon md-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11h12z"/></svg>
|
||||
</span>
|
||||
Table of contents
|
||||
</label>
|
||||
<ul class="md-nav__list" data-md-scrollfix>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#git_clone" class="md-nav__link">
|
||||
Git clone
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#download_zip" class="md-nav__link">
|
||||
Download zip
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="md-content">
|
||||
<article class="md-content__inner md-typeset">
|
||||
|
||||
|
||||
<a href="https://github.com/peaceiris/mkdocs-material-boilerplate/edit/master/docs_sample/getting-started/download-boilerplate.md" title="Edit this page" class="md-content__button md-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20.71 7.04c.39-.39.39-1.04 0-1.41l-2.34-2.34c-.37-.39-1.02-.39-1.41 0l-1.84 1.83 3.75 3.75M3 17.25V21h3.75L17.81 9.93l-3.75-3.75L3 17.25z"/></svg>
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<h1 id="download_boilerplate">Download boilerplate<a class="headerlink" href="#download_boilerplate" title="Permanent link">¶</a></h1>
|
||||
<h2 id="git_clone">Git clone<a class="headerlink" href="#git_clone" title="Permanent link">¶</a></h2>
|
||||
<div class="codehilite"><pre><span></span><code>git clone https://github.com/peaceiris/mkdocs-material-boilerplate.git
|
||||
<span class="nb">cd</span> mkdocs-material-boilerplate
|
||||
</code></pre></div>
|
||||
|
||||
|
||||
<h2 id="download_zip">Download zip<a class="headerlink" href="#download_zip" title="Permanent link">¶</a></h2>
|
||||
<div class="codehilite"><pre><span></span><code>wget <span class="s1">'https://github.com/peaceiris/mkdocs-material-boilerplate/archive/master.zip'</span>
|
||||
unzip master.zip
|
||||
<span class="nb">cd</span> mkdocs-material-boilerplate-master
|
||||
</code></pre></div>
|
||||
|
||||
|
||||
<p>👉 <a href="https://github.com/peaceiris/mkdocs-material-boilerplate/archive/master.zip">Click me to download zip</a></p>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
||||
<footer class="md-footer">
|
||||
|
||||
<div class="md-footer-nav">
|
||||
<nav class="md-footer-nav__inner md-grid" aria-label="Footer">
|
||||
|
||||
<a href="../../material-for-mkdocs/" title="Material for MkDocs" class="md-footer-nav__link md-footer-nav__link--prev" rel="prev">
|
||||
<div class="md-footer-nav__button md-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11h12z"/></svg>
|
||||
</div>
|
||||
<div class="md-footer-nav__title">
|
||||
<div class="md-ellipsis">
|
||||
<span class="md-footer-nav__direction">
|
||||
Previous
|
||||
</span>
|
||||
Material for MkDocs
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
|
||||
<a href="../pipenv/" title="pipenv" class="md-footer-nav__link md-footer-nav__link--next" rel="next">
|
||||
<div class="md-footer-nav__title">
|
||||
<div class="md-ellipsis">
|
||||
<span class="md-footer-nav__direction">
|
||||
Next
|
||||
</span>
|
||||
pipenv
|
||||
</div>
|
||||
</div>
|
||||
<div class="md-footer-nav__button md-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4 11v2h12l-5.5 5.5 1.42 1.42L19.84 12l-7.92-7.92L10.5 5.5 16 11H4z"/></svg>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="md-footer-meta md-typeset">
|
||||
<div class="md-footer-meta__inner md-grid">
|
||||
<div class="md-footer-copyright">
|
||||
|
||||
<div class="md-footer-copyright__highlight">
|
||||
© 2019 peaceiris
|
||||
</div>
|
||||
|
||||
Made with
|
||||
<a href="https://squidfunk.github.io/mkdocs-material/" target="_blank" rel="noopener">
|
||||
Material for MkDocs
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="md-footer-social">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<a href="https://github.com/peaceiris" target="_blank" rel="noopener" title="github.com" class="md-footer-social__link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 512"><path d="M186.1 328.7c0 20.9-10.9 55.1-36.7 55.1s-36.7-34.2-36.7-55.1 10.9-55.1 36.7-55.1 36.7 34.2 36.7 55.1zM480 278.2c0 31.9-3.2 65.7-17.5 95-37.9 76.6-142.1 74.8-216.7 74.8-75.8 0-186.2 2.7-225.6-74.8-14.6-29-20.2-63.1-20.2-95 0-41.9 13.9-81.5 41.5-113.6-5.2-15.8-7.7-32.4-7.7-48.8 0-21.5 4.9-32.3 14.6-51.8 45.3 0 74.3 9 108.8 36 29-6.9 58.8-10 88.7-10 27 0 54.2 2.9 80.4 9.2 34-26.7 63-35.2 107.8-35.2 9.8 19.5 14.6 30.3 14.6 51.8 0 16.4-2.6 32.7-7.7 48.2 27.5 32.4 39 72.3 39 114.2zm-64.3 50.5c0-43.9-26.7-82.6-73.5-82.6-18.9 0-37 3.4-56 6-14.9 2.3-29.8 3.2-45.1 3.2-15.2 0-30.1-.9-45.1-3.2-18.7-2.6-37-6-56-6-46.8 0-73.5 38.7-73.5 82.6 0 87.8 80.4 101.3 150.4 101.3h48.2c70.3 0 150.6-13.4 150.6-101.3zm-82.6-55.1c-25.8 0-36.7 34.2-36.7 55.1s10.9 55.1 36.7 55.1 36.7-34.2 36.7-55.1-10.9-55.1-36.7-55.1z"/></svg>
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<a href="https://twitter.com/piris314en" target="_blank" rel="noopener" title="twitter.com" class="md-footer-social__link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"/></svg>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="../../assets/javascripts/vendor.d710d30a.min.js"></script>
|
||||
<script src="../../assets/javascripts/bundle.7f4f3c92.min.js"></script><script id="__lang" type="application/json">{"clipboard.copy": "Copy to clipboard", "clipboard.copied": "Copied to clipboard", "search.config.lang": "en", "search.config.pipeline": "trimmer, stopWordFilter", "search.config.separator": "[\\s\\-]+", "search.result.placeholder": "Type to start searching", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents"}</script>
|
||||
|
||||
<script>
|
||||
app = initialize({
|
||||
base: "../..",
|
||||
features: ["tabs"],
|
||||
search: Object.assign({
|
||||
worker: "../../assets/javascripts/worker/search.9b3611bd.min.js"
|
||||
}, typeof search !== "undefined" && search)
|
||||
})
|
||||
</script>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.6/MathJax.js?config=TeX-MML-AM_CHTML"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
export const Reader = () => {
|
||||
const shadowDomRef = useShadowDom();
|
||||
|
||||
React.useEffect(() => {
|
||||
const divElement = shadowDomRef.current;
|
||||
if (!divElement?.shadowRoot) {
|
||||
return;
|
||||
}
|
||||
divElement.shadowRoot.innerHTML = mockHtml;
|
||||
}, [shadowDomRef]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3>Shadow DOM should be underneath</h3>
|
||||
<div ref={shadowDomRef} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -14,4 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { default } from './ExampleComponent';
|
||||
export { useShadowDom } from './shadowDom';
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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 { renderWithEffects } from '@backstage/test-utils';
|
||||
import { useShadowDom } from './shadowDom';
|
||||
|
||||
const ComponentWithoutHook = () => {
|
||||
return <div data-testid="shadow-dom" />;
|
||||
};
|
||||
|
||||
const ComponentWithHook = () => {
|
||||
const ref = useShadowDom();
|
||||
return <div data-testid="shadow-dom" ref={ref} />;
|
||||
};
|
||||
|
||||
describe('useShadowDom', () => {
|
||||
it('does not create a Shadow DOM instance', async () => {
|
||||
const rendered = await renderWithEffects(<ComponentWithoutHook />);
|
||||
|
||||
const divElement = rendered.getByTestId('shadow-dom');
|
||||
expect(divElement.shadowRoot).not.toBeInstanceOf(ShadowRoot);
|
||||
});
|
||||
|
||||
it('create a Shadow DOM instance', async () => {
|
||||
const rendered = await renderWithEffects(<ComponentWithHook />);
|
||||
|
||||
const divElement = rendered.getByTestId('shadow-dom');
|
||||
expect(divElement.shadowRoot).toBeInstanceOf(ShadowRoot);
|
||||
});
|
||||
});
|
||||
+13
-11
@@ -14,15 +14,17 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import mockFetch from 'jest-fetch-mock';
|
||||
import ExampleFetchComponent from './ExampleFetchComponent';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import type { RefObject } from 'react';
|
||||
|
||||
describe('ExampleFetchComponent', () => {
|
||||
it('should render', async () => {
|
||||
mockFetch.mockResponse(() => new Promise(() => {}));
|
||||
const rendered = render(<ExampleFetchComponent />);
|
||||
expect(await rendered.findByTestId('progress')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
type IShadowDOMRefObject = RefObject<HTMLDivElement>;
|
||||
export const useShadowDom: () => IShadowDOMRefObject = () => {
|
||||
const ref: IShadowDOMRefObject = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const divElement = ref.current;
|
||||
divElement?.attachShadow({ mode: 'open' });
|
||||
}, [ref]);
|
||||
|
||||
return ref;
|
||||
};
|
||||
+1
-1
@@ -14,4 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { default } from './ExampleFetchComponent';
|
||||
export * from './hooks';
|
||||
@@ -3433,6 +3433,13 @@
|
||||
resolved "https://registry.npmjs.org/@types/diff/-/diff-4.0.2.tgz#2e9bb89f9acc3ab0108f0f3dc4dbdcf2fff8a99c"
|
||||
integrity sha512-mIenTfsIe586/yzsyfql69KRnA75S8SVXQbTLpDejRrjH0QSJcpu3AUOi/Vjnt9IOsXKxPhJfGpQUNMueIU1fQ==
|
||||
|
||||
"@types/dockerode@^2.5.32":
|
||||
version "2.5.32"
|
||||
resolved "https://registry.npmjs.org/@types/dockerode/-/dockerode-2.5.32.tgz#52d3628f605f8ea65202541c59a8a6dd166384fd"
|
||||
integrity sha512-TfaGOoOHxsjkWRj2sPoQ3FLmTC5mVMhZ4kzZy13U7mjtIDoloE4e7AMj5jPLbffWB6Csy5DF5e0lC9M+tnKz/A==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/eslint-visitor-keys@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
|
||||
|
||||
Reference in New Issue
Block a user