diff --git a/backend/proxy/envoy.yaml b/backend/proxy/envoy.yaml
index eca02ae0c8..b23d39ccb6 100644
--- a/backend/proxy/envoy.yaml
+++ b/backend/proxy/envoy.yaml
@@ -36,7 +36,7 @@ static_resources:
route:
cluster: inventory_service
max_grpc_timeout: 0s
- - match: { prefix: '/spotify.backstage.scaffolder.v1.Scaffolder' }
+ - match: { prefix: '/spotify.backstage.scaffolder.v1.Scaffolder/' }
route:
cluster: scaffolder_service
max_grpc_timeout: 0s
@@ -87,5 +87,5 @@ static_resources:
lb_policy: round_robin
hosts:
- socket_address:
- address: identity
+ address: scaffolder
port_value: 50051
diff --git a/docker-compose.yaml b/docker-compose.yaml
index 647fa6a4cf..ddde99b79a 100755
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -48,4 +48,7 @@ services:
args:
service: scaffolder
restart: unless-stopped
+ volumes:
+ - ./backend/scaffolder/templates:/app/templates:ro
+
env_file: secrets.env
diff --git a/frontend/packages/app/package.json b/frontend/packages/app/package.json
index 55f747560a..adc216d0b6 100644
--- a/frontend/packages/app/package.json
+++ b/frontend/packages/app/package.json
@@ -8,6 +8,7 @@
"@backstage/plugin-hello-world": "0.0.0",
"@backstage/plugin-home-page": "0.0.0",
"@backstage/plugin-login": "0.0.0",
+ "@backstage/plugin-create-entity": "0.0.0",
"@react-workspaces/react-scripts": "^3.3.0-alpha-08",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
diff --git a/frontend/packages/app/src/App.tsx b/frontend/packages/app/src/App.tsx
index 9fbbf28dca..e3a03db49f 100644
--- a/frontend/packages/app/src/App.tsx
+++ b/frontend/packages/app/src/App.tsx
@@ -1,7 +1,12 @@
-import { BackstageTheme, createApp, InfoCard } from '@backstage/core';
-import HomePagePlugin from '@backstage/plugin-home-page';
+import {
+ BackstageTheme,
+ createApp,
+ InfoCard,
+} from '@backstage/core';
//import PageHeader from './components/PageHeader';
import { LoginComponent } from '@backstage/plugin-login';
+import HomePagePlugin from '@backstage/plugin-home-page';
+import CreateEntityPlugin from '@backstage/plugin-create-entity';
import { CssBaseline, makeStyles, ThemeProvider } from '@material-ui/core';
import React, { FC } from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
@@ -60,6 +65,7 @@ const app = createApp();
app.registerEntityKind(...entities);
app.registerPlugin(HomePagePlugin);
+app.registerPlugin(CreateEntityPlugin);
const AppComponent = app.build();
diff --git a/frontend/packages/core/src/components/TemplateList/CardLayoutStyles.js b/frontend/packages/core/src/components/TemplateList/CardLayoutStyles.js
new file mode 100644
index 0000000000..215a904ec3
--- /dev/null
+++ b/frontend/packages/core/src/components/TemplateList/CardLayoutStyles.js
@@ -0,0 +1,21 @@
+import { makeStyles } from '@material-ui/core/styles';
+
+// Shared MUI styles for a grid-based Card layout
+export const CardLayoutStyles = theme => ({
+ container: {
+ display: 'grid',
+ gridTemplateColumns: 'repeat(auto-fill, 296px)',
+ gridGap: theme.spacing(3),
+ marginBottom: theme.spacing(6),
+ },
+ card: {
+ display: 'flex',
+ flexDirection: 'column',
+ },
+ cardActions: {
+ flexGrow: '1',
+ alignItems: 'flex-end',
+ },
+});
+
+export const useCardLayoutStyles = makeStyles(CardLayoutStyles);
diff --git a/frontend/packages/core/src/components/TemplateList/TemplateCard.js b/frontend/packages/core/src/components/TemplateList/TemplateCard.js
new file mode 100644
index 0000000000..525ab6ec9e
--- /dev/null
+++ b/frontend/packages/core/src/components/TemplateList/TemplateCard.js
@@ -0,0 +1,67 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import TemplateCardMedia from './TemplateCardMedia';
+import {
+ Button,
+ Card,
+ CardActions,
+ CardContent,
+ Chip,
+ Typography,
+ withStyles,
+} from '@material-ui/core';
+import { Link } from '@material-ui/core';
+import { CardLayoutStyles } from './CardLayoutStyles';
+
+const styles = theme => ({
+ ...CardLayoutStyles(theme),
+ chip: {
+ backgroundColor: theme.palette.gold,
+ marginRight: 6,
+ },
+});
+
+export class TemplateCard extends Component {
+ static propTypes = {
+ isGoldenPath: PropTypes.bool,
+ item: PropTypes.object.isRequired,
+ };
+
+ static defaultProps = {
+ isGoldenPath: false,
+ };
+
+ render() {
+ const { item, isGoldenPath, classes } = this.props;
+ const isExperimental = item.lifecycle === 'experimental';
+ // if (!item.owner) {
+ // // If an item has no owner, it has most likely been unregistered from sysmodel. Should not crash the whole page.
+ // return null;
+ // }
+ return (
+
+
+ {item.description && (
+
+ {isGoldenPath && (
+
+ )}
+ {isExperimental && }
+ {item.description}
+
+ )}
+
+
+
+
+
+
+ );
+ }
+}
+
+export default withStyles(styles)(TemplateCard);
diff --git a/frontend/packages/core/src/components/TemplateList/TemplateCardMedia.js b/frontend/packages/core/src/components/TemplateList/TemplateCardMedia.js
new file mode 100644
index 0000000000..a68212e9e0
--- /dev/null
+++ b/frontend/packages/core/src/components/TemplateList/TemplateCardMedia.js
@@ -0,0 +1,41 @@
+import React, { Component } from 'react';
+import { Typography, withStyles } from '@material-ui/core';
+
+const styles = theme => ({
+ media: {
+ color: theme.palette.bursts.fontColor,
+ backgroundColor: theme.palette.bursts.backgroundColor.default,
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'flex-start',
+ padding: theme.spacing(2),
+ minHeight: 120,
+ position: 'relative',
+ isolation: 'isolate',
+ },
+ title: {
+ paddingTop: theme.spacing(1),
+ paddingBottom: theme.spacing(1),
+ lineHeight: 1,
+ },
+ slackChannel: {
+ color: theme.palette.bursts.slackChannelText,
+ },
+});
+
+export class TemplateCardMedia extends Component {
+ render() {
+ const { classes, name, ownerName } = this.props;
+
+ return (
+
+ {ownerName}
+
+ {name}
+
+
+ );
+ }
+}
+
+export default withStyles(styles)(TemplateCardMedia);
diff --git a/frontend/packages/core/src/components/TemplateList/TemplateList.js b/frontend/packages/core/src/components/TemplateList/TemplateList.js
new file mode 100644
index 0000000000..707ab62580
--- /dev/null
+++ b/frontend/packages/core/src/components/TemplateList/TemplateList.js
@@ -0,0 +1,42 @@
+import React, { Component, Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { withStyles } from '@material-ui/core';
+import TemplateCard from './TemplateCard';
+import { CardLayoutStyles } from './CardLayoutStyles';
+
+export class TemplateList extends Component {
+ static GOLDEN_PATH_TEMPLATE_IDS = [
+ 'simple-apollo-standalone',
+ 'scio-cookie',
+ 'react-skeleton',
+ 'science-box-cookie',
+ ];
+
+ static propTypes = {
+ items: PropTypes.array.isRequired,
+ };
+
+ static isGoldenPathItem(item) {
+ return TemplateList.GOLDEN_PATH_TEMPLATE_IDS.includes(item.id);
+ }
+
+ render() {
+ const { items, classes } = this.props;
+
+ return (
+
+
+ {items.map(item => (
+
+ ))}
+
+
+ );
+ }
+}
+
+export default withStyles(CardLayoutStyles)(TemplateList);
diff --git a/frontend/packages/core/src/index.ts b/frontend/packages/core/src/index.ts
index be8b1e851e..32467adfc3 100644
--- a/frontend/packages/core/src/index.ts
+++ b/frontend/packages/core/src/index.ts
@@ -16,3 +16,4 @@ export { default as ProgressCard } from './components/ProgressCard';
export { default as CircleProgress } from './components/CircleProgress';
export { default as Progress } from './components/Progress';
export { default as SortableTable } from './components/SortableTable';
+export { default as TemplateList } from '../src/components/TemplateList/TemplateList';
diff --git a/frontend/packages/plugins/create-entity/README.md b/frontend/packages/plugins/create-entity/README.md
new file mode 100644
index 0000000000..e084431c00
--- /dev/null
+++ b/frontend/packages/plugins/create-entity/README.md
@@ -0,0 +1 @@
+Welcome to your create-entity plugin!
diff --git a/frontend/packages/plugins/create-entity/jest.config.js b/frontend/packages/plugins/create-entity/jest.config.js
new file mode 100644
index 0000000000..6b28dacb3d
--- /dev/null
+++ b/frontend/packages/plugins/create-entity/jest.config.js
@@ -0,0 +1,4 @@
+module.exports = {
+ ...require('@spotify/web-scripts/config/jest.config.js'),
+ setupFilesAfterEnv: ['../jest.setup.ts'],
+};
diff --git a/frontend/packages/plugins/create-entity/jest.setup.ts b/frontend/packages/plugins/create-entity/jest.setup.ts
new file mode 100644
index 0000000000..666127af39
--- /dev/null
+++ b/frontend/packages/plugins/create-entity/jest.setup.ts
@@ -0,0 +1 @@
+import '@testing-library/jest-dom/extend-expect';
diff --git a/frontend/packages/plugins/create-entity/package.json b/frontend/packages/plugins/create-entity/package.json
new file mode 100644
index 0000000000..4855921c54
--- /dev/null
+++ b/frontend/packages/plugins/create-entity/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "@backstage/plugin-create-entity",
+ "version": "0.0.0",
+ "main": "src/index.ts",
+ "main:src": "src/index.ts",
+ "devDependencies": {
+ "@backstage/core": "0.0.0",
+ "@backstage/protobuf-definitions": "0.0.0",
+ "@spotify/web-scripts": "^6.0.0",
+ "@testing-library/jest-dom": "^4.2.4",
+ "@testing-library/react": "^9.3.2",
+ "@testing-library/user-event": "^7.1.2",
+ "@types/jest": "^24.0.0",
+ "@types/node": "^12.0.0",
+ "@types/react": "^16.9.0",
+ "@types/react-dom": "^16.9.0",
+ "react": "^16.12.0",
+ "react-dom": "^16.12.0",
+ "@material-ui/core": "^4.9.1",
+ "@material-ui/icons": "^4.9.1",
+ "formik": "2.1.4"
+ },
+ "scripts": {
+ "lint": "web-scripts lint",
+ "test": "web-scripts test"
+ },
+ "license": "Apache-2.0"
+}
diff --git a/frontend/packages/plugins/create-entity/src/components/CreateEntityFormPage/CreateEntityFormPage.tsx b/frontend/packages/plugins/create-entity/src/components/CreateEntityFormPage/CreateEntityFormPage.tsx
new file mode 100644
index 0000000000..6482aff6bf
--- /dev/null
+++ b/frontend/packages/plugins/create-entity/src/components/CreateEntityFormPage/CreateEntityFormPage.tsx
@@ -0,0 +1,61 @@
+import React, { Fragment } from 'react';
+import { useRouteMatch } from 'react-router-dom';
+import { useFormik } from 'formik';
+import { Button, TextField, makeStyles } from '@material-ui/core';
+import { InfoCard } from '@backstage/core';
+
+const useStyles = makeStyles(theme => ({
+ formGroup: {
+ padding: theme.spacing(2),
+ },
+}));
+
+const CreateEntityFormPage = () => {
+ const classes = useStyles();
+ const match = useRouteMatch<{ templateId: string }>();
+ const templateId = decodeURIComponent(match.params.templateId);
+
+ const formik = useFormik({
+ initialValues: {
+ entityId: '',
+ description: '',
+ },
+ onSubmit: (values: any) => {
+ alert(JSON.stringify(values, null, 2));
+ },
+ });
+
+ return (
+
+
+
+
+
+ );
+};
+
+export default CreateEntityFormPage;
diff --git a/frontend/packages/plugins/create-entity/src/components/CreateEntityFormPage/index.ts b/frontend/packages/plugins/create-entity/src/components/CreateEntityFormPage/index.ts
new file mode 100644
index 0000000000..e5eabfdf87
--- /dev/null
+++ b/frontend/packages/plugins/create-entity/src/components/CreateEntityFormPage/index.ts
@@ -0,0 +1 @@
+export { default } from './CreateEntityFormPage';
diff --git a/frontend/packages/plugins/create-entity/src/components/CreateEntityPage/CreateEntityPage.test.tsx b/frontend/packages/plugins/create-entity/src/components/CreateEntityPage/CreateEntityPage.test.tsx
new file mode 100644
index 0000000000..5e84fa9a51
--- /dev/null
+++ b/frontend/packages/plugins/create-entity/src/components/CreateEntityPage/CreateEntityPage.test.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import { render } from '@testing-library/react';
+import CreateEntityPage from './CreateEntityPage';
+
+describe('CreateEntityPage', () => {
+ it('should render', () => {
+ const rendered = render();
+ expect(
+ rendered.getByText('Create New', { exact: false }),
+ ).toBeInTheDocument();
+ });
+});
diff --git a/frontend/packages/plugins/create-entity/src/components/CreateEntityPage/CreateEntityPage.tsx b/frontend/packages/plugins/create-entity/src/components/CreateEntityPage/CreateEntityPage.tsx
new file mode 100644
index 0000000000..970267896b
--- /dev/null
+++ b/frontend/packages/plugins/create-entity/src/components/CreateEntityPage/CreateEntityPage.tsx
@@ -0,0 +1,27 @@
+import React, { FC, Fragment, useEffect, useState } from 'react';
+import { InfoCard, TemplateList } from '@backstage/core';
+import { scaffolderV1 } from '@backstage/protobuf-definitions';
+
+const CreateEntityPage: FC<{}> = () => {
+ const [templates, setTemplates] = useState<
+ scaffolderV1.Template.AsObject[]
+ >();
+
+ useEffect(() => {
+ const client = new scaffolderV1.Client('http://localhost:8080');
+ const req = new scaffolderV1.Empty();
+ client.listTemplates(req).then((res: scaffolderV1.ListTemplatesReply) => {
+ setTemplates(res.toObject().templatesList);
+ });
+ }, []);
+
+ return (
+
+
+ {templates && }
+
+
+ );
+};
+
+export default CreateEntityPage;
diff --git a/frontend/packages/plugins/create-entity/src/components/CreateEntityPage/index.ts b/frontend/packages/plugins/create-entity/src/components/CreateEntityPage/index.ts
new file mode 100644
index 0000000000..9593a79ece
--- /dev/null
+++ b/frontend/packages/plugins/create-entity/src/components/CreateEntityPage/index.ts
@@ -0,0 +1 @@
+export { default } from './CreateEntityPage';
diff --git a/frontend/packages/plugins/create-entity/src/index.ts b/frontend/packages/plugins/create-entity/src/index.ts
new file mode 100644
index 0000000000..ee7f3893af
--- /dev/null
+++ b/frontend/packages/plugins/create-entity/src/index.ts
@@ -0,0 +1,2 @@
+export { default } from './plugin';
+export { default as CreateEntityPage } from './components/CreateEntityPage';
diff --git a/frontend/packages/plugins/create-entity/src/plugin.test.ts b/frontend/packages/plugins/create-entity/src/plugin.test.ts
new file mode 100644
index 0000000000..0f9e9609f4
--- /dev/null
+++ b/frontend/packages/plugins/create-entity/src/plugin.test.ts
@@ -0,0 +1,7 @@
+import plugin from './plugin';
+
+describe('create-entity', () => {
+ it('should export plugin', () => {
+ expect(plugin).toBeDefined();
+ });
+});
diff --git a/frontend/packages/plugins/create-entity/src/plugin.ts b/frontend/packages/plugins/create-entity/src/plugin.ts
new file mode 100644
index 0000000000..627f7da037
--- /dev/null
+++ b/frontend/packages/plugins/create-entity/src/plugin.ts
@@ -0,0 +1,11 @@
+import { createPlugin } from '@backstage/core';
+import CreateEntityPage from './components/CreateEntityPage';
+import CreateEntityFormPage from './components/CreateEntityFormPage';
+
+export default createPlugin({
+ id: 'create-entity',
+ register({ router }) {
+ router.registerRoute('/create', CreateEntityPage);
+ router.registerRoute('/create/:templateId', CreateEntityFormPage);
+ },
+});
\ No newline at end of file
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 8873cbc461..a4961f9b5c 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -5733,6 +5733,11 @@ deep-is@~0.1.3:
resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
+deepmerge@^2.1.1:
+ version "2.2.1"
+ resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170"
+ integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==
+
default-gateway@^4.2.0:
version "4.2.0"
resolved "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz#167104c7500c2115f6dd69b0a536bb8ed720552b"
@@ -7122,6 +7127,20 @@ form-data@~2.3.2:
combined-stream "^1.0.6"
mime-types "^2.1.12"
+formik@2.1.4:
+ version "2.1.4"
+ resolved "https://registry.npmjs.org/formik/-/formik-2.1.4.tgz#8deef07ec845ea98f75e03da4aad7aab4ac46570"
+ integrity sha512-oKz8S+yQBzuQVSEoxkqqJrKQS5XJASWGVn6mrs+oTWrBoHgByVwwI1qHiVc9GKDpZBU9vAxXYAKz2BvujlwunA==
+ dependencies:
+ deepmerge "^2.1.1"
+ hoist-non-react-statics "^3.3.0"
+ lodash "^4.17.14"
+ lodash-es "^4.17.14"
+ react-fast-compare "^2.0.1"
+ scheduler "^0.18.0"
+ tiny-warning "^1.0.2"
+ tslib "^1.10.0"
+
forwarded@~0.1.2:
version "0.1.2"
resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
@@ -7783,7 +7802,7 @@ hoist-non-react-statics@^2.3.1:
resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==
-hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.2.1, hoist-non-react-statics@^3.3.2:
+hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.2.1, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
version "3.3.2"
resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@@ -10347,6 +10366,11 @@ lockfile@^1.0.4:
dependencies:
signal-exit "^3.0.2"
+lodash-es@^4.17.14:
+ version "4.17.15"
+ resolved "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78"
+ integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==
+
lodash._baseuniq@~4.6.0:
version "4.6.0"
resolved "https://registry.npmjs.org/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8"
@@ -13470,7 +13494,7 @@ react-error-overlay@^6.0.5:
resolved "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.5.tgz#55d59c2a3810e8b41922e0b4e5f85dcf239bd533"
integrity sha512-+DMR2k5c6BqMDSMF8hLH0vYKtKTeikiFW+fj0LClN+XZg4N9b8QUAdHC62CGWNLTi/gnuuemNcNcTFrCvK1f+A==
-react-fast-compare@^2.0.2, react-fast-compare@^2.0.4:
+react-fast-compare@^2.0.1, react-fast-compare@^2.0.2, react-fast-compare@^2.0.4:
version "2.0.4"
resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==