This commit is contained in:
Tim Hansen
2020-09-16 08:07:18 -06:00
42 changed files with 395 additions and 96 deletions
+4
View File
@@ -57,6 +57,10 @@ catalog:
rules:
- allow: [Component, API, Group, Template, Location]
processors:
github:
privateToken:
$secret:
env: GITHUB_PRIVATE_TOKEN
githubApi:
privateToken:
$secret:
@@ -177,9 +177,6 @@ docs on creating private GitHub access tokens is available
Note that the need for private GitHub access tokens will be replaced with GitHub
Apps integration further down the line.
> **Right now it is only possible to scaffold repositories inside GitHub
> organizations, and not under personal accounts.**
The GitHub access token is passed along using the `GITHUB_ACCESS_TOKEN`
environment variable.
@@ -30,11 +30,6 @@ the documentation template.
Create an entity from the documentation template and you will get the needed
setup for free.
!!! warning Currently the Backstage Software Templates are limited to create
repositories inside GitHub organizations. You also need to generate an personal
access token and use as an environment variable. Read more about this
[here](../software-templates/installation.md#runtime-dependencies).
### Manually add documentation setup to already existing repository
Prerequisities:
@@ -3,21 +3,39 @@ id: running-backstage-locally
title: Running Backstage Locally
---
## Prerequisites
- Node.js
First make sure you are using NodeJS with an Active LTS Release, currently v12.
This is made easy with a version manager such as
[nvm](https://github.com/nvm-sh/nvm) which allows for version switching.
```bash
# Checking your version
node --version
> v14.7.0
# Adding a second node version
# Installing a new version
nvm install 12
> Downloading and installing node v12.18.3...
> Now using node v12.18.3 (npm v6.14.6)
# Checking your version
node --version
> v12.18.3
```
- yarn
Please refer to the
[installation instructions for yarn](https://classic.yarnpkg.com/en/docs/install/).
- Docker
We use Docker for few of our core features. So, you will need Docker installed
locally to use features like Software Templates and TechDocs. Please refer to
the
[installation instructions for Docker](https://docs.docker.com/engine/install/).
## Clone and Build
To get up and running with a local Backstage to evaluate it, let's clone it off
of GitHub and run an initial build.
@@ -15,26 +15,26 @@
*/
import React from 'react';
import { ProgressCard } from './ProgressCard';
import { GaugeCard } from './GaugeCard';
import { Grid } from '@material-ui/core';
const linkInfo = { title: 'Go to XYZ Location', link: '#' };
export default {
title: 'Progress Card',
component: ProgressCard,
component: GaugeCard,
};
export const Default = () => (
<Grid container spacing={2}>
<Grid item>
<ProgressCard title="Progress" progress={0.3} />
<GaugeCard title="Progress" progress={0.3} />
</Grid>
<Grid item>
<ProgressCard title="Progress" progress={0.57} />
<GaugeCard title="Progress" progress={0.57} />
</Grid>
<Grid item>
<ProgressCard title="Progress" progress={0.89} />
<GaugeCard title="Progress" progress={0.89} />
</Grid>
</Grid>
);
@@ -42,21 +42,17 @@ export const Default = () => (
export const Subhead = () => (
<Grid container spacing={2}>
<Grid item>
<ProgressCard
title="Progress"
subheader="With a subheader"
progress={0.3}
/>
<GaugeCard title="Progress" subheader="With a subheader" progress={0.3} />
</Grid>
<Grid item>
<ProgressCard
<GaugeCard
title="Progress"
subheader="With a subheader"
progress={0.57}
/>
</Grid>
<Grid item>
<ProgressCard
<GaugeCard
title="Progress"
subheader="With a subheader"
progress={0.89}
@@ -68,13 +64,13 @@ export const Subhead = () => (
export const LinkInFooter = () => (
<Grid container spacing={2}>
<Grid item>
<ProgressCard title="Progress" deepLink={linkInfo} progress={0.3} />
<GaugeCard title="Progress" deepLink={linkInfo} progress={0.3} />
</Grid>
<Grid item>
<ProgressCard title="Progress" deepLink={linkInfo} progress={0.57} />
<GaugeCard title="Progress" deepLink={linkInfo} progress={0.57} />
</Grid>
<Grid item>
<ProgressCard title="Progress" deepLink={linkInfo} progress={0.89} />
<GaugeCard title="Progress" deepLink={linkInfo} progress={0.89} />
</Grid>
</Grid>
);
@@ -18,32 +18,30 @@ import React from 'react';
import { render } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import { ProgressCard } from './ProgressCard';
import { GaugeCard } from './GaugeCard';
const minProps = { title: 'Tingle upgrade', progress: 0.12 };
describe('<ProgressCard />', () => {
describe('<GaugeCard />', () => {
it('renders without exploding', () => {
const { getByText } = render(wrapInTestApp(<ProgressCard {...minProps} />));
const { getByText } = render(wrapInTestApp(<GaugeCard {...minProps} />));
expect(getByText(/Tingle.*/)).toBeInTheDocument();
});
it('renders progress and title', () => {
const { getByText } = render(wrapInTestApp(<ProgressCard {...minProps} />));
const { getByText } = render(wrapInTestApp(<GaugeCard {...minProps} />));
expect(getByText(/Tingle.*/)).toBeInTheDocument();
expect(getByText(/12%.*/)).toBeInTheDocument();
});
it('does not render deepLink', () => {
const { queryByText } = render(
wrapInTestApp(<ProgressCard {...minProps} />),
);
const { queryByText } = render(wrapInTestApp(<GaugeCard {...minProps} />));
expect(queryByText('View more')).not.toBeInTheDocument();
});
it('handles invalid numbers', () => {
const badProps = { title: 'Tingle upgrade', progress: 'hejjo' };
const { getByText } = render(wrapInTestApp(<ProgressCard {...badProps} />));
const { getByText } = render(wrapInTestApp(<GaugeCard {...badProps} />));
expect(getByText(/N\/A.*/)).toBeInTheDocument();
});
});
@@ -18,7 +18,7 @@ import React, { FC } from 'react';
import { makeStyles } from '@material-ui/core';
import { InfoCard } from '../../layout/InfoCard';
import { BottomLinkProps } from '../../layout/BottomLink';
import { CircleProgress } from './CircleProgress';
import { GaugeProgress } from './GaugeProgress';
type Props = {
title: string;
@@ -36,7 +36,7 @@ const useStyles = makeStyles({
},
});
export const ProgressCard: FC<Props> = props => {
export const GaugeCard: FC<Props> = props => {
const classes = useStyles(props);
const { title, subheader, progress, deepLink, variant } = props;
@@ -48,7 +48,7 @@ export const ProgressCard: FC<Props> = props => {
deepLink={deepLink}
variant={variant}
>
<CircleProgress value={progress} />
<GaugeProgress value={progress} />
</InfoCard>
</div>
);
@@ -17,32 +17,32 @@
import React from 'react';
import { render } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import { CircleProgress, getProgressColor } from './CircleProgress';
import { GaugeProgress, getProgressColor } from './GaugeProgress';
describe('<CircleProgress />', () => {
describe('<GaugeProgress />', () => {
it('renders without exploding', () => {
const { getByText } = render(
wrapInTestApp(<CircleProgress value={10} fractional={false} />),
wrapInTestApp(<GaugeProgress value={10} fractional={false} />),
);
getByText('10%');
});
it('handles fractional prop', () => {
const { getByText } = render(
wrapInTestApp(<CircleProgress value={0.1} fractional />),
wrapInTestApp(<GaugeProgress value={0.1} fractional />),
);
getByText('10%');
});
it('handles max prop', () => {
const { getByText } = render(
wrapInTestApp(<CircleProgress value={1} max={10} fractional={false} />),
wrapInTestApp(<GaugeProgress value={1} max={10} fractional={false} />),
);
getByText('1%');
});
it('handles unit prop', () => {
const { getByText } = render(
wrapInTestApp(<CircleProgress value={10} fractional={false} unit="m" />),
wrapInTestApp(<GaugeProgress value={10} fractional={false} unit="m" />),
);
getByText('10m');
});
@@ -77,7 +77,7 @@ export function getProgressColor(
return palette.status.ok;
}
export const CircleProgress: FC<Props> = props => {
export const GaugeProgress: FC<Props> = props => {
const classes = useStyles(props);
const theme = useTheme<BackstageTheme>();
const { value, fractional, inverse, unit, max } = {
@@ -15,29 +15,29 @@
*/
import React from 'react';
import { HorizontalProgress } from './HorizontalProgress';
import { LinearGauge } from './LinearGauge';
const containerStyle = { width: 300 };
export default {
title: 'HorizontalProgress',
component: HorizontalProgress,
title: 'LinearGauge',
component: LinearGauge,
};
export const Default = () => (
<div style={containerStyle}>
<HorizontalProgress value={0.8} />
<LinearGauge value={0.8} />
</div>
);
export const MediumProgress = () => (
<div style={containerStyle}>
<HorizontalProgress value={0.5} />
<LinearGauge value={0.5} />
</div>
);
export const LowProgress = () => (
<div style={containerStyle}>
<HorizontalProgress value={0.2} />
<LinearGauge value={0.2} />
</div>
);
@@ -19,7 +19,7 @@ import { Tooltip, useTheme } from '@material-ui/core';
// @ts-ignore
import { Line } from 'rc-progress';
import { BackstageTheme } from '@backstage/theme';
import { getProgressColor } from './CircleProgress';
import { getProgressColor } from './GaugeProgress';
type Props = {
/**
@@ -28,7 +28,7 @@ type Props = {
value: number;
};
export const HorizontalProgress: FC<Props> = ({ value }) => {
export const LinearGauge: FC<Props> = ({ value }) => {
const theme = useTheme<BackstageTheme>();
if (isNaN(value)) {
return null;
@@ -14,6 +14,6 @@
* limitations under the License.
*/
export { ProgressCard } from './ProgressCard';
export { CircleProgress } from './CircleProgress';
export { HorizontalProgress } from './HorizontalProgress';
export { GaugeCard } from './GaugeCard';
export { GaugeProgress } from './GaugeProgress';
export { LinearGauge } from './LinearGauge';
@@ -30,7 +30,7 @@ import {
Table,
StatusOK,
TableColumn,
ProgressCard,
GaugeCard,
TrendLine,
} from '../../components';
import { Box, Typography, Link, Chip, Grid } from '@material-ui/core';
@@ -120,14 +120,14 @@ const DataGrid = () => (
direction="row"
>
<Grid item xs={6}>
<ProgressCard
<GaugeCard
title="GKE Usage Score"
subheader="This should be above 75%"
progress={0.87}
/>
</Grid>
<Grid item xs={6}>
<ProgressCard
<GaugeCard
title="Deployment Score"
subheader="This should be above 40%"
progress={0.58}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 890 B

After

Width:  |  Height:  |  Size: 883 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long
@@ -0,0 +1,47 @@
/*
* 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';
const useStyles = makeStyles({
svg: {
width: 'auto',
height: 28,
},
path: {
fill: '#7df3e1',
},
});
const LogoIcon: FC<{}> = () => {
const classes = useStyles();
return (
<svg
className={classes.svg}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 337.46 428.5"
>
<path
className={classes.path}
d="M303,166.05a80.69,80.69,0,0,0,13.45-10.37c.79-.77,1.55-1.53,2.3-2.3a83.12,83.12,0,0,0,7.93-9.38A63.69,63.69,0,0,0,333,133.23a48.58,48.58,0,0,0,4.35-16.4c1.49-19.39-10-38.67-35.62-54.22L198.56,0,78.3,115.23,0,190.25l108.6,65.91a111.59,111.59,0,0,0,57.76,16.41c24.92,0,48.8-8.8,66.42-25.69,19.16-18.36,25.52-42.12,13.7-61.87a49.22,49.22,0,0,0-6.8-8.87A89.17,89.17,0,0,0,259,178.29h.15a85.08,85.08,0,0,0,31-5.79A80.88,80.88,0,0,0,303,166.05ZM202.45,225.86c-19.32,18.51-50.4,21.23-75.7,5.9L51.61,186.15l67.45-64.64,76.41,46.38C223,184.58,221.49,207.61,202.45,225.86Zm8.93-82.22-70.65-42.89L205.14,39,274.51,81.1c25.94,15.72,29.31,37,10.55,55A60.69,60.69,0,0,1,211.38,143.64Zm29.86,190c-19.57,18.75-46.17,29.09-74.88,29.09a123.73,123.73,0,0,1-64.1-18.2L0,282.52v24.67L108.6,373.1a111.6,111.6,0,0,0,57.76,16.42c24.92,0,48.8-8.81,66.42-25.69,12.88-12.34,20-27.13,19.68-41.49v-1.79A87.27,87.27,0,0,1,241.24,333.68Zm0-39c-19.57,18.75-46.17,29.08-74.88,29.08a123.81,123.81,0,0,1-64.1-18.19L0,243.53v24.68l108.6,65.91a111.6,111.6,0,0,0,57.76,16.42c24.92,0,48.8-8.81,66.42-25.69,12.88-12.34,20-27.13,19.68-41.5v-1.78A87.27,87.27,0,0,1,241.24,294.7Zm0-39c-19.57,18.76-46.17,29.09-74.88,29.09a123.81,123.81,0,0,1-64.1-18.19L0,204.55v24.68l108.6,65.91a111.59,111.59,0,0,0,57.76,16.41c24.92,0,48.8-8.8,66.42-25.68,12.88-12.35,20-27.13,19.68-41.5v-1.82A86.09,86.09,0,0,1,241.24,255.71Zm83.7,25.74a94.15,94.15,0,0,1-60.2,25.86h0V334a81.6,81.6,0,0,0,51.74-22.37c14-13.38,21.14-28.11,21-42.64v-2.19A94.92,94.92,0,0,1,324.94,281.45Zm-83.7,91.21c-19.57,18.76-46.17,29.09-74.88,29.09a123.73,123.73,0,0,1-64.1-18.2L0,321.5v24.68l108.6,65.9a111.6,111.6,0,0,0,57.76,16.42c24.92,0,48.8-8.8,66.42-25.69,12.88-12.34,20-27.13,19.68-41.49v-1.79A86.29,86.29,0,0,1,241.24,372.66ZM327,162.45c-.68.69-1.35,1.38-2.05,2.06a94.37,94.37,0,0,1-10.64,8.65,91.35,91.35,0,0,1-11.6,7,94.53,94.53,0,0,1-26.24,8.71,97.69,97.69,0,0,1-14.16,1.57c.5,1.61.9,3.25,1.25,4.9a53.27,53.27,0,0,1,1.14,12V217h.05a84.41,84.41,0,0,0,25.35-5.55,81,81,0,0,0,26.39-16.82c.8-.77,1.5-1.56,2.26-2.34a82.08,82.08,0,0,0,7.93-9.38A63.76,63.76,0,0,0,333,172.17a48.55,48.55,0,0,0,4.32-16.45c.09-1.23.2-2.47.19-3.7V150q-1.08,1.54-2.25,3.09A96.73,96.73,0,0,1,327,162.45Zm0,77.92c-.69.7-1.31,1.41-2,2.1a94.2,94.2,0,0,1-60.2,25.86h0l0,26.67h0a81.6,81.6,0,0,0,51.74-22.37A73.51,73.51,0,0,0,333,250.13a48.56,48.56,0,0,0,4.32-16.44c.09-1.24.2-2.47.19-3.71v-2.19c-.74,1.07-1.46,2.15-2.27,3.21A95.68,95.68,0,0,1,327,240.37Zm0-39c-.69.7-1.31,1.41-2,2.1a93.18,93.18,0,0,1-10.63,8.65,91.63,91.63,0,0,1-11.63,7,95.47,95.47,0,0,1-37.94,10.18h0V256h0a81.65,81.65,0,0,0,51.74-22.37c.8-.77,1.5-1.56,2.26-2.34a82.08,82.08,0,0,0,7.93-9.38A63.76,63.76,0,0,0,333,211.15a48.56,48.56,0,0,0,4.32-16.44c.09-1.24.2-2.48.19-3.71v-2.2c-.74,1.08-1.46,2.16-2.27,3.22A95.68,95.68,0,0,1,327,201.39Z"
/>
</svg>
);
};
export default LogoIcon;
@@ -1,15 +1,21 @@
import React from 'react';
import React, { FC, useContext } from 'react';
import HomeIcon from '@material-ui/icons/Home';
import LibraryBooks from '@material-ui/icons/LibraryBooks';
import CreateComponentIcon from '@material-ui/icons/AddCircleOutline';
import BuildIcon from '@material-ui/icons/BuildRounded';
import RuleIcon from '@material-ui/icons/AssignmentTurnedIn';
import MapIcon from '@material-ui/icons/MyLocation';
import { Link, makeStyles } from '@material-ui/core';
import { NavLink } from 'react-router-dom';
import LogoFull from './LogoFull';
import LogoIcon from './LogoIcon';
import {
Sidebar,
SidebarItem,
SidebarDivider,
sidebarConfig,
SidebarContext,
SidebarSpace,
SidebarUserSettings,
SidebarThemeToggle,
@@ -19,6 +25,7 @@ import {
export const AppSidebar = () => (
<Sidebar>
<SidebarLogo />
<SidebarDivider />
{/* Global nav, not org-specific */}
<SidebarItem icon={HomeIcon} to="./" text="Home" />
@@ -37,3 +44,36 @@ export const AppSidebar = () => (
<SidebarPinButton />
</Sidebar>
);
const useSidebarLogoStyles = makeStyles({
root: {
width: sidebarConfig.drawerWidthClosed,
height: 3 * sidebarConfig.logoHeight,
display: 'flex',
flexFlow: 'row nowrap',
alignItems: 'center',
marginBottom: -14,
},
link: {
width: sidebarConfig.drawerWidthClosed,
marginLeft: 24,
},
});
const SidebarLogo: FC<{}> = () => {
const classes = useSidebarLogoStyles();
const { isOpen } = useContext(SidebarContext);
return (
<div className={classes.root}>
<Link
component={NavLink}
to="/"
underline="none"
className={classes.link}
>
{isOpen ? <LogoFull /> : <LogoIcon />}
</Link>
</div>
);
};
+20 -11
View File
@@ -277,19 +277,28 @@ async function testAppServe(pluginName: string, appDir: string) {
let successful = false;
try {
const browser = new Browser();
for (let attempts = 1; ; attempts++) {
try {
const browser = new Browser();
await waitForPageWithText(browser, '/', 'Backstage Service Catalog');
await waitForPageWithText(
browser,
`/${pluginName}`,
`Welcome to ${pluginName}!`,
);
await waitForPageWithText(browser, '/', 'Backstage Service Catalog');
await waitForPageWithText(
browser,
`/${pluginName}`,
`Welcome to ${pluginName}!`,
);
print('Both App and Plugin loaded correctly');
successful = true;
} catch (error) {
throw new Error(`App serve test failed, ${error}`);
print('Both App and Plugin loaded correctly');
successful = true;
break;
} catch (error) {
if (attempts >= 5) {
throw new Error(`App serve test failed, ${error}`);
}
console.log(`App serve failed, trying again, ${error}`);
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
} finally {
// Kill entire process group, otherwise we'll end up with hanging serve processes
killTree(startApp.pid);
+1 -1
View File
@@ -22,7 +22,7 @@ module.exports = {
const [jsLoader] = config.module.rules.splice(0, 1);
if (jsLoader.use[0].loader !== 'babel-loader') {
throw new Error(
`Unexpected loader removed from storybook config, ${jsonLoader.use[0].loader}`,
`Unexpected loader removed from storybook config, ${jsLoader.use[0].loader}`,
);
}
+1 -1
View File
@@ -18,7 +18,7 @@ FROM python:3.8-alpine
RUN apk update && apk --no-cache add gcc musl-dev openjdk11-jdk curl graphviz ttf-dejavu fontconfig
RUN curl -L http://sourceforge.net/projects/plantuml/files/plantuml.1.2020.16.jar/download > /opt/plantuml.jar
RUN pip install --upgrade pip && pip install mkdocs-techdocs-core==0.0.4
RUN pip install --upgrade pip && pip install mkdocs-techdocs-core==0.0.7
# Create script to call plantuml.jar from a location in path
@@ -59,3 +59,5 @@ digraph G {
Goofy <-- MickeyMouse: responds
@enduml
```
:bulb:
@@ -50,6 +50,18 @@ python -m black src/
## Changelog
### 0.0.7
- Fix an issue with configuration of emoji support
### 0.0.6
- Further adjustments to versions to find ones that are compatible
### 0.0.5
- Downgrade some versions of markdown extensions to versions that are more stable
### 0.0.4
- Added support for more mkdocs extensions
@@ -4,10 +4,10 @@
mkdocs==1.1.2
mkdocs-material==5.3.2
mkdocs-monorepo-plugin==0.4.5
plantuml-markdown==3.4.0
plantuml-markdown==3.1.2
markdown_inline_graphviz_extension==1.1
pygments==2.6.1
pymdown-extensions==7.1
pymdown-extensions==8.0.0
# The linter using for Python
# Note: This requires Python 3.6+ to run, but can format Python 2 code too.
@@ -18,7 +18,7 @@ from setuptools import setup, find_packages
setup(
name='mkdocs-techdocs-core',
version='0.0.4',
version='0.0.7',
description='A Mkdocs package that contains TechDocs defaults',
long_description='',
keywords='mkdocs',
@@ -31,10 +31,10 @@ setup(
'mkdocs>=1.1.2',
'mkdocs-material==5.3.2',
'mkdocs-monorepo-plugin==0.4.5',
'plantuml-markdown==3.4.0',
'plantuml-markdown==3.1.2',
'markdown_inline_graphviz_extension==1.1',
'pygments==2.6.1',
'pymdown-extensions==7.1'
'pymdown-extensions==8.0.0'
],
classifiers=[
'Development Status :: 1 - Planning',
@@ -18,6 +18,7 @@ from mkdocs.plugins import BasePlugin, PluginCollection
from mkdocs.theme import Theme
from mkdocs.contrib.search import SearchPlugin
from mkdocs_monorepo_plugin.plugin import MonorepoPlugin
from pymdownx.emoji import to_svg
import tempfile
import os
@@ -76,9 +77,7 @@ class TechDocsCore(BasePlugin):
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["mdx_configs"]["pymdownx.emoji"] = {"emoji_generator": to_svg}
config["markdown_extensions"].append("pymdownx.inlinehilite")
config["markdown_extensions"].append("pymdownx.magiclink")
config["markdown_extensions"].append("pymdownx.mark")
+1
View File
@@ -36,6 +36,7 @@
"git-url-parse": "^11.1.2",
"globby": "^11.0.0",
"helmet": "^4.0.0",
"jsonschema": "^1.2.6",
"morgan": "^1.10.0",
"nodegit": "0.26.5",
"uuid": "^8.2.0",
@@ -22,11 +22,11 @@ spec:
component_id:
title: Name
type: string
description: Unique name of the component
description: Unique name of the component. Lowercase, URL-safe characters only.
description:
title: Description
type: string
description: Description of the component
description: Help others understand what this website is for.
use_typescript:
title: Use Typescript
type: boolean
@@ -25,5 +25,5 @@ spec:
description:
title: Description
type: string
description: Description of the component
description: Help others understand what these docs are about.
@@ -24,4 +24,4 @@ spec:
description:
title: Description
type: string
description: Description of the component
description: Help others understand what this website is for.
@@ -25,7 +25,7 @@ spec:
description:
title: Description
type: string
description: Description of the component
description: Help others understand what this service does.
http_port:
title: Port
type: integer
@@ -19,6 +19,9 @@ export const mockGithubClient = {
createInOrg: jest.fn(),
createForAuthenticatedUser: jest.fn(),
},
users: {
getByUsername: jest.fn(),
},
};
export class Octokit {
@@ -19,11 +19,18 @@ jest.mock('nodegit');
import { Octokit } from '@octokit/rest';
import * as NodeGit from 'nodegit';
import { OctokitResponse, ReposCreateInOrgResponseData } from '@octokit/types';
import {
OctokitResponse,
ReposCreateInOrgResponseData,
UsersGetByUsernameResponseData,
} from '@octokit/types';
import { GithubPublisher } from './github';
const { mockGithubClient } = require('@octokit/rest') as {
mockGithubClient: { repos: jest.Mocked<Octokit['repos']> };
mockGithubClient: {
repos: jest.Mocked<Octokit['repos']>;
users: jest.Mocked<Octokit['users']>;
};
};
const {
@@ -59,10 +66,14 @@ describe('GitHub Publisher', () => {
clone_url: 'mockclone',
},
} as OctokitResponse<ReposCreateInOrgResponseData>);
mockGithubClient.users.getByUsername.mockResolvedValue({
data: {
type: 'Organization',
},
} as OctokitResponse<UsersGetByUsernameResponseData>);
await publisher.publish({
values: {
isOrg: true,
storePath: 'blam/test',
owner: 'bob',
},
@@ -81,6 +92,11 @@ describe('GitHub Publisher', () => {
clone_url: 'mockclone',
},
} as OctokitResponse<ReposCreateInOrgResponseData>);
mockGithubClient.users.getByUsername.mockResolvedValue({
data: {
type: 'User',
},
} as OctokitResponse<UsersGetByUsernameResponseData>);
await publisher.publish({
values: {
@@ -112,6 +128,12 @@ describe('GitHub Publisher', () => {
clone_url: 'mockclone',
},
} as OctokitResponse<ReposCreateInOrgResponseData>);
mockGithubClient.users.getByUsername.mockResolvedValue({
data: {
type: 'Organization',
},
} as OctokitResponse<UsersGetByUsernameResponseData>);
it('should call init on the repo with the directory', async () => {
await publisher.publish({
values,
@@ -45,9 +45,12 @@ export class GithubPublisher implements PublisherBase {
) {
const [owner, name] = values.storePath.split('/');
const repoCreationPromise = values.isOrg
? this.client.repos.createInOrg({ name, org: owner })
: this.client.repos.createForAuthenticatedUser({ name });
const user = await this.client.users.getByUsername({ username: owner });
const repoCreationPromise =
user.data.type === 'Organization'
? this.client.repos.createInOrg({ name, org: owner })
: this.client.repos.createForAuthenticatedUser({ name });
const { data } = await repoCreationPromise;
@@ -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 { getVoidLogger } from '@backstage/backend-common';
import express from 'express';
import request from 'supertest';
import { createRouter } from './router';
import { Templaters, Preparers, PublisherBase } from '../scaffolder';
import Docker from 'dockerode';
jest.mock('dockerode');
describe('createRouter', () => {
let app: express.Express;
const publisher: jest.Mocked<PublisherBase> = { publish: jest.fn() };
beforeAll(async () => {
const router = await createRouter({
logger: getVoidLogger(),
preparers: new Preparers(),
templaters: new Templaters(),
publisher: publisher,
dockerClient: new Docker(),
});
app = express().use(router);
});
beforeEach(() => {
jest.resetAllMocks();
});
describe('POST /v1/jobs', () => {
const template = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Template',
metadata: {
description: 'Create a new CRA website project',
name: 'create-react-app-template',
tags: ['experimental', 'react', 'cra'],
title: 'Create React App Template',
},
spec: {
owner: 'web@example.com',
path: '.',
schema: {
properties: {
component_id: {
description: 'Unique name of the component',
title: 'Name',
type: 'string',
},
description: {
description: 'Description of the component',
title: 'Description',
type: 'string',
},
use_typescript: {
default: true,
description: 'Include typescript',
title: 'Use Typescript',
type: 'boolean',
},
},
required: ['component_id', 'use_typescript'],
},
templater: 'cra',
type: 'website',
},
};
it('rejects template values which do not match the template schema definition', async () => {
const response = await request(app).post('/v1/jobs').send({
template,
values: {},
});
expect(response.status).toEqual(400);
});
});
});
@@ -28,6 +28,7 @@ import {
TemplaterBuilder,
PublisherBase,
} from '../scaffolder';
import { validate, ValidatorResult } from 'jsonschema';
export interface RouterOptions {
preparers: PreparerBuilder;
@@ -84,6 +85,15 @@ export async function createRouter(
const values: RequiredTemplateValues & Record<string, JsonValue> =
req.body.values;
const validationResult: ValidatorResult = validate(
values,
template.spec.schema,
);
if (!validationResult.valid) {
res.status(400).json({ errors: validationResult.errors });
return;
}
const job = jobProcessor.create({
entity: template,
values,
+1 -2
View File
@@ -46,8 +46,7 @@ export class ScaffolderApi {
headers: {
'Content-Type': 'application/json',
},
// TODO(shmidt-i): when repo picker is implemented, take isOrg from it
body: JSON.stringify({ template, values: { ...values, isOrg: true } }),
body: JSON.stringify({ template, values: { ...values } }),
});
if (response.status !== 201) {
@@ -72,7 +72,7 @@ export const JobStatusModal = ({
{entity && (
<DialogActions>
<Button
to={generatePath(entityRoute.path, {
to={generatePath(`/catalog/${entityRoute.path}`, {
kind: entity.kind,
optionalNamespaceAndName: [
entity.metadata.namespace,
+5
View File
@@ -13288,6 +13288,11 @@ jsonpointer@^4.0.1:
resolved "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9"
integrity sha1-T9kss04OnbPInIYi7PUfm5eMbLk=
jsonschema@^1.2.6:
version "1.2.6"
resolved "https://registry.npmjs.org/jsonschema/-/jsonschema-1.2.6.tgz#52b0a8e9dc06bbae7295249d03e4b9faee8a0c0b"
integrity sha512-SqhURKZG07JyKKeo/ir24QnS4/BV7a6gQy93bUSe4lUdNp0QNpIz2c9elWJQ9dpc5cQYY6cvCzgRwy0MQCLyqA==
jspdf-autotable@3.5.3:
version "3.5.3"
resolved "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-3.5.3.tgz#2f73adb07f340e7dbf22950e3e6c8bf853991479"