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==