Merge pull request #73 from spotify/soapraj/create-entity-view
Soapraj/create entity view
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -48,4 +48,7 @@ services:
|
||||
args:
|
||||
service: scaffolder
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./backend/scaffolder/templates:/app/templates:ro
|
||||
|
||||
env_file: secrets.env
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -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 (
|
||||
<Card key={item.id} className={classes.card}>
|
||||
<TemplateCardMedia name={item.name} />
|
||||
{item.description && (
|
||||
<CardContent>
|
||||
{isGoldenPath && (
|
||||
<Chip
|
||||
label="Golden Path"
|
||||
color="secondary"
|
||||
classes={{ root: classes.chip }}
|
||||
/>
|
||||
)}
|
||||
{isExperimental && <Chip label="Experimental" />}
|
||||
<Typography component="div">{item.description}</Typography>
|
||||
</CardContent>
|
||||
)}
|
||||
<CardActions className={classes.cardActions}>
|
||||
<Link href={item.link || `/create/${item.id}`}>
|
||||
<Button color="primary">{item.callToAction || 'Choose'}</Button>
|
||||
</Link>
|
||||
</CardActions>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(TemplateCard);
|
||||
@@ -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 (
|
||||
<div className={classes.media}>
|
||||
<Typography color="inherit">{ownerName}</Typography>
|
||||
<Typography color="inherit" className={classes.title} variant="h6">
|
||||
{name}
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(TemplateCardMedia);
|
||||
@@ -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 (
|
||||
<Fragment>
|
||||
<div className={classes.container}>
|
||||
{items.map(item => (
|
||||
<TemplateCard
|
||||
key={item.id}
|
||||
item={item}
|
||||
isGoldenPath={TemplateList.isGoldenPathItem(item)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(CardLayoutStyles)(TemplateList);
|
||||
@@ -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';
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Welcome to your create-entity plugin!
|
||||
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
...require('@spotify/web-scripts/config/jest.config.js'),
|
||||
setupFilesAfterEnv: ['../jest.setup.ts'],
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
@@ -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"
|
||||
}
|
||||
+61
@@ -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 (
|
||||
<Fragment>
|
||||
<InfoCard title={`Create New ${templateId}`}>
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<div className={classes.formGroup}>
|
||||
<TextField
|
||||
label="Entity Id:"
|
||||
name="entity-id"
|
||||
id="entity-id"
|
||||
value={formik.values.entityId}
|
||||
variant="outlined"
|
||||
></TextField>
|
||||
</div>
|
||||
<div className={classes.formGroup}>
|
||||
<TextField
|
||||
label="Description:"
|
||||
name="description"
|
||||
id="description"
|
||||
value={formik.values.description}
|
||||
variant="outlined"
|
||||
></TextField>
|
||||
</div>
|
||||
<div className={classes.formGroup}>
|
||||
<Button variant="contained" color="primary">
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</InfoCard>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateEntityFormPage;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './CreateEntityFormPage';
|
||||
+12
@@ -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(<CreateEntityPage />);
|
||||
expect(
|
||||
rendered.getByText('Create New', { exact: false }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
+27
@@ -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 (
|
||||
<Fragment>
|
||||
<InfoCard title="Create New ...">
|
||||
{templates && <TemplateList items={templates}></TemplateList>}
|
||||
</InfoCard>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateEntityPage;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './CreateEntityPage';
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from './plugin';
|
||||
export { default as CreateEntityPage } from './components/CreateEntityPage';
|
||||
@@ -0,0 +1,7 @@
|
||||
import plugin from './plugin';
|
||||
|
||||
describe('create-entity', () => {
|
||||
it('should export plugin', () => {
|
||||
expect(plugin).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
+26
-2
@@ -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==
|
||||
|
||||
Reference in New Issue
Block a user