From 923e5abd17e1405d501f1ca7b2605f1c80bf98db Mon Sep 17 00:00:00 2001 From: Raghunandan Balachandran Date: Wed, 5 Feb 2020 00:42:34 +0100 Subject: [PATCH 01/12] bring in Page, Header, InfoCard, ErrorBoundary, BackstageTheme, Navigation, SupportButtons, HeaderAction, HeaderLabel components from backstage frontend --- frontend/packages/app/src/App.tsx | 72 ++-- .../HomepageTimer/HomepageTimer.tsx | 58 +++ .../app/src/components/HomepageTimer/index.ts | 1 + .../src/components/PageHeader/PageHeader.tsx | 28 -- .../app/src/components/PageHeader/index.ts | 1 - frontend/packages/core/package.json | 5 +- .../core/src/assets/bursts/bursts.css | 19 + .../core/src/assets/bursts/circle-1.svg | 1 + .../core/src/assets/bursts/circle-2.svg | 1 + .../core/src/assets/bursts/rectangle-1.svg | 1 + .../core/src/assets/bursts/rectangle-2.svg | 1 + .../core/src/assets/bursts/rectangle-3.svg | 1 + .../core/src/assets/bursts/triangle-1.svg | 1 + .../core/src/assets/bursts/triangle-2.svg | 1 + .../core/src/assets/bursts/triangle-3.svg | 1 + frontend/packages/core/src/index.ts | 10 + .../src/layout/ContentHeader/ContentHeader.js | 69 ++++ .../src/layout/ErrorBoundary/ErrorBoundary.js | 46 +++ .../core/src/layout/ErrorBoundary/index.ts | 1 + .../packages/core/src/layout/Header/Burst.js | 16 + .../packages/core/src/layout/Header/Header.js | 141 +++++++ .../core/src/layout/Header/Header.test.js | 37 ++ .../packages/core/src/layout/Header/index.ts | 1 + .../HeaderActionMenu/HeaderActionMenu.js | 56 +++ .../HeaderActionMenu/HeaderActionMenu.test.js | 70 ++++ .../src/layout/HeaderLabel/HeaderLabel.js | 50 +++ .../layout/HeaderLabel/HeaderLabel.test.js | 28 ++ .../layout/HeaderLabel/OwnerHeaderLabel.js | 65 ++++ .../HeaderLabel/OwnerHeaderLabel.test.js | 37 ++ .../core/src/layout/HeaderLabel/index.ts | 1 + .../core/src/layout/InfoCard/BottomLink.js | 31 ++ .../src/layout/InfoCard/BottomLink.test.js | 17 + .../core/src/layout/InfoCard/InfoCard.js | 197 ++++++++++ .../core/src/layout/InfoCard/InfoCard.test.js | 24 ++ .../core/src/layout/InfoCard/index.js | 1 + .../core/src/layout/Navigation/NavItem.js | 168 ++++++++ .../src/layout/Navigation/NavItem.test.js | 95 +++++ .../core/src/layout/Navigation/Navigation.js | 91 +++++ .../src/layout/Navigation/Navigation.test.js | 57 +++ .../packages/core/src/layout/Page/Page.js | 37 ++ .../core/src/layout/Page/PageThemeProvider.js | 172 +++++++++ .../packages/core/src/layout/Page/index.ts | 2 + .../src/layout/SupportButton/SupportButton.js | 114 ++++++ .../SupportButton/SupportButton.test.js | 36 ++ .../packages/core/src/theme/BackstageTheme.js | 358 ++++++++++++++++++ .../components/MyComponent/MyComponent.tsx | 8 +- frontend/yarn.lock | 148 +++++++- 47 files changed, 2311 insertions(+), 65 deletions(-) create mode 100644 frontend/packages/app/src/components/HomepageTimer/HomepageTimer.tsx create mode 100644 frontend/packages/app/src/components/HomepageTimer/index.ts delete mode 100644 frontend/packages/app/src/components/PageHeader/PageHeader.tsx delete mode 100644 frontend/packages/app/src/components/PageHeader/index.ts create mode 100644 frontend/packages/core/src/assets/bursts/bursts.css create mode 100644 frontend/packages/core/src/assets/bursts/circle-1.svg create mode 100644 frontend/packages/core/src/assets/bursts/circle-2.svg create mode 100644 frontend/packages/core/src/assets/bursts/rectangle-1.svg create mode 100644 frontend/packages/core/src/assets/bursts/rectangle-2.svg create mode 100644 frontend/packages/core/src/assets/bursts/rectangle-3.svg create mode 100644 frontend/packages/core/src/assets/bursts/triangle-1.svg create mode 100644 frontend/packages/core/src/assets/bursts/triangle-2.svg create mode 100644 frontend/packages/core/src/assets/bursts/triangle-3.svg create mode 100644 frontend/packages/core/src/layout/ContentHeader/ContentHeader.js create mode 100644 frontend/packages/core/src/layout/ErrorBoundary/ErrorBoundary.js create mode 100644 frontend/packages/core/src/layout/ErrorBoundary/index.ts create mode 100644 frontend/packages/core/src/layout/Header/Burst.js create mode 100644 frontend/packages/core/src/layout/Header/Header.js create mode 100644 frontend/packages/core/src/layout/Header/Header.test.js create mode 100644 frontend/packages/core/src/layout/Header/index.ts create mode 100644 frontend/packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.js create mode 100644 frontend/packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.test.js create mode 100644 frontend/packages/core/src/layout/HeaderLabel/HeaderLabel.js create mode 100644 frontend/packages/core/src/layout/HeaderLabel/HeaderLabel.test.js create mode 100644 frontend/packages/core/src/layout/HeaderLabel/OwnerHeaderLabel.js create mode 100644 frontend/packages/core/src/layout/HeaderLabel/OwnerHeaderLabel.test.js create mode 100644 frontend/packages/core/src/layout/HeaderLabel/index.ts create mode 100644 frontend/packages/core/src/layout/InfoCard/BottomLink.js create mode 100644 frontend/packages/core/src/layout/InfoCard/BottomLink.test.js create mode 100644 frontend/packages/core/src/layout/InfoCard/InfoCard.js create mode 100644 frontend/packages/core/src/layout/InfoCard/InfoCard.test.js create mode 100644 frontend/packages/core/src/layout/InfoCard/index.js create mode 100644 frontend/packages/core/src/layout/Navigation/NavItem.js create mode 100644 frontend/packages/core/src/layout/Navigation/NavItem.test.js create mode 100644 frontend/packages/core/src/layout/Navigation/Navigation.js create mode 100644 frontend/packages/core/src/layout/Navigation/Navigation.test.js create mode 100644 frontend/packages/core/src/layout/Page/Page.js create mode 100644 frontend/packages/core/src/layout/Page/PageThemeProvider.js create mode 100644 frontend/packages/core/src/layout/Page/index.ts create mode 100644 frontend/packages/core/src/layout/SupportButton/SupportButton.js create mode 100644 frontend/packages/core/src/layout/SupportButton/SupportButton.test.js create mode 100644 frontend/packages/core/src/theme/BackstageTheme.js diff --git a/frontend/packages/app/src/App.tsx b/frontend/packages/app/src/App.tsx index f51cf54d0b..0d82f79aca 100644 --- a/frontend/packages/app/src/App.tsx +++ b/frontend/packages/app/src/App.tsx @@ -1,9 +1,9 @@ -import React, { FC, Fragment } from 'react'; +import React, { FC } from 'react'; import helloWorld, { MyComponent } from '@backstage/plugin-hello-world'; -import { makeStyles } from '@material-ui/core/styles'; import Typography from '@material-ui/core/Typography'; import SideBar from './components/SideBar'; -import PageHeader from './components/PageHeader'; +//import PageHeader from './components/PageHeader'; +import { Header, Page, InfoCard } from '@backstage/core'; import { LoginComponent } from '@backstage/plugin-login'; import { BrowserRouter as Router, @@ -11,6 +11,15 @@ import { Route, Link as RouterLink, } from 'react-router-dom'; +import { BackstageTheme, withGlobalStyles, theme } from '@backstage/core'; +import { CssBaseline, MuiThemeProvider, makeStyles } from '@material-ui/core'; +import { ThemeProvider } from '@material-ui/styles'; +import { StylesProvider, createGenerateClassName } from '@material-ui/styles'; +import HomePageTimer from './components/HomepageTimer'; + +const generateClassName = createGenerateClassName({ + productionPrefix: 'stajl', +}); const useStyles = makeStyles(theme => ({ root: { @@ -26,8 +35,7 @@ const useStyles = makeStyles(theme => ({ overflowY: 'auto', }, pageBody: { - paddingLeft: theme.spacing(2), - paddingTop: theme.spacing(2), + padding: theme.spacing(2), }, avatarButton: { padding: theme.spacing(2), @@ -36,24 +44,32 @@ const useStyles = makeStyles(theme => ({ const App: FC<{}> = () => { return ( - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + ); }; const Home: FC<{}> = () => { return ( - + {' '} …with plugin {helloWorld?.id ?? 'wat'}: @@ -62,18 +78,18 @@ const Home: FC<{}> = () => {
Go to Login
-
+ ); }; const Login: FC<{}> = () => { return ( - +
Go to Home
-
+ ); }; @@ -83,12 +99,18 @@ const AppShell: FC<{}> = ({ children }) => { return (
-
- -
{children}
-
+ +
+
+ +
+
{children}
+
+
); }; +const AppContent = withGlobalStyles(AppShell); + export default App; diff --git a/frontend/packages/app/src/components/HomepageTimer/HomepageTimer.tsx b/frontend/packages/app/src/components/HomepageTimer/HomepageTimer.tsx new file mode 100644 index 0000000000..54d269bf9c --- /dev/null +++ b/frontend/packages/app/src/components/HomepageTimer/HomepageTimer.tsx @@ -0,0 +1,58 @@ +import React, { FC } from 'react'; +import { HeaderLabel } from '@backstage/core'; + +const timeFormat = { hour: '2-digit', minute: '2-digit' }; +const nycOptions = { timeZone: 'America/New_York', ...timeFormat }; +const utcOptions = { timeZone: 'UTC', ...timeFormat }; +const lonOptions = { timeZone: 'Europe/London', ...timeFormat }; +const stoOptions = { timeZone: 'Europe/Stockholm', ...timeFormat }; + +const defaultTimes = { + timeNY: '', + timeUTC: '', + timeLON: '', + timeSTO: '', +}; + +function getTimes() { + const d = new Date(); + const lang = window.navigator.language; + + // Using the browser native toLocaleTimeString instead of huge moment-tz + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleTimeString + const timeNY = d.toLocaleTimeString(lang, nycOptions); + const timeUTC = d.toLocaleTimeString(lang, utcOptions); + const timeLON = d.toLocaleTimeString(lang, lonOptions); + const timeSTO = d.toLocaleTimeString(lang, stoOptions); + + return { timeNY, timeUTC, timeLON, timeSTO }; +} + +const HomePageTimer: FC<{}> = () => { + const [{ timeNY, timeUTC, timeLON, timeSTO }, setTimes] = React.useState( + defaultTimes, + ); + + React.useEffect(() => { + setTimes(getTimes()); + + const intervalId = setInterval(() => { + setTimes(getTimes()); + }, 1000); + + return () => { + clearInterval(intervalId); + }; + }, []); + + return ( + <> + + + + + + ); +}; + +export default HomePageTimer; diff --git a/frontend/packages/app/src/components/HomepageTimer/index.ts b/frontend/packages/app/src/components/HomepageTimer/index.ts new file mode 100644 index 0000000000..122eef2c23 --- /dev/null +++ b/frontend/packages/app/src/components/HomepageTimer/index.ts @@ -0,0 +1 @@ +export { default } from './HomepageTimer'; diff --git a/frontend/packages/app/src/components/PageHeader/PageHeader.tsx b/frontend/packages/app/src/components/PageHeader/PageHeader.tsx deleted file mode 100644 index 38f77ddbc5..0000000000 --- a/frontend/packages/app/src/components/PageHeader/PageHeader.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { makeStyles } from '@material-ui/core/styles'; -import Typography from '@material-ui/core/Typography'; -import React, { FC } from 'react'; - -const useStyles = makeStyles(theme => ({ - root: { - background: 'linear-gradient(262.63deg, #19D15A 4.2%, #1CAB5B 72.01%)', - height: '120px', - paddingTop: theme.spacing(2), - paddingLeft: theme.spacing(2), - color: 'white', - }, -})); - -const SideBar: FC<{}> = () => { - const classes = useStyles(); - - return ( -
- This is Backstage! - - Backstage is an open platform for building developer portals - -
- ); -}; - -export default SideBar; diff --git a/frontend/packages/app/src/components/PageHeader/index.ts b/frontend/packages/app/src/components/PageHeader/index.ts deleted file mode 100644 index e1b036be68..0000000000 --- a/frontend/packages/app/src/components/PageHeader/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './PageHeader'; diff --git a/frontend/packages/core/package.json b/frontend/packages/core/package.json index 27c8453567..fb5f3cc6de 100644 --- a/frontend/packages/core/package.json +++ b/frontend/packages/core/package.json @@ -13,7 +13,10 @@ "@types/react": "^16.9.0", "@types/react-dom": "^16.9.0", "react": "^16.12.0", - "react-dom": "^16.12.0" + "react-dom": "^16.12.0", + "react-helmet":"5.2.1", + "tinycolor2": "1.4.1", + "react-addons-text-content": "0.0.4" }, "scripts": { "lint": "web-scripts lint", diff --git a/frontend/packages/core/src/assets/bursts/bursts.css b/frontend/packages/core/src/assets/bursts/bursts.css new file mode 100644 index 0000000000..92499bf056 --- /dev/null +++ b/frontend/packages/core/src/assets/bursts/bursts.css @@ -0,0 +1,19 @@ + +.burst { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + z-index: -1; +} + +/* base style of burst shape SVGs */ +.burst-shape { + position: absolute; + right: 0; + height: 100%; + background-repeat: no-repeat; + background-size: cover; + opacity:0.1; +} diff --git a/frontend/packages/core/src/assets/bursts/circle-1.svg b/frontend/packages/core/src/assets/bursts/circle-1.svg new file mode 100644 index 0000000000..9ee5157ab8 --- /dev/null +++ b/frontend/packages/core/src/assets/bursts/circle-1.svg @@ -0,0 +1 @@ + diff --git a/frontend/packages/core/src/assets/bursts/circle-2.svg b/frontend/packages/core/src/assets/bursts/circle-2.svg new file mode 100644 index 0000000000..15b89e96db --- /dev/null +++ b/frontend/packages/core/src/assets/bursts/circle-2.svg @@ -0,0 +1 @@ + diff --git a/frontend/packages/core/src/assets/bursts/rectangle-1.svg b/frontend/packages/core/src/assets/bursts/rectangle-1.svg new file mode 100644 index 0000000000..4403b0031a --- /dev/null +++ b/frontend/packages/core/src/assets/bursts/rectangle-1.svg @@ -0,0 +1 @@ + diff --git a/frontend/packages/core/src/assets/bursts/rectangle-2.svg b/frontend/packages/core/src/assets/bursts/rectangle-2.svg new file mode 100644 index 0000000000..2d307d75f6 --- /dev/null +++ b/frontend/packages/core/src/assets/bursts/rectangle-2.svg @@ -0,0 +1 @@ + diff --git a/frontend/packages/core/src/assets/bursts/rectangle-3.svg b/frontend/packages/core/src/assets/bursts/rectangle-3.svg new file mode 100644 index 0000000000..947df51df9 --- /dev/null +++ b/frontend/packages/core/src/assets/bursts/rectangle-3.svg @@ -0,0 +1 @@ + diff --git a/frontend/packages/core/src/assets/bursts/triangle-1.svg b/frontend/packages/core/src/assets/bursts/triangle-1.svg new file mode 100644 index 0000000000..7a33cf3f5c --- /dev/null +++ b/frontend/packages/core/src/assets/bursts/triangle-1.svg @@ -0,0 +1 @@ + diff --git a/frontend/packages/core/src/assets/bursts/triangle-2.svg b/frontend/packages/core/src/assets/bursts/triangle-2.svg new file mode 100644 index 0000000000..184e714b89 --- /dev/null +++ b/frontend/packages/core/src/assets/bursts/triangle-2.svg @@ -0,0 +1 @@ + diff --git a/frontend/packages/core/src/assets/bursts/triangle-3.svg b/frontend/packages/core/src/assets/bursts/triangle-3.svg new file mode 100644 index 0000000000..e8b0cb1f7c --- /dev/null +++ b/frontend/packages/core/src/assets/bursts/triangle-3.svg @@ -0,0 +1 @@ + diff --git a/frontend/packages/core/src/index.ts b/frontend/packages/core/src/index.ts index 0b69aa4ff3..f741f0e384 100644 --- a/frontend/packages/core/src/index.ts +++ b/frontend/packages/core/src/index.ts @@ -1,2 +1,12 @@ export * from './types'; export { default as createPlugin } from './createPlugin'; +export { default as Page } from '../src/layout/Page'; +export { shapes, gradients, theme } from '../src/layout/Page'; +export { default as Header } from '../src/layout/Header/Header'; +export { default as HeaderLabel } from '../src/layout/HeaderLabel'; +export { default as InfoCard } from '../src/layout/InfoCard'; +export { default as ErrorBoundary } from '../src/layout/ErrorBoundary'; + +export { default as BackstageTheme } from '../src/theme/BackstageTheme'; +export { withGlobalStyles } from '../src/theme/BackstageTheme'; + diff --git a/frontend/packages/core/src/layout/ContentHeader/ContentHeader.js b/frontend/packages/core/src/layout/ContentHeader/ContentHeader.js new file mode 100644 index 0000000000..ed3e69d37a --- /dev/null +++ b/frontend/packages/core/src/layout/ContentHeader/ContentHeader.js @@ -0,0 +1,69 @@ +import React, { Component, Fragment } from 'react'; +import { Typography, withStyles } from '@material-ui/core'; +import Helmet from 'react-helmet'; +import FavoriteButton from 'shared/components/layout/FavoriteButton'; + +const styles = theme => ({ + container: { + width: '100%', + display: 'flex', + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'flex-end', + alignItems: 'center', + }, + leftItemsBox: { + flex: '1 1 auto', + marginBottom: theme.spacing(1), + minWidth: 0, + overflow: 'visible', + }, + rightItemsBox: { + flex: '0 1 auto', + display: 'flex', + flexDirection: 'row', + flexWrap: 'wrap', + alignItems: 'center', + marginLeft: theme.spacing(1), + marginBottom: theme.spacing(1), + minWidth: 0, + overflow: 'visible', + }, + description: {}, + title: { + display: 'inline-flex', + }, +}); + +class ContentHeader extends Component { + static defaultProps = { + favoriteable: true, + title: 'Unknown page', + }; + + render() { + const { title, description, favoriteable, children, classes } = this.props; + + return ( + + +
+
+ + {title} + + {favoriteable && } + {description && ( + + {description} + + )} +
+
{children}
+
+
+ ); + } +} + +export default withStyles(styles)(ContentHeader); diff --git a/frontend/packages/core/src/layout/ErrorBoundary/ErrorBoundary.js b/frontend/packages/core/src/layout/ErrorBoundary/ErrorBoundary.js new file mode 100644 index 0000000000..6963083e41 --- /dev/null +++ b/frontend/packages/core/src/layout/ErrorBoundary/ErrorBoundary.js @@ -0,0 +1,46 @@ +import React, { Component } from 'react'; + +export default class ErrorBoundary extends Component { + constructor(props) { + super(props); + + this.state = { + error: null, + errorInfo: null, + onError: props.onError, + }; + } + + componentDidCatch(error, errorInfo) { + console.error(`ErrorBoundary, error: ${error}, info: ${errorInfo}`); + this.setState({ error, errorInfo }); + + // Exposed for testing + if (ErrorBoundary.onError) { + ErrorBoundary.onError(error, errorInfo); + } + } + + render() { + const { slackChannel } = this.props; + const { error, errorInfo } = this.state; + + if (!errorInfo) { + return this.props.children; + } + + return ( + + ); + } +} + +// Importing Error would mean importing a lot of stuff +// will take it up in a separate PR +const Error = ({ slackChannel }) => { + return ( +
+ Something went wrong here. Please contact {slackChannel} for help. +
+ ); +}; diff --git a/frontend/packages/core/src/layout/ErrorBoundary/index.ts b/frontend/packages/core/src/layout/ErrorBoundary/index.ts new file mode 100644 index 0000000000..257f6a24f6 --- /dev/null +++ b/frontend/packages/core/src/layout/ErrorBoundary/index.ts @@ -0,0 +1 @@ +export { default } from './ErrorBoundary'; diff --git a/frontend/packages/core/src/layout/Header/Burst.js b/frontend/packages/core/src/layout/Header/Burst.js new file mode 100644 index 0000000000..64d95f7e07 --- /dev/null +++ b/frontend/packages/core/src/layout/Header/Burst.js @@ -0,0 +1,16 @@ +import React, { Component } from 'react'; +import '../../assets/bursts/bursts.css'; + +class Burst extends Component { + render() { + const { theme } = this.props; + + return ( +
+
+
+ ); + } +} + +export default Burst; diff --git a/frontend/packages/core/src/layout/Header/Header.js b/frontend/packages/core/src/layout/Header/Header.js new file mode 100644 index 0000000000..f78e43ee76 --- /dev/null +++ b/frontend/packages/core/src/layout/Header/Header.js @@ -0,0 +1,141 @@ +import React, { Component, Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { Typography, withStyles, Tooltip } from '@material-ui/core'; +import { Theme } from '../Page/Page'; +// import { Link } from 'shared/components'; +import Burst from './Burst'; +import Helmet from 'react-helmet'; + +class Header extends Component { + static propTypes = { + type: PropTypes.string, + typeLink: PropTypes.string, + title: PropTypes.node.isRequired, + tooltip: PropTypes.string, + subtitle: PropTypes.node, + pageTitleOverride: PropTypes.string, + style: PropTypes.object, + component: PropTypes.object, + }; + + typeFragment() { + const { type, typeLink, classes } = this.props; + if (!type) { + return null; + } + + return typeLink ? ( + // + {type} + // + ) : ( + {type} + ); + } + + titleFragment() { + const { title, pageTitleOverride, classes, tooltip } = this.props; + const FinalTitle = ( + + {title || pageTitleOverride} + + ); + if (tooltip) { + return ( + + {FinalTitle} + + ); + } + return FinalTitle; + } + + subtitleFragment() { + const { subtitle, classes } = this.props; + if (!subtitle) { + return null; + } else if (typeof subtitle !== 'string') { + return subtitle; + } else { + return ( + + {subtitle} + + ); + } + } + + render() { + const { title, pageTitleOverride, children, style, classes } = this.props; + const pageTitle = pageTitleOverride || title; + if (typeof pageTitle !== 'string') { + console.warn('
title prop is not a string, pageTitleOverride should be provided.'); + } + + return ( + + + + {theme => ( +
+ +
+ {this.typeFragment()} + {this.titleFragment()} + {this.subtitleFragment()} +
+
{children}
+
+ )} +
+
+ ); + } +} + +const styles = theme => ({ + header: { + gridArea: 'pageHeader', + padding: theme.spacing(3), + minHeight: 118, + width: 'calc(100vw - 224px)', + boxShadow: '0 0 8px 3px rgba(20, 20, 20, 0.3)', + position: 'relative', + zIndex: 100, + display: 'flex', + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'flex-end', + alignItems: 'center', + }, + leftItemsBox: { + flex: '1 1 auto', + }, + rightItemsBox: { + flex: '0 1 auto', + display: 'flex', + flexDirection: 'row', + flexWrap: 'wrap', + alignItems: 'center', + }, + title: { + color: theme.palette.bursts.fontColor, + lineHeight: '1.0em', + wordBreak: 'break-all', + fontSize: 'calc(24px + 6 * ((100vw - 320px) / 680))', + marginBottom: theme.spacing(1), + }, + subtitle: { + color: 'rgba(255, 255, 255, 0.8)', + lineHeight: '1.0em', + }, + type: { + textTransform: 'uppercase', + fontSize: 9, + opacity: 0.8, + marginBottom: 10, + color: theme.palette.bursts.fontColor, + }, +}); + +export default withStyles(styles)(Header); diff --git a/frontend/packages/core/src/layout/Header/Header.test.js b/frontend/packages/core/src/layout/Header/Header.test.js new file mode 100644 index 0000000000..15d5f3f4b4 --- /dev/null +++ b/frontend/packages/core/src/layout/Header/Header.test.js @@ -0,0 +1,37 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { Header } from 'shared/components/layout'; +import { wrapInThemedTestApp } from 'testUtils'; + +jest.mock('react-helmet', () => { + return ({ defaultTitle }) =>
defaultTitle: {defaultTitle}
; +}); + +describe('
', () => { + it('should render with title', () => { + const rendered = render(wrapInThemedTestApp(
)); + rendered.getByText('Title'); + }); + + it('should set document title', () => { + const rendered = render(wrapInThemedTestApp(
)); + rendered.getByText('Title1'); + rendered.getByText('defaultTitle: Title1 | Backstage'); + }); + + it('should override document title', () => { + const rendered = render(wrapInThemedTestApp(
)); + rendered.getByText('Title1'); + rendered.getByText('defaultTitle: Title2 | Backstage'); + }); + + it('should have subtitle', () => { + const rendered = render(wrapInThemedTestApp(
)); + rendered.getByText('Subtitle'); + }); + + it('should have type rendered', () => { + const rendered = render(wrapInThemedTestApp(
)); + rendered.getByText('service'); + }); +}); diff --git a/frontend/packages/core/src/layout/Header/index.ts b/frontend/packages/core/src/layout/Header/index.ts new file mode 100644 index 0000000000..579f1ac23f --- /dev/null +++ b/frontend/packages/core/src/layout/Header/index.ts @@ -0,0 +1 @@ +export { default } from './Header'; diff --git a/frontend/packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.js b/frontend/packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.js new file mode 100644 index 0000000000..d16556d4c2 --- /dev/null +++ b/frontend/packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.js @@ -0,0 +1,56 @@ +import React, { Fragment } from 'react'; +import { IconButton, List, ListItem, ListItemIcon, ListItemText, Popover } from '@material-ui/core'; +import { KebabMenuIcon } from 'shared/icons'; + +const ActionItem = ({ label, secondaryLabel, icon, disabled = false, onClick, WrapperComponent = React.Fragment }) => { + return ( + + { + if (onClick) { + onClick(event); + } + }} + > + {icon && {icon}} + + + + ); +}; + +const HeaderActionMenu = ({ actionItems }) => { + const [open, setOpen] = React.useState(false); + const anchorElRef = React.useRef(null); + + return ( + + setOpen(true)} + data-testid="header-action-menu" + ref={anchorElRef} + style={{ color: 'white', height: 56, width: 56, marginRight: -4, padding: 0 }} + > + + + setOpen(false)} + > + + {actionItems.map(actionItem => { + return ; + })} + + + + ); +}; + +export default HeaderActionMenu; diff --git a/frontend/packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.test.js b/frontend/packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.test.js new file mode 100644 index 0000000000..dcfecf5e3f --- /dev/null +++ b/frontend/packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.test.js @@ -0,0 +1,70 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import { wrapInThemedTestApp, Keyboard } from 'testUtils'; +import HeaderActionMenu from './HeaderActionMenu'; + +describe('', () => { + it('renders without any items and without exploding', () => { + render(wrapInThemedTestApp()); + }); + + it('can open the menu and click menu items', () => { + const onClickFunction = jest.fn(); + const rendered = render( + wrapInThemedTestApp(), + ); + expect(rendered.queryByText('Some label')).not.toBeInTheDocument(); + expect(onClickFunction).not.toHaveBeenCalled(); + fireEvent.click(rendered.getByTestId('header-action-menu')); + expect(onClickFunction).not.toHaveBeenCalled(); + expect(rendered.getByTestId('header-action-item')).not.toHaveAttribute('aria-disabled', 'true'); + fireEvent.click(rendered.queryByText('Some label')); + expect(onClickFunction).toHaveBeenCalled(); + // We do not expect the dropdown to disappear after click + expect(rendered.queryByText('Some label')).toBeInTheDocument(); + }); + + it('Disabled', async () => { + const rendered = render( + wrapInThemedTestApp(), + ); + + fireEvent.click(rendered.getByTestId('header-action-menu')); + expect(rendered.getByTestId('header-action-item')).toHaveAttribute('aria-disabled', 'true'); + }); + + it('Test wrapper, and secondary label', () => { + const onClickFunction = jest.fn(); + const rendered = render( + wrapInThemedTestApp( + , + }, + ]} + />, + ), + ); + + expect(onClickFunction).not.toHaveBeenCalled(); + fireEvent.click(rendered.getByTestId('header-action-menu')); + expect(onClickFunction).not.toHaveBeenCalled(); + fireEvent.click(rendered.queryByText('Secondary label')); + expect(onClickFunction).toHaveBeenCalled(); + // We do not expect the dropdown to disappear after click + expect(rendered.queryByText('Some label')).toBeInTheDocument(); + }); + + it('should close when hitting escape', async () => { + const rendered = render(wrapInThemedTestApp()); + + expect(rendered.container.getAttribute('aria-hidden')).toBeNull(); + fireEvent.click(rendered.getByTestId('header-action-menu')); + expect(rendered.container.getAttribute('aria-hidden')).toBe('true'); + await Keyboard.type(rendered, ''); + expect(rendered.container.getAttribute('aria-hidden')).toBeNull(); + }); +}); diff --git a/frontend/packages/core/src/layout/HeaderLabel/HeaderLabel.js b/frontend/packages/core/src/layout/HeaderLabel/HeaderLabel.js new file mode 100644 index 0000000000..91290412b3 --- /dev/null +++ b/frontend/packages/core/src/layout/HeaderLabel/HeaderLabel.js @@ -0,0 +1,50 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { Typography, withStyles } from '@material-ui/core'; +import { Link } from '@material-ui/core'; + +const style = theme => ({ + root: { + textAlign: 'left', + margin: theme.spacing(2), + display: 'inline-block', + }, + label: { + color: '#FFFFFF', + fontWeight: 'bold', + lineHeight: '16px', + letterSpacing: 0, + fontSize: 14, + height: '16px', + marginBottom: 2, + }, + value: { + color: 'rgba(255, 255, 255, 0.8)', + lineHeight: '16px', + fontSize: 14, + height: '16px', + }, +}); + +class HeaderLabel extends Component { + static propTypes = { + label: PropTypes.string.isRequired, + value: PropTypes.node, + url: PropTypes.string, + }; + + render() { + const { label, value, url, classes } = this.props; + const content = ( + {value || ''} + ); + return ( + + {label} + {url ? {content} : content} + + ); + } +} + +export default withStyles(style)(HeaderLabel); diff --git a/frontend/packages/core/src/layout/HeaderLabel/HeaderLabel.test.js b/frontend/packages/core/src/layout/HeaderLabel/HeaderLabel.test.js new file mode 100644 index 0000000000..06eccae61c --- /dev/null +++ b/frontend/packages/core/src/layout/HeaderLabel/HeaderLabel.test.js @@ -0,0 +1,28 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { HeaderLabel } from 'shared/components/layout'; +import { wrapInThemedTestApp } from 'testUtils'; + +describe('', () => { + it('should have a label', () => { + const rendered = render(wrapInThemedTestApp()); + expect(rendered.getByText('Label')).toBeInTheDocument(); + }); + + it('should say unknown', () => { + const rendered = render(wrapInThemedTestApp()); + expect(rendered.getByText('')).toBeInTheDocument(); + }); + + it('should have value', () => { + const rendered = render(wrapInThemedTestApp()); + expect(rendered.getByText('Value')).toBeInTheDocument(); + }); + + it('should have a link', () => { + const rendered = render(wrapInThemedTestApp()); + const anchor = rendered.container.querySelector('a'); + expect(rendered.getByText('Value')).toBeInTheDocument(); + expect(anchor.href).toBe('http://localhost/test'); + }); +}); diff --git a/frontend/packages/core/src/layout/HeaderLabel/OwnerHeaderLabel.js b/frontend/packages/core/src/layout/HeaderLabel/OwnerHeaderLabel.js new file mode 100644 index 0000000000..5c71c1b6ea --- /dev/null +++ b/frontend/packages/core/src/layout/HeaderLabel/OwnerHeaderLabel.js @@ -0,0 +1,65 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { Tooltip, withStyles } from '@material-ui/core'; + +import { StatusError } from 'shared/components/Status'; +import Link from 'shared/components/Link'; +import HeaderLabel from 'shared/components/layout/HeaderLabel'; + +const style = theme => ({ + notVerified: { + color: theme.palette.status.error, + borderRadius: 4, + padding: '3px 6px', + fontSize: '8pt', + opacity: 0.8, + fontWeight: 'bold', + position: 'relative', + top: -4, + backgroundColor: 'pink', + float: 'right', + marginLeft: 14, + }, + label: { float: 'left' }, +}); + +class OwnerHeaderLabel extends Component { + static propTypes = { + owner: PropTypes.object.isRequired, + }; + + render() { + const { owner, classes } = this.props; + const isBadSquad = owner.type !== 'squad'; + + const notVerified = isBadSquad && ( + + + Squad not verified! + + + ); + const label = ( + + + {owner.name} + + + ); + return ( + <> + + {notVerified} + + ); + } +} + +export default withStyles(style)(OwnerHeaderLabel); diff --git a/frontend/packages/core/src/layout/HeaderLabel/OwnerHeaderLabel.test.js b/frontend/packages/core/src/layout/HeaderLabel/OwnerHeaderLabel.test.js new file mode 100644 index 0000000000..11ae8f3a89 --- /dev/null +++ b/frontend/packages/core/src/layout/HeaderLabel/OwnerHeaderLabel.test.js @@ -0,0 +1,37 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { OwnerHeaderLabel } from 'shared/components/layout'; +import { wrapInThemedTestApp } from 'testUtils'; + +const properOwner = { id: 'tools', name: 'tools', type: 'squad' }; +const badOwner = { id: 'tools-xxx', name: 'tools-xxx' }; + +describe('', () => { + it('should have a label', () => { + const rendered = render(wrapInThemedTestApp()); + expect(rendered.getByText('Owner')).toBeInTheDocument(); + expect(rendered.getByText('tools')).toBeInTheDocument(); + expect(rendered.queryByText('Squad not verified!')).not.toBeInTheDocument(); + }); + + it('should have an org link', () => { + const rendered = render(wrapInThemedTestApp()); + const anchor = rendered.container.querySelector('a'); + expect(anchor.href).toBe('http://localhost/org/tools'); + }); + + it('should have WARNING label', () => { + const rendered = render(wrapInThemedTestApp()); + expect(rendered.getByText('Squad not verified!')).toBeInTheDocument(); + }); + + it('should have status error label', () => { + const rendered = render(wrapInThemedTestApp()); + expect(rendered.getByLabelText('Status error')).toBeInTheDocument(); + }); + + it('should handle empty input', () => { + const rendered = render(wrapInThemedTestApp()); + expect(rendered.getByLabelText('Status error')).toBeInTheDocument(); + }); +}); diff --git a/frontend/packages/core/src/layout/HeaderLabel/index.ts b/frontend/packages/core/src/layout/HeaderLabel/index.ts new file mode 100644 index 0000000000..913f71a783 --- /dev/null +++ b/frontend/packages/core/src/layout/HeaderLabel/index.ts @@ -0,0 +1 @@ +export { default } from './HeaderLabel'; diff --git a/frontend/packages/core/src/layout/InfoCard/BottomLink.js b/frontend/packages/core/src/layout/InfoCard/BottomLink.js new file mode 100644 index 0000000000..c6b259cb72 --- /dev/null +++ b/frontend/packages/core/src/layout/InfoCard/BottomLink.js @@ -0,0 +1,31 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { Link } from '@material-ui/core'; +import { Divider, ListItemText } from '@material-ui/core'; +import { ListItem, ListItemIcon } from '@material-ui/core'; +import ArrowIcon from '@material-ui/icons/ArrowForward'; + +export default class BottomLink extends Component { + static propTypes = { + link: PropTypes.string, + title: PropTypes.string, + onClick: PropTypes.func, + }; + + render() { + const { link, title, onClick } = this.props; + return ( +
+ + + + + + + {title} + + +
+ ); + } +} diff --git a/frontend/packages/core/src/layout/InfoCard/BottomLink.test.js b/frontend/packages/core/src/layout/InfoCard/BottomLink.test.js new file mode 100644 index 0000000000..3941c53b53 --- /dev/null +++ b/frontend/packages/core/src/layout/InfoCard/BottomLink.test.js @@ -0,0 +1,17 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { wrapInTestApp } from 'testUtils'; + +import BottomLink from './BottomLink'; + +const minProps = { + title: 'A deepLink title', + link: '/mocked', +}; + +describe('', () => { + it('renders without exploding', () => { + const rendered = render(wrapInTestApp()); + expect(rendered.getByText('A deepLink title')).toBeInTheDocument(); + }); +}); diff --git a/frontend/packages/core/src/layout/InfoCard/InfoCard.js b/frontend/packages/core/src/layout/InfoCard/InfoCard.js new file mode 100644 index 0000000000..2a0b332a9f --- /dev/null +++ b/frontend/packages/core/src/layout/InfoCard/InfoCard.js @@ -0,0 +1,197 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { + Card, + CardActions, + CardContent, + CardHeader, + Divider, + withStyles, +} from '@material-ui/core'; +import ErrorBoundary from '../ErrorBoundary/ErrorBoundary'; +import BottomLink from './BottomLink'; + +import textContent from 'react-addons-text-content'; + +const BoldHeader = withStyles({ title: { fontWeight: '700' } })(CardHeader); +const CardActionsTopRight = withStyles({ + root: { + display: 'inline-block', + paddingRight: '16px', + paddingTop: '16px', + float: 'right', + }, +})(CardActions); + +const VARIANT_STYLES = { + card: { + flex: { + display: 'flex', + flexDirection: 'column', + }, + widget: { + height: 430, + }, + fullHeight: { + height: '100%', + }, + height100: { + display: 'flex', + flexDirection: 'column', + height: 'calc(100% - 10px)', // for pages without content header + marginBottom: '10px', + }, + contentheader: { + height: 'calc(100% - 40px)', // for pages with content header + }, + contentheadertabs: { + height: 'calc(100% - 97px)', // for pages with content header and tabs (Tingle) + }, + noShrink: { + flexShrink: 0, + }, + minheight300: { + minHeight: 300, + overflow: 'initial', + }, + }, + cardContent: { + widget: { + overflowY: 'auto', + height: 332, + width: '100%', + }, + fullHeight: { + height: 'calc(100% - 50px)', + }, + height100: { + height: 'calc(100% - 50px)', + }, + contentRow: { + display: 'flex', + flexDirection: 'row', + }, + }, +}; + +/** + * InfoCard is used to display a paper-styled block on the screen, similar to a panel. + * + * You can custom style an InfoCard with the 'style' (outer container) and 'cardStyle' (inner container) + * styles. + * + * The InfoCard serves as an error boundary. As a result, if you provide a 'slackChannel' property this + * specifies the channel to display in the error component that is displayed if an error occurs + * in any descendent components. + * + * By default the InfoCard has no custom layout of its children, but is treated as a block element. A + * couple common variants are provided and can be specified via the variant property: + * + * Display the card full height suitable for DataGrid: + * + * ... + * + * Variants can be combined in a whitespace delimited list like so: + * + * ... + */ +class InfoCard extends Component { + static propTypes = { + title: PropTypes.oneOfType([PropTypes.element, PropTypes.string]), + subheader: PropTypes.oneOfType([PropTypes.node, PropTypes.string]), + divider: PropTypes.bool, + deepLink: PropTypes.object, + slackChannel: PropTypes.string, + variant: PropTypes.string, + }; + + render() { + const { + title, + subheader, + divider, + deepLink, + children, + actions, + actionsTopRight, + headerStyle, + headerProps, + classes, + slackChannel, + variant, + } = this.props; + + if (this.props.style) { + console.warn( + 'InfoCard: using `style` property directly, consider migrating your style to variant in InfoCard', + ); + } + if (this.props.cardStyle) { + console.warn( + 'InfoCard: using `cardStyle` property directly, consider migrating your style to variant in InfoCard', + ); + } + + /** + * If variant is specified, we build up styles for that particular variant for both + * the Card and the CardContent (since these need to be synced) + */ + let calculatedStyle = {}; + let calculatedCardStyle = {}; + + if (variant) { + let variants = variant.split(/[\s]+/g); + variants.forEach(name => { + calculatedStyle = { ...calculatedStyle, ...VARIANT_STYLES.card[name] }; + calculatedCardStyle = { + ...calculatedCardStyle, + ...VARIANT_STYLES.cardContent[name], + }; + }); + } + + // Apply the passed styles on top + const computedStyle = { ...calculatedStyle, ...this.props.style }; + const computedCardStyle = { + ...calculatedCardStyle, + ...this.props.cardStyle, + }; + + return ( + + + {title && ( + + )} + {actionsTopRight && ( + {actionsTopRight} + )} + {divider && } + + {children} + + {actions && ( + + {actions} + + )} + {deepLink && } + + + ); + } +} + +export default InfoCard; diff --git a/frontend/packages/core/src/layout/InfoCard/InfoCard.test.js b/frontend/packages/core/src/layout/InfoCard/InfoCard.test.js new file mode 100644 index 0000000000..88c0e08fa9 --- /dev/null +++ b/frontend/packages/core/src/layout/InfoCard/InfoCard.test.js @@ -0,0 +1,24 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { wrapInTestApp } from 'testUtils'; +import InfoCard from './InfoCard'; + +const minProps = { + title: 'Some title', + deepLink: { + title: 'A deepLink title', + link: '/mocked', + }, +}; + +describe('', () => { + it('renders without exploding', () => { + const rendered = render(wrapInTestApp()); + expect(rendered.getByText('Some title')).toBeInTheDocument(); + }); + + it('renders a deepLink when prop is set', () => { + const rendered = render(wrapInTestApp()); + expect(rendered.getByText('A deepLink title')).toBeInTheDocument(); + }); +}); diff --git a/frontend/packages/core/src/layout/InfoCard/index.js b/frontend/packages/core/src/layout/InfoCard/index.js new file mode 100644 index 0000000000..6e402b0695 --- /dev/null +++ b/frontend/packages/core/src/layout/InfoCard/index.js @@ -0,0 +1 @@ +export { default } from './InfoCard'; diff --git a/frontend/packages/core/src/layout/Navigation/NavItem.js b/frontend/packages/core/src/layout/Navigation/NavItem.js new file mode 100644 index 0000000000..b6d47ab9f6 --- /dev/null +++ b/frontend/packages/core/src/layout/Navigation/NavItem.js @@ -0,0 +1,168 @@ +import React, { Component, Fragment } from 'react'; +import { matchPath } from 'react-router'; +import { ListItem, ListItemIcon, ListItemText, Typography, withStyles, Tooltip, IconButton } from '@material-ui/core'; +import { AlphaLabel, BetaLabel } from 'shared/components/Lifecycle'; +import Link from 'shared/components/Link'; +import { Theme } from 'shared/components/layout/Page'; +import ArrowDropUp from '@material-ui/icons/ArrowDropUp'; +import ArrowDropDown from '@material-ui/icons/ArrowDropDown'; + +const styles = theme => ({ + root: { + display: 'block', + overflow: 'hidden', + '&:first-child': { + borderTopRightRadius: 6, + borderTopLeftRadius: 6, + }, + '&:last-child': { + borderBottomRightRadius: 6, + borderBottomLeftRadius: 6, + }, + }, + child: { + paddingLeft: theme.spacing(7), + '&:last-child': { + borderBottom: '1px solid #727272', + }, + }, + boldLabel: { + color: '#FFFFFF', + fontWeight: 'bolder', + }, + childLabel: { + color: theme.palette.textVerySubtle, + fontSize: 14, + }, + label: { + whiteSpace: 'nowrap', + lineHeight: 1.0, + }, + iconImg: { + width: 24, + height: 24, + }, + icon: { + margin: theme.spacing(0.5, 2, 0.5, 0), + minWidth: 0, + }, + expand: { + color: 'white', + }, +}); + +class NavItem extends Component { + static defaultProps = { + title: 'Mystery Link', + href: '/', + exact: true, // Only used for react-router matchPath to see if this sidebar item is active + icon: null, + isAlpha: false, + isBeta: false, + type: 'other', // Provided by Navigation + child: false, // Whether this is a child of another NavItem (for indenting) + }; + + // If NavItem has React children, the sub-navigation can be expanded or collapsed + state = { + expanded: false, + }; + + static isActive(href, exact) { + return matchPath(window.location.pathname, { path: href, exact: exact }); + } + + maybeActiveNavStyle(theme) { + return NavItem.isActive(this.props.href, this.props.exact) + ? { + background: `linear-gradient(to right, ${theme.activeNavLinkColor} 0px, ${theme.activeNavLinkColor} 6px, #4F4F4F 6px, #4F4F4F 100%)`, + } + : {}; + } + + renderIcon = (condensed, iconElement, title, classes) => { + if (!iconElement) return null; + const listItemIcon = {iconElement}; + return condensed ? ( + + {listItemIcon} + + ) : ( + listItemIcon + ); + }; + + render() { + const { title, href, icon, isAlpha, isBeta, classes, condensed, child, children } = this.props; + const { expanded } = this.state; + const isSection = !!children; + + // Automatically expand nested nav if one of the child elements is active + const isChildActive = React.Children.toArray(children).some(c => NavItem.isActive(c.props.href, c.props.exact)); + const isExpanded = expanded || isChildActive; + const selfAwareChildren = children && React.cloneElement(children, { child: true }); + + let iconElement; + + if (icon) { + iconElement = typeof icon === 'object' ? icon : {title}; + } + + return ( + + {theme => ( + <> + + + {this.renderIcon(condensed, iconElement, title, classes)} + {!condensed && ( + <> + + {title} + {isAlpha && ( + +   + + + )} + {isBeta && ( + +   + + + )} + + } + disableTypography + /> + {isSection && ( + this.setState(prev => ({ expanded: !prev.expanded }))} + style={{ padding: 4 }} + data-testid="expand-toggle" + > + {isExpanded ? ( + + ) : ( + + )} + + )} + + )} + + + {isExpanded && selfAwareChildren} + + )} + + ); + } +} + +export default withStyles(styles)(NavItem); diff --git a/frontend/packages/core/src/layout/Navigation/NavItem.test.js b/frontend/packages/core/src/layout/Navigation/NavItem.test.js new file mode 100644 index 0000000000..997b765a12 --- /dev/null +++ b/frontend/packages/core/src/layout/Navigation/NavItem.test.js @@ -0,0 +1,95 @@ +import React from 'react'; +import { matchPath } from 'react-router'; +import { fireEvent, render } from '@testing-library/react'; + +import NavItem from 'shared/components/layout/NavItem'; +import { wrapInThemedTestApp } from 'testUtils'; + +jest.mock('react-router', () => ({ + ...jest.requireActual('react-router'), + matchPath: jest.fn(), +})); + +const minProps = { + title: 'A link title', + href: '/mocked', +}; + +const minChildProps = { + title: 'A child title', + href: '/child', +}; + +describe('', () => { + it('renders without exploding', () => { + const rendered = render(wrapInThemedTestApp()); + expect(rendered.getByText('A link title')).toBeInTheDocument(); + }); + + it('should have an anchor with correct href', () => { + const rendered = render(wrapInThemedTestApp()); + const anchor = rendered.container.querySelector('a'); + expect(anchor.href).toBe('http://localhost/mocked'); + }); + + it('renders with icon src', () => { + const rendered = render(wrapInThemedTestApp()); + const icon = rendered.getByAltText('A link title'); + expect(icon.src).toBe('http://localhost/mocked-icon.png'); + }); + + it('renders with icon component', () => { + const Falafel = () =>
falafel
; + const rendered = render(wrapInThemedTestApp(} {...minProps} />)); + const icon = rendered.getByText('falafel'); + expect(icon).toBeInTheDocument(); + }); + + it('shows alpha icon', () => { + const rendered = render(wrapInThemedTestApp()); + expect(rendered.getByText('α')).toBeInTheDocument(); + }); + + it('shows beta icon', () => { + const rendered = render(wrapInThemedTestApp()); + expect(rendered.getByText('β')).toBeInTheDocument(); + }); + + it('renders nested navigation without exploding', () => { + const { getByText, getByTestId } = render( + wrapInThemedTestApp( + + + , + ), + ); + expect(getByText('A link title')).toBeInTheDocument(); + expect(getByTestId('expand-toggle')).toBeInTheDocument(); + }); + + it('toggles nested navigation', () => { + const { getByText, getByTestId, queryByText } = render( + wrapInThemedTestApp( + + + , + ), + ); + expect(queryByText('A child link')).not.toBeInTheDocument(); + fireEvent.click(getByTestId('expand-toggle')); + expect(getByText('A child title')).toBeInTheDocument(); + }); + + it('shows nested navigation automatically if the child is active', () => { + matchPath.mockReturnValue(true); + const { getByText } = render( + wrapInThemedTestApp( + + + , + ), + ); + expect(getByText('A link title')).toBeInTheDocument(); + expect(getByText('A child title')).toBeInTheDocument(); + }); +}); diff --git a/frontend/packages/core/src/layout/Navigation/Navigation.js b/frontend/packages/core/src/layout/Navigation/Navigation.js new file mode 100644 index 0000000000..fc5c15340f --- /dev/null +++ b/frontend/packages/core/src/layout/Navigation/Navigation.js @@ -0,0 +1,91 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { List, withStyles } from '@material-ui/core'; +import ChevronLeft from '@material-ui/icons/ChevronLeft'; +import ChevronRight from '@material-ui/icons/ChevronRight'; + +import { IconButton } from 'shared/components'; +import { getFromStore, saveToStore } from 'shared/apis/storage/persistentStore'; + +const styles = theme => ({ + nav: { + gridArea: 'pageNav', + width: 62, + marginLeft: theme.spacing(3), + marginTop: theme.spacing(3), + transition: 'width 0.07s, height 0s', + transitionTimingFunction: 'ease-in', + }, + item: { + display: 'block', + }, + list: { + padding: 0, + border: '0px solid #333333', + borderRadius: 6, + backgroundColor: '#333333', + }, + buttonContainer: { + marginLeft: 'auto', + marginRight: 'auto', + marginTop: theme.spacing(0.5), + width: 48, + }, + button: { + color: '#888', + }, +}); + +class Navigation extends Component { + static propTypes = { + condensable: PropTypes.bool, + }; + + state = { + condensed: false, + }; + + constructor(props) { + super(props); + + const condensedPref = getFromStore('navigation', 'condensed', false); + this.state.condensed = this.props.condensable && condensedPref; + } + + buttonClicked = () => { + const condensed = !this.state.condensed; + saveToStore('navigation', 'condensed', condensed); + this.setState({ condensed }); + }; + + render() { + const { classes, condensable } = this.props; + const { condensed } = this.state; + + const ExpandButton = condensable && ( +
+ + {condensed ? : } + +
+ ); + + const children = React.Children.map(this.props.children, child => { + if (!child) return null; + return React.cloneElement(child, { + condensed: condensed, + }); + }); + + return ( + + ); + } +} + +export default withStyles(styles)(Navigation); diff --git a/frontend/packages/core/src/layout/Navigation/Navigation.test.js b/frontend/packages/core/src/layout/Navigation/Navigation.test.js new file mode 100644 index 0000000000..d20fa0ddf2 --- /dev/null +++ b/frontend/packages/core/src/layout/Navigation/Navigation.test.js @@ -0,0 +1,57 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; + +import { Navigation, NavItem } from 'shared/components/layout'; +import { wrapInThemedTestApp } from 'testUtils'; + +import { GcpProjectIcon, OverviewIcon } from 'shared/icons'; + +describe('', () => { + it('renders without exploding', () => { + const nav = ( + + } href={'/'} /> + } href={'/projects'} /> + + ); + const rendered = render(wrapInThemedTestApp(nav)); + expect(rendered.getByText('Overview')).toBeInTheDocument(); + expect(rendered.getByText('Projects')).toBeInTheDocument(); + }); + + it('renders a condenseable navigation bar', () => { + const nav = ( + + } href={'/'} /> + } href={'/projects'} /> + + ); + const rendered = render(wrapInThemedTestApp(nav)); + const btn = rendered.getByTestId('nav-expand-toggle'); + expect(btn).toBeInTheDocument(); + }); + + it('is condenseable', () => { + const nav = ( + + } href={'/'} /> + } href={'/projects'} /> + + ); + const rendered = render(wrapInThemedTestApp(nav)); + const btn = rendered.getByTestId('nav-expand-toggle'); + fireEvent.click(btn); + expect(rendered.queryByText('Overview')).not.toBeInTheDocument(); + }); + + it('should have an anchor with correct href', () => { + const nav = ( + + } href={'/projects'} /> + + ); + const rendered = render(wrapInThemedTestApp(nav)); + const anchor = rendered.container.querySelector('a'); + expect(anchor.href).toBe('http://localhost/projects'); + }); +}); diff --git a/frontend/packages/core/src/layout/Page/Page.js b/frontend/packages/core/src/layout/Page/Page.js new file mode 100644 index 0000000000..8425abeb8a --- /dev/null +++ b/frontend/packages/core/src/layout/Page/Page.js @@ -0,0 +1,37 @@ +import React, { Component } from 'react'; +import { withStyles } from '@material-ui/core'; +import { theme } from './PageThemeProvider'; + +const styles = theme => ({ + root: { + display: 'grid', + gridTemplateAreas: + "'pageHeader pageHeader pageHeader' 'pageSubheader pageSubheader pageSubheader' 'pageNav pageContent pageSidebar'", + gridTemplateRows: 'auto auto 1fr', + gridTemplateColumns: 'auto 1fr auto', + minHeight: '100%', + paddingBottom: theme.spacing(3), + }, +}); + +export const Theme = React.createContext({}); + +class Page extends Component { + static defaultProps = { + theme: theme.other, + }; + + render() { + const { theme, backgroundColor, classes, children, styles = {}, ...otherProps } = this.props; + + return ( + +
+ {children} +
+
+ ); + } +} + +export default withStyles(styles)(Page); diff --git a/frontend/packages/core/src/layout/Page/PageThemeProvider.js b/frontend/packages/core/src/layout/Page/PageThemeProvider.js new file mode 100644 index 0000000000..d6832b82d3 --- /dev/null +++ b/frontend/packages/core/src/layout/Page/PageThemeProvider.js @@ -0,0 +1,172 @@ +import '../../assets/bursts/bursts.css'; +import circle1 from '../../assets/bursts/circle-1.svg'; +import circle2 from '../../assets/bursts/circle-2.svg'; +import triangle1 from '../../assets/bursts/triangle-1.svg'; +import triangle2 from '../../assets/bursts/triangle-2.svg'; +import triangle3 from '../../assets/bursts/triangle-3.svg'; +import rectangle1 from '../../assets/bursts/rectangle-1.svg'; +import rectangle2 from '../../assets/bursts/rectangle-2.svg'; + +// TODO move this file to shared and out of core + +export const shapes = { + circle1: { + backgroundImage: `url(${circle1})`, + width: 'calc(400px + 20%)', + backgroundPosition: '100px 60%', + }, + circle2: { + backgroundImage: `url(${circle2})`, + width: 'calc(400px + 20%)', + backgroundPosition: '100px 58%', + }, + rectangle1: { + backgroundImage: `url(${rectangle1})`, + width: 'calc(400px + 20%)', + backgroundPosition: '100px 60%', + }, + rectangle2: { + backgroundImage: `url(${rectangle2})`, + width: 'calc(400px + 20%)', + backgroundPosition: '30px 35%', + }, + triangle1: { + backgroundImage: `url(${triangle1})`, + width: 'calc(500px + 30%)', + backgroundPosition: '100px calc(50px + 45%)', + }, + triangle2: { + backgroundImage: `url(${triangle2})`, + width: 'calc(400px + 20%)', + backgroundPosition: '100px 38%', + }, + triangle3: { + backgroundImage: `url(${triangle3})`, + width: 'calc(400px + 20%)', + backgroundPosition: '100px 76%', + }, +}; + +export const gradients = { + blue: 'linear-gradient(135deg, #2D46B9 0%, #509BF5 100%)', + darkBlue: 'linear-gradient(44deg, #1E3264 0%, #A0C3D2 100%)', + brown: 'linear-gradient(44deg, #674638 0%, #C39887 100%)', + green: 'linear-gradient(-90deg, #1DB954 0%, #006350 100%)', + orangeYellow: 'linear-gradient(37deg, #FF6437 0%, #FFC864 100%)', + redOrange: 'linear-gradient(37deg, #A72525 0%, #E6542D 100%)', + pinkOrange: 'linear-gradient(43deg, #F13DA2 0%, #FF8A48 100%)', + purpleBlue: 'linear-gradient(-137deg, #4100F4 0%, #AF2996 100%)', + tealGreen: 'linear-gradient(-137deg, #19E68C 0%, #1D7F6E 100%)', + violetPeach: 'linear-gradient(44deg, #B39AC8 0%, #FCCBD3 100%)', + violetGreen: 'linear-gradient(44deg, #4302F4 0%, #C3EFC8 100%)', + purple: 'linear-gradient(-90deg, #a186bd 0%, #7c5c92 100%)', + tpm: 'linear-gradient(-137deg, #00FFF2 0%, #035355 100%)', + royalBlue: 'linear-gradient(45deg, #000044 0%, #0000DD 61.47%, #0033DD 74%, #4B80D4 100%)', + grey: 'linear-gradient(45deg, #111111 0%, #777777 100%)', + sunset: 'linear-gradient(148deg, #cf8022 0%, #4e6ec7 100%)', + sky: 'linear-gradient(135deg, #69B9FF 0%, #ACCEEC 100%)', +}; + +export const theme = { + service: { + activeNavLinkColor: '#1D7F6E', + gradient: gradients.tealGreen, + burstShape: shapes.rectangle2, + }, + appFeature: { + activeNavLinkColor: '#FF6437', + gradient: gradients.orangeYellow, + burstShape: shapes.triangle1, + }, + app: { + activeNavLinkColor: '#A72525', + gradient: gradients.redOrange, + burstShape: shapes.triangle1, + }, + endpoint: { + activeNavLinkColor: '#9315b0', + gradient: gradients.purpleBlue, + burstShape: shapes.circle1, + }, + pipeline: { + activeNavLinkColor: '#9315b0', + gradient: gradients.purpleBlue, + burstShape: shapes.circle1, + }, + website: { + activeNavLinkColor: '#765d90', + gradient: gradients.purple, + burstShape: shapes.rectangle2, + }, + workflow: { + activeNavLinkColor: '#AF2996', + secondary: '#9315b0', + gradient: gradients.purpleBlue, + burstShape: shapes.circle1, + }, + library: { + activeNavLinkColor: '#B39AC8', + gradient: gradients.sunset, + burstShape: shapes.triangle3, + }, + other: { + activeNavLinkColor: '#8f6858', + gradient: gradients.brown, + burstShape: shapes.rectangle1, + }, + system: { + activeNavLinkColor: '#526C90', + gradient: gradients.darkBlue, + burstShape: shapes.triangle2, + }, + project: { + activeNavLinkColor: '#F13DA2', + gradient: gradients.pinkOrange, + burstShape: shapes.triangle3, + }, + tool: { + activeNavLinkColor: '#B39AC8', + gradient: gradients.violetPeach, + burstShape: shapes.rectangle1, + }, + home: { + activeNavLinkColor: '#00814e', + gradient: gradients.green, + burstShape: shapes.triangle2, + }, + org: { + activeNavLinkColor: '#6044ef', + gradient: gradients.violetGreen, + burstShape: shapes.triangle3, + }, + documentation: { + activeNavLinkColor: '#04c2ba', + gradient: gradients.tpm, + burstShape: shapes.circle2, + }, + blogPost: { + activeNavLinkColor: '#FFF', + gradient: 'linear-gradient(90deg, #033A45 0%, #033A45 100%)', + burstShape: shapes.circle2, + }, + machineLearning: { + activeNavLinkColor: '#0033DD', + gradient: gradients.royalBlue, + burstShape: shapes.circle1, + }, + partnership: { + activeNavLinkColor: '#000000', + gradient: gradients.grey, + burstShape: shapes.rectangle2, + }, + ml: { + activeNavLinkColor: '#B39AC8', + gradient: gradients.blue, + burstShape: shapes.circle1, + }, + scienceBox: { + activeNavLinkColor: '#69B9FF', // matches sky gradient beginning point + gradient: gradients.sky, + burstShape: shapes.circle1, + }, +}; \ No newline at end of file diff --git a/frontend/packages/core/src/layout/Page/index.ts b/frontend/packages/core/src/layout/Page/index.ts new file mode 100644 index 0000000000..889778f7e8 --- /dev/null +++ b/frontend/packages/core/src/layout/Page/index.ts @@ -0,0 +1,2 @@ +export { default } from './Page'; +export { shapes, gradients, theme } from './PageThemeProvider'; \ No newline at end of file diff --git a/frontend/packages/core/src/layout/SupportButton/SupportButton.js b/frontend/packages/core/src/layout/SupportButton/SupportButton.js new file mode 100644 index 0000000000..b8663fcb77 --- /dev/null +++ b/frontend/packages/core/src/layout/SupportButton/SupportButton.js @@ -0,0 +1,114 @@ +import React, { Component, Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { compose } from 'recompose'; +import { List, ListItem, ListItemIcon, Popover, Typography, withStyles } from '@material-ui/core'; +import { withPluginContext } from 'core/app/withPluginContext'; +import { EmailIcon, SlackIcon, SupportIcon } from 'shared/icons'; +import { Button, Link } from 'shared/components'; +import { StackOverflow, StackOverflowTag } from 'shared/components/layout'; + +class SupportButton extends Component { + static defaultProps = { + slackChannel: '#backstage', + }; + + static propTypes = { + slackChannel: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]), + }; + + state = { + popoverOpen: false, + anchorEl: null, + }; + + button_clickHandler = event => { + this.setState({ anchorEl: event.currentTarget, popoverOpen: true }); + }; + + popover_closeHandler = () => { + this.setState({ popoverOpen: false }); + }; + + render() { + const { slackChannel, email, children, classes, plugin } = this.props; + const tags = plugin ? plugin.stackoverflowTags : undefined; // plugin is undefined on base routes + const { popoverOpen, anchorEl } = this.state; + const slackChannels = slackChannel ? (Array.isArray(slackChannel) ? slackChannel : [slackChannel]) : null; + const contactEmails = email ? (Array.isArray(email) ? email : [email]) : null; + + return ( + + + + + {React.Children.map(children, (child, index) => ( + + {child} + + ))} + {tags && tags.length > 0 && ( + + + {tags.map((tag, idx) => ( + + ))} + + + )} + {slackChannels && ( + + + + +
+ Support + {slackChannels.map((channel, index) => ( + + {channel} + + ))} +
+
+ )} + {contactEmails && ( + + + + +
+ Contact + {contactEmails.map((email, index) => ( + + {email} + + ))} +
+
+ )} +
+
+
+ ); + } +} + +const styles = theme => ({ + leftIcon: { + marginRight: theme.spacing(1), + }, + popoverList: { + minWidth: 260, + maxWidth: 320, + }, +}); + +export default compose(withStyles(styles), withPluginContext)(SupportButton); diff --git a/frontend/packages/core/src/layout/SupportButton/SupportButton.test.js b/frontend/packages/core/src/layout/SupportButton/SupportButton.test.js new file mode 100644 index 0000000000..b2289542f1 --- /dev/null +++ b/frontend/packages/core/src/layout/SupportButton/SupportButton.test.js @@ -0,0 +1,36 @@ +import React from 'react'; +import { render, fireEvent, waitForElement } from '@testing-library/react'; + +import SupportButton from './SupportButton'; +import { wrapInThemedTestApp, Keyboard } from 'testUtils'; + +describe('', () => { + it('renders without exploding', () => { + const rendered = render(); + rendered.getByText('Support'); + + fireEvent.click(rendered.baseElement); + }); + + it('should show popover', () => { + const rendered = render( + wrapInThemedTestApp(), + ); + + expect(rendered.queryByText('Contact')).not.toBeInTheDocument(); + fireEvent.click(rendered.getByText('Support')); + expect(rendered.queryByText('Contact')).toBeInTheDocument(); + rendered.getByText('a@b.com'); + rendered.getByText('a-tag'); + }); + + it('should close popover when hitting escape', async () => { + const rendered = render(wrapInThemedTestApp()); + + fireEvent.click(rendered.getByText('Support')); + expect(rendered.queryByText('Contact')).toBeInTheDocument(); + await Keyboard.type(rendered, ''); + await waitForElement(() => !rendered.queryByText('Contact')); + expect(rendered.queryByText('Contact')).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/packages/core/src/theme/BackstageTheme.js b/frontend/packages/core/src/theme/BackstageTheme.js new file mode 100644 index 0000000000..d69a86ac37 --- /dev/null +++ b/frontend/packages/core/src/theme/BackstageTheme.js @@ -0,0 +1,358 @@ +import { createMuiTheme, withStyles } from '@material-ui/core'; +import { darken, lighten } from '@material-ui/core/styles/colorManipulator'; +import { blue, yellow } from '@material-ui/core/colors'; +import tinycolor from 'tinycolor2'; + +export const COLORS = { + PAGE_BACKGROUND: '#F8F8F8', + DEFAULT_PAGE_THEME_COLOR: '#7C3699', + DEFAULT_PAGE_THEME_LIGHT_COLOR: tinycolor('#7C3699') + .lighten(50) + .toString(), + ERROR_BACKGROUND_COLOR: '#FFEBEE', + ERROR_TEXT_COLOR: '#CA001B', + INFO_TEXT_COLOR: '#004e8a', + LINK_TEXT: '#0A6EBE', + LINK_TEXT_HOVER: '#2196F3', + COMPONENT_TYPES: { + SERVICE: '#39732E', + ENDPOINT: '#B71C1C', + PROJECT: '#E657AA', + WORKFLOW: '#c15013', + }, + NAMED: { + WHITE: '#FEFEFE', + }, + STATUS: { + OK: '#1db855', + WARNING: '#f49b20', + ERROR: '#CA001B', + }, +}; + +const extendedThemeConfig = { + props: { + MuiGrid: { + spacing: 2, + }, + MuiSwitch: { + color: 'primary', + }, + }, + palette: { + background: { + default: COLORS.PAGE_BACKGROUND, + informational: '#60a3cb', + }, + status: { + ok: COLORS.STATUS.OK, + warning: COLORS.STATUS.WARNING, + error: COLORS.STATUS.ERROR, + running: '#BEBEBE', + pending: '#5BC0DE', + background: COLORS.NAMED.WHITE, + }, + bursts: { + fontColor: COLORS.NAMED.WHITE, + slackChannelText: '#ddd', + backgroundColor: { + service: COLORS.COMPONENT_TYPES.SERVICE, + endpoint: COLORS.COMPONENT_TYPES.ENDPOINT, + project: COLORS.COMPONENT_TYPES.PROJECT, + default: COLORS.DEFAULT_PAGE_THEME_COLOR, + }, + }, + primary: { + main: blue[500], + }, + border: '#E6E6E6', + textVerySubtle: '#DDD', + textSubtle: '#6E6E6E', + highlight: '#FFFBCC', + errorBackground: COLORS.ERROR_BACKGROUND_COLOR, + warningBackground: '#F59B23', + infoBackground: '#ebf5ff', + errorText: COLORS.ERROR_TEXT_COLOR, + infoText: COLORS.INFO_TEXT_COLOR, + warningText: COLORS.NAMED.WHITE, + linkHover: COLORS.LINK_TEXT_HOVER, + link: COLORS.LINK_TEXT, + gold: yellow.A700, + }, + navigation: { + width: 220, + background: '#333333', + }, + typography: { + h4: { + // Page name/heading | Dialog titles + fontSize: 48, + color: '#2D2D2D', + fontWeight: 'bold', + }, + h3: { + // Page titles + fontSize: 32, + color: '#2D2D2D', + fontWeight: 'bold', + }, + h2: { + // Card titles | Sub headings in dialogs + fontSize: 24, + color: '#2D2D2D', + fontWeight: 'bold', + lineHeight: 1.2, + marginBottom: 6, + }, + header3: { + // Table headers (ALL CAPS!) + fontSize: 12, + color: '#9E9E9E', + }, + menuItem1: { + // Sidebar menu item + fontSize: 16, + color: '#828282', + fontWeight: 'bold', + }, + menuItem2: { + // Dropdown menu items | Form labels | Deep links from cards (Go to...) + fontSize: 16, + color: '#2D2D2D', + }, + text: { + // Table entries | Information/error text/messages | General copy/paragraphs + fontSize: 13, + color: '#2D2D2D', + }, + links: { + // Table entries | Information/error text/messages | General copy/paragraphs + color: '#509BF5', + }, + tabSelected: { + // Selected tab + fontSize: 18, + color: '#2D2D2D', + }, + tabUnselected: { + // Unselected tab + fontSize: 18, + color: '#9E9E9E', + }, + code: { + fontFamily: 'monospace', + whiteSpace: 'pre', + fontSize: '16px', + }, + caption: { + // Restores caption to display: block to match MUI3 behavior; this global override should + // be removed if possible. + display: 'block', + }, + }, +}; + +const createOverrides = theme => { + const overrides = { + MuiAppBar: { + root: { + zIndex: 1049, + height: 44, + }, + colorPrimary: { + backgroundColor: '#212121', + }, + }, + MuiToolbar: { + root: { + '@media (min-width:600px)': { + minHeight: 44, + }, + }, + }, + MuiTableRow: { + root: { + height: 'auto', + '&:nth-of-type(odd)': { + backgroundColor: theme.palette.background.default, + }, + }, + hover: { + '&:hover': { + cursor: 'pointer', + }, + }, + head: { + height: 'auto', + '&:nth-of-type(odd)': { + backgroundColor: COLORS.NAMED.WHITE, + }, + }, + }, + MuiTableCell: { + root: { + wordBreak: 'break-word', + overflow: 'hidden', + verticalAlign: 'middle', + lineHeight: '1', + margin: 0, + padding: '8px', + borderBottom: 0, + }, + head: { + wordBreak: 'break-word', + overflow: 'hidden', + color: 'rgb(179, 179, 179)', + fontWeight: 'normal', + lineHeight: '1', + }, + }, + MuiTabs: { + root: { + minHeight: 24, + }, + }, + MuiTab: { + root: { + color: theme.palette.link, + minHeight: 24, + textTransform: 'initial', + '&:hover': { + color: darken(theme.palette.link, 0.3), + background: lighten(theme.palette.link, 0.95), + }, + [theme.breakpoints.up('md')]: { + minWidth: 120, + fontSize: theme.typography.pxToRem(14), + fontWeight: 500, + }, + }, + textColorPrimary: { + color: theme.palette.link, + }, + }, + MuiTableSortLabel: { + root: { + color: 'inherit', + '&:hover': { + color: 'inherit', + }, + '&:focus': { + color: 'inherit', + }, + }, + active: { + fontWeight: 'bold', + color: 'inherit', + }, + }, + MuiListItemText: { + dense: { + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + }, + }, + MuiButton: { + text: { + padding: '6px 16px', + }, + }, + MuiChip: { + root: { + marginRight: theme.spacing(1), + marginBottom: theme.spacing(1), + }, + }, + MuiDivider: { + light: { + backgroundColor: '#727272', + }, + }, + MuiDialogTitle: { + root: { + minWidth: 600, + }, + }, + MuiCardHeader: { + root: { + // Remove bottom padding on titles; there is always a CardContent below that also has padding, + // so without this fix there will be too much space below the title. + paddingBottom: '0', + // Mui 1.2.1 introduced more padding left and right with media queries. + // Question is if we should override them or just go with the defaults + '@media (min-width:600px)': { + paddingLeft: theme.spacing(2), + paddingRight: theme.spacing(2), + }, + }, + }, + MuiCardContent: { + root: { + '&:last-child': { + // There is some odd extra whitespace at the bottom of card content, which makes it look + // bottom-heavy and uneven; set it to the same as the other sides + paddingBottom: theme.spacing(2), + }, + // Mui 1.2.1 introduced more padding left and right with media queries. + // Question is if we should override them or just go with the defaults + '@media (min-width:600px)': { + paddingLeft: theme.spacing(2), + paddingRight: theme.spacing(2), + }, + }, + }, + MuiCardActions: { + root: { + justifyContent: 'flex-end', + }, + }, + MuiListSubheader: { + sticky: { + // Sticky subheaders need to be opaque so that they overwrite list items as you scroll down. + backgroundColor: theme.palette.background.paper, + }, + }, + Toolbar: { + toolbar: { + // Override the toolbar in dx-react-grid table + marginTop: '-64px', + }, + }, + }; + return { + overrides, + }; +}; + +const extendedTheme = createMuiTheme(extendedThemeConfig); + +// V1 theming +// https://material-ui-next.com/customization/themes/ +// For CSS it is advised to use JSS, see https://material-ui-next.com/customization/css-in-js/ + +const BackstageTheme = { ...extendedTheme, ...createOverrides(extendedTheme) }; + +// Temporary workaround for files incorrectly importing the theme directly +export const V1 = BackstageTheme; + +// Default styles for the application other than the built in mui components +const styles = theme => ({ + '@global': { + html: { + height: '100%', + fontFamily: theme.typography.fontFamily, + }, + body: { + height: '100%', + fontFamily: theme.typography.fontFamily, + }, + a: { + color: 'inherit', + textDecoration: 'none', + }, + }, +}); + +export const withGlobalStyles = withStyles(styles); +export default BackstageTheme; \ No newline at end of file diff --git a/frontend/packages/plugins/hello-world/src/components/MyComponent/MyComponent.tsx b/frontend/packages/plugins/hello-world/src/components/MyComponent/MyComponent.tsx index ec28306367..a9ad846047 100644 --- a/frontend/packages/plugins/hello-world/src/components/MyComponent/MyComponent.tsx +++ b/frontend/packages/plugins/hello-world/src/components/MyComponent/MyComponent.tsx @@ -1,8 +1,8 @@ -import React, { FC, useState, useEffect } from 'react'; +import React, { FC, useState, useEffect, Fragment } from 'react'; import Button from '@material-ui/core/Button'; import { HelloPromiseClient } from '../../proto/hello_grpc_web_pb'; import { HelloRequest } from '../../proto/hello_pb'; -import { Paper, Typography } from '@material-ui/core'; +import { Typography } from '@material-ui/core'; const MyComponent: FC<{}> = () => { const [message, setMessage] = useState(''); @@ -25,7 +25,7 @@ const MyComponent: FC<{}> = () => { }, []); return ( - + {message} {error} @@ -37,7 +37,7 @@ const MyComponent: FC<{}> = () => { > Hello! - + ); }; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index fc348a1cce..64df4b69f2 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -876,7 +876,7 @@ core-js-pure "^3.0.0" regenerator-runtime "^0.13.2" -"@babel/runtime@7.8.4", "@babel/runtime@^7.0.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6": +"@babel/runtime@7.8.4", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6": version "7.8.4" resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.8.4.tgz#d79f5a2040f7caa24d53e563aad49cbc05581308" integrity sha512-neAp3zt80trRVBI1x0azq6c57aNBqYZH8KhMm3TaB7wEI5Q4A2SHfBHE8w9gOhI/lrqxtEbXZgQIrHP+wvSGwQ== @@ -2850,6 +2850,11 @@ "@types/minimatch" "*" "@types/node" "*" +"@types/history@*": + version "4.7.5" + resolved "https://registry.npmjs.org/@types/history/-/history-4.7.5.tgz#527d20ef68571a4af02ed74350164e7a67544860" + integrity sha512-wLD/Aq2VggCJXSjxEwrMafIP51Z+13H78nXIX0ABEuIGhmB5sNGbR113MOKo+yfw+RDo1ZU3DM6yfnnRF/+ouw== + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.1" resolved "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff" @@ -2932,6 +2937,23 @@ dependencies: "@types/react" "*" +"@types/react-router-dom@^5.1.3": + version "5.1.3" + resolved "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.3.tgz#b5d28e7850bd274d944c0fbbe5d57e6b30d71196" + integrity sha512-pCq7AkOvjE65jkGS5fQwQhvUp4+4PVD9g39gXLZViP2UqFiFzsEpB3PKf0O6mdbKsewSK8N14/eegisa/0CwnA== + dependencies: + "@types/history" "*" + "@types/react" "*" + "@types/react-router" "*" + +"@types/react-router@*": + version "5.1.4" + resolved "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.4.tgz#7d70bd905543cb6bcbdcc6bd98902332054f31a6" + integrity sha512-PZtnBuyfL07sqCJvGg3z+0+kt6fobc/xmle08jBiezLS8FrmGeiGkJnuxL/8Zgy9L83ypUhniV5atZn/L8n9MQ== + dependencies: + "@types/history" "*" + "@types/react" "*" + "@types/react-transition-group@^4.2.0": version "4.2.3" resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.2.3.tgz#4924133f7268694058e415bf7aea2d4c21131470" @@ -7504,6 +7526,11 @@ grpc-web@^1.0.7: resolved "https://registry.npmjs.org/grpc-web/-/grpc-web-1.0.7.tgz#9e4fbcf63d3734515332ab59e42baa7d0d290015" integrity sha512-Fkbz1nyvvt6GC6ODcxh9Fen6LLB3OTCgGHzHwM2Eni44SUhzqPz1UQgFp9sfBEfInOhx3yBdwo9ZLjZAmJ+TtA== +gud@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0" + integrity sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw== + gzip-size@5.1.1: version "5.1.1" resolved "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz#cb9bee692f87c0612b232840a873904e4c135274" @@ -7637,6 +7664,18 @@ hex-color-regex@^1.1.0: resolved "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== +history@^4.9.0: + version "4.10.1" + resolved "https://registry.npmjs.org/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3" + integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew== + dependencies: + "@babel/runtime" "^7.1.2" + loose-envify "^1.2.0" + resolve-pathname "^3.0.0" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" + value-equal "^1.0.1" + hmac-drbg@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -7646,7 +7685,7 @@ hmac-drbg@^1.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -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.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== @@ -10352,7 +10391,7 @@ longest@^2.0.1: resolved "https://registry.npmjs.org/longest/-/longest-2.0.1.tgz#781e183296aa94f6d4d916dc335d0d17aefa23f8" integrity sha1-eB4YMpaqlPbU2RbcM10NF676I/g= -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -10712,6 +10751,15 @@ min-indent@^1.0.0: resolved "https://registry.npmjs.org/min-indent/-/min-indent-1.0.0.tgz#cfc45c37e9ec0d8f0a0ec3dd4ef7f7c3abe39256" integrity sha1-z8RcN+nsDY8KDsPdTvf3w6vjklY= +mini-create-react-context@^0.3.0: + version "0.3.2" + resolved "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.3.2.tgz#79fc598f283dd623da8e088b05db8cddab250189" + integrity sha512-2v+OeetEyliMt5VHMXsBhABoJ0/M4RCe7fatd/fBy6SMiKazUSEt3gxxypfnk2SHMkdBYvorHRoQxuGoiwbzAw== + dependencies: + "@babel/runtime" "^7.4.0" + gud "^1.0.0" + tiny-warning "^1.0.2" + mini-css-extract-plugin@0.8.0: version "0.8.0" resolved "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.8.0.tgz#81d41ec4fe58c713a96ad7c723cdb2d0bd4d70e1" @@ -12053,6 +12101,13 @@ path-to-regexp@0.1.7: resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= +path-to-regexp@^1.7.0: + version "1.8.0" + resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" + integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== + dependencies: + isarray "0.0.1" + path-type@^1.0.0: version "1.1.0" resolved "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" @@ -12982,7 +13037,7 @@ promzard@^0.3.0: dependencies: read "1" -prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@^15.5.4, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -13192,6 +13247,11 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.2.8: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-addons-text-content@0.0.4: + version "0.0.4" + resolved "https://registry.npmjs.org/react-addons-text-content/-/react-addons-text-content-0.0.4.tgz#d2e259fdc951d1d8906c08902002108dce8792e5" + integrity sha1-0uJZ/clR0diQbAiQIAIQjc6HkuU= + react-app-polyfill@^1.0.5: version "1.0.6" resolved "https://registry.npmjs.org/react-app-polyfill/-/react-app-polyfill-1.0.6.tgz#890f8d7f2842ce6073f030b117de9130a5f385f0" @@ -13249,11 +13309,62 @@ 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-is@^16.12.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is@^16.8.4: +react-fast-compare@^2.0.2: + 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== + +react-helmet@5.2.1: + version "5.2.1" + resolved "https://registry.npmjs.org/react-helmet/-/react-helmet-5.2.1.tgz#16a7192fdd09951f8e0fe22ffccbf9bb3e591ffa" + integrity sha512-CnwD822LU8NDBnjCpZ4ySh8L6HYyngViTZLfBBb3NjtrpN8m49clH8hidHouq20I51Y6TpCTISCBbqiY5GamwA== + dependencies: + object-assign "^4.1.1" + prop-types "^15.5.4" + react-fast-compare "^2.0.2" + react-side-effect "^1.1.0" + +react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is@^16.8.4: version "16.12.0" resolved "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c" integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q== +react-router-dom@^5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.1.2.tgz#06701b834352f44d37fbb6311f870f84c76b9c18" + integrity sha512-7BPHAaIwWpZS074UKaw1FjVdZBSVWEk8IuDXdB+OkLb8vd/WRQIpA4ag9WQk61aEfQs47wHyjWUoUGGZxpQXew== + dependencies: + "@babel/runtime" "^7.1.2" + history "^4.9.0" + loose-envify "^1.3.1" + prop-types "^15.6.2" + react-router "5.1.2" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" + +react-router@5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/react-router/-/react-router-5.1.2.tgz#6ea51d789cb36a6be1ba5f7c0d48dd9e817d3418" + integrity sha512-yjEuMFy1ONK246B+rsa0cUam5OeAQ8pyclRDgpxuSCrAlJ1qN9uZ5IgyKC7gQg0w8OM50NXHEegPh/ks9YuR2A== + dependencies: + "@babel/runtime" "^7.1.2" + history "^4.9.0" + hoist-non-react-statics "^3.1.0" + loose-envify "^1.3.1" + mini-create-react-context "^0.3.0" + path-to-regexp "^1.7.0" + prop-types "^15.6.2" + react-is "^16.6.0" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" + +react-side-effect@^1.1.0: + version "1.2.0" + resolved "https://registry.npmjs.org/react-side-effect/-/react-side-effect-1.2.0.tgz#0e940c78faba0c73b9b0eba9cd3dda8dfb7e7dae" + integrity sha512-v1ht1aHg5k/thv56DRcjw+WtojuuDHFUgGfc+bFHOWsF4ZK6C2V57DO0Or0GPsg6+LSTE0M6Ry/gfzhzSwbc5w== + dependencies: + shallowequal "^1.0.1" + react-transition-group@^4.3.0: version "4.3.0" resolved "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.3.0.tgz#fea832e386cf8796c58b61874a3319704f5ce683" @@ -13765,6 +13876,11 @@ resolve-global@1.0.0, resolve-global@^1.0.0: dependencies: global-dirs "^0.1.1" +resolve-pathname@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd" + integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng== + resolve-url-loader@3.1.1: version "3.1.1" resolved "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-3.1.1.tgz#28931895fa1eab9be0647d3b2958c100ae3c0bf0" @@ -14218,6 +14334,11 @@ shallow-clone@^3.0.0: dependencies: kind-of "^6.0.2" +shallowequal@^1.0.1: + version "1.1.0" + resolved "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" + integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== + shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -15184,16 +15305,26 @@ timsort@^0.3.0: resolved "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= +tiny-invariant@^1.0.2: + version "1.1.0" + resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875" + integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw== + tiny-relative-date@^1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/tiny-relative-date/-/tiny-relative-date-1.3.0.tgz#fa08aad501ed730f31cc043181d995c39a935e07" integrity sha512-MOQHpzllWxDCHHaDno30hhLfbouoYlOI8YlMNtvKe1zXbjEVhbcEovQxvZrPvtiYW630GQDoMMarCnjfyfHA+A== -tiny-warning@^1.0.2: +tiny-warning@^1.0.0, tiny-warning@^1.0.2: version "1.0.3" resolved "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== +tinycolor2@1.4.1: + version "1.4.1" + resolved "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz#f4fad333447bc0b07d4dc8e9209d8f39a8ac77e8" + integrity sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g= + tmp@^0.0.33: version "0.0.33" resolved "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -15715,6 +15846,11 @@ validate-npm-package-name@^3.0.0, validate-npm-package-name@~3.0.0: dependencies: builtins "^1.0.3" +value-equal@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c" + integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw== + vary@~1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" From 6104d88d5a52c7b2d9a5c5c7d92045158d1f9c2e Mon Sep 17 00:00:00 2001 From: Raghunandan Balachandran Date: Wed, 5 Feb 2020 01:02:00 +0100 Subject: [PATCH 02/12] more deps --- frontend/packages/app/src/App.test.tsx | 1 - .../core/src/layout/Header/Header.test.js | 4 +- .../HeaderActionMenu/HeaderActionMenu.test.js | 2 +- .../layout/HeaderLabel/HeaderLabel.test.js | 4 +- .../HeaderLabel/OwnerHeaderLabel.test.js | 4 +- .../src/layout/InfoCard/BottomLink.test.js | 3 +- .../core/src/layout/InfoCard/InfoCard.test.js | 2 +- .../layout/Navigation/GcpProjectByTools.tsx | 14 + .../src/layout/Navigation/InfoSquared.tsx | 11 + .../src/layout/Navigation/NavItem.test.js | 5 +- .../src/layout/Navigation/Navigation.test.js | 32 +- .../SupportButton/SupportButton.test.js | 2 +- .../packages/core/src/testUtils/Keyboard.js | 188 ++++++++++++ .../core/src/testUtils/Keyboard.test.js | 79 +++++ frontend/packages/core/src/testUtils/index.js | 281 ++++++++++++++++++ .../core/src/testUtils/mockBreakpoint.ts | 73 +++++ 16 files changed, 683 insertions(+), 22 deletions(-) create mode 100644 frontend/packages/core/src/layout/Navigation/GcpProjectByTools.tsx create mode 100644 frontend/packages/core/src/layout/Navigation/InfoSquared.tsx create mode 100644 frontend/packages/core/src/testUtils/Keyboard.js create mode 100644 frontend/packages/core/src/testUtils/Keyboard.test.js create mode 100644 frontend/packages/core/src/testUtils/index.js create mode 100644 frontend/packages/core/src/testUtils/mockBreakpoint.ts diff --git a/frontend/packages/app/src/App.test.tsx b/frontend/packages/app/src/App.test.tsx index a4b304af28..b67d9918cb 100644 --- a/frontend/packages/app/src/App.test.tsx +++ b/frontend/packages/app/src/App.test.tsx @@ -6,7 +6,6 @@ describe('App', () => { it('renders learn react link', () => { const rendered = render(); rendered.getByText('This is Backstage!'); - rendered.getByText('Backstage is an open platform for building developer portals'); rendered.getByText('…with plugin hello-world:'); }); }); diff --git a/frontend/packages/core/src/layout/Header/Header.test.js b/frontend/packages/core/src/layout/Header/Header.test.js index 15d5f3f4b4..0609cbbc17 100644 --- a/frontend/packages/core/src/layout/Header/Header.test.js +++ b/frontend/packages/core/src/layout/Header/Header.test.js @@ -1,7 +1,7 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { Header } from 'shared/components/layout'; -import { wrapInThemedTestApp } from 'testUtils'; +import Header from './Header'; +import { wrapInThemedTestApp } from '../../testUtils'; jest.mock('react-helmet', () => { return ({ defaultTitle }) =>
defaultTitle: {defaultTitle}
; diff --git a/frontend/packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.test.js b/frontend/packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.test.js index dcfecf5e3f..5212c0c2df 100644 --- a/frontend/packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.test.js +++ b/frontend/packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.test.js @@ -1,6 +1,6 @@ import React from 'react'; import { render, fireEvent } from '@testing-library/react'; -import { wrapInThemedTestApp, Keyboard } from 'testUtils'; +import { wrapInThemedTestApp, Keyboard } from '../../testUtils'; import HeaderActionMenu from './HeaderActionMenu'; describe('', () => { diff --git a/frontend/packages/core/src/layout/HeaderLabel/HeaderLabel.test.js b/frontend/packages/core/src/layout/HeaderLabel/HeaderLabel.test.js index 06eccae61c..5c35cc1d4e 100644 --- a/frontend/packages/core/src/layout/HeaderLabel/HeaderLabel.test.js +++ b/frontend/packages/core/src/layout/HeaderLabel/HeaderLabel.test.js @@ -1,7 +1,7 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { HeaderLabel } from 'shared/components/layout'; -import { wrapInThemedTestApp } from 'testUtils'; +import HeaderLabel from './HeaderLabel'; +import { wrapInThemedTestApp } from '../../testUtils'; describe('', () => { it('should have a label', () => { diff --git a/frontend/packages/core/src/layout/HeaderLabel/OwnerHeaderLabel.test.js b/frontend/packages/core/src/layout/HeaderLabel/OwnerHeaderLabel.test.js index 11ae8f3a89..b4e64f64b0 100644 --- a/frontend/packages/core/src/layout/HeaderLabel/OwnerHeaderLabel.test.js +++ b/frontend/packages/core/src/layout/HeaderLabel/OwnerHeaderLabel.test.js @@ -1,7 +1,7 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { OwnerHeaderLabel } from 'shared/components/layout'; -import { wrapInThemedTestApp } from 'testUtils'; +import OwnerHeaderLabel from './OwnerHeaderLabel'; +import { wrapInThemedTestApp } from '../../testUtils'; const properOwner = { id: 'tools', name: 'tools', type: 'squad' }; const badOwner = { id: 'tools-xxx', name: 'tools-xxx' }; diff --git a/frontend/packages/core/src/layout/InfoCard/BottomLink.test.js b/frontend/packages/core/src/layout/InfoCard/BottomLink.test.js index 3941c53b53..740f9fd835 100644 --- a/frontend/packages/core/src/layout/InfoCard/BottomLink.test.js +++ b/frontend/packages/core/src/layout/InfoCard/BottomLink.test.js @@ -1,7 +1,6 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { wrapInTestApp } from 'testUtils'; - +import { wrapInTestApp } from '../../testUtils'; import BottomLink from './BottomLink'; const minProps = { diff --git a/frontend/packages/core/src/layout/InfoCard/InfoCard.test.js b/frontend/packages/core/src/layout/InfoCard/InfoCard.test.js index 88c0e08fa9..ef02efa8a2 100644 --- a/frontend/packages/core/src/layout/InfoCard/InfoCard.test.js +++ b/frontend/packages/core/src/layout/InfoCard/InfoCard.test.js @@ -1,6 +1,6 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { wrapInTestApp } from 'testUtils'; +import { wrapInTestApp } from '../../testUtils'; import InfoCard from './InfoCard'; const minProps = { diff --git a/frontend/packages/core/src/layout/Navigation/GcpProjectByTools.tsx b/frontend/packages/core/src/layout/Navigation/GcpProjectByTools.tsx new file mode 100644 index 0000000000..104fcee7d3 --- /dev/null +++ b/frontend/packages/core/src/layout/Navigation/GcpProjectByTools.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon'; + +const SvgGcpProjectByTools = (props: SvgIconProps) => + React.createElement( + SvgIcon, + props, + , + ); + +export default SvgGcpProjectByTools; diff --git a/frontend/packages/core/src/layout/Navigation/InfoSquared.tsx b/frontend/packages/core/src/layout/Navigation/InfoSquared.tsx new file mode 100644 index 0000000000..0f51727069 --- /dev/null +++ b/frontend/packages/core/src/layout/Navigation/InfoSquared.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon'; + +const SvgInfoSquared = (props: SvgIconProps) => + React.createElement( + SvgIcon, + props, + , + ); + +export default SvgInfoSquared; diff --git a/frontend/packages/core/src/layout/Navigation/NavItem.test.js b/frontend/packages/core/src/layout/Navigation/NavItem.test.js index 997b765a12..3cc95b635b 100644 --- a/frontend/packages/core/src/layout/Navigation/NavItem.test.js +++ b/frontend/packages/core/src/layout/Navigation/NavItem.test.js @@ -1,9 +1,8 @@ import React from 'react'; import { matchPath } from 'react-router'; import { fireEvent, render } from '@testing-library/react'; - -import NavItem from 'shared/components/layout/NavItem'; -import { wrapInThemedTestApp } from 'testUtils'; +import NavItem from './NavItem'; +import { wrapInThemedTestApp } from '../../testUtils'; jest.mock('react-router', () => ({ ...jest.requireActual('react-router'), diff --git a/frontend/packages/core/src/layout/Navigation/Navigation.test.js b/frontend/packages/core/src/layout/Navigation/Navigation.test.js index d20fa0ddf2..ed18457637 100644 --- a/frontend/packages/core/src/layout/Navigation/Navigation.test.js +++ b/frontend/packages/core/src/layout/Navigation/Navigation.test.js @@ -1,17 +1,23 @@ import React from 'react'; import { render, fireEvent } from '@testing-library/react'; -import { Navigation, NavItem } from 'shared/components/layout'; -import { wrapInThemedTestApp } from 'testUtils'; +import Navigation from './Navigation'; +import NavItem from './NavItem'; +import { wrapInThemedTestApp } from '../../testUtils'; -import { GcpProjectIcon, OverviewIcon } from 'shared/icons'; +import { default as GcpProjectIcon } from './GcpProjectByTools'; +import { default as OverviewIcon } from './InfoSquared'; describe('', () => { it('renders without exploding', () => { const nav = ( } href={'/'} /> - } href={'/projects'} /> + } + href={'/projects'} + /> ); const rendered = render(wrapInThemedTestApp(nav)); @@ -23,7 +29,11 @@ describe('', () => { const nav = ( } href={'/'} /> - } href={'/projects'} /> + } + href={'/projects'} + /> ); const rendered = render(wrapInThemedTestApp(nav)); @@ -35,7 +45,11 @@ describe('', () => { const nav = ( } href={'/'} /> - } href={'/projects'} /> + } + href={'/projects'} + /> ); const rendered = render(wrapInThemedTestApp(nav)); @@ -47,7 +61,11 @@ describe('', () => { it('should have an anchor with correct href', () => { const nav = ( - } href={'/projects'} /> + } + href={'/projects'} + /> ); const rendered = render(wrapInThemedTestApp(nav)); diff --git a/frontend/packages/core/src/layout/SupportButton/SupportButton.test.js b/frontend/packages/core/src/layout/SupportButton/SupportButton.test.js index b2289542f1..d008fc42b6 100644 --- a/frontend/packages/core/src/layout/SupportButton/SupportButton.test.js +++ b/frontend/packages/core/src/layout/SupportButton/SupportButton.test.js @@ -2,7 +2,7 @@ import React from 'react'; import { render, fireEvent, waitForElement } from '@testing-library/react'; import SupportButton from './SupportButton'; -import { wrapInThemedTestApp, Keyboard } from 'testUtils'; +import { wrapInThemedTestApp, Keyboard } from '../../testUtils'; describe('', () => { it('renders without exploding', () => { diff --git a/frontend/packages/core/src/testUtils/Keyboard.js b/frontend/packages/core/src/testUtils/Keyboard.js new file mode 100644 index 0000000000..6e91a6b562 --- /dev/null +++ b/frontend/packages/core/src/testUtils/Keyboard.js @@ -0,0 +1,188 @@ +import { act, fireEvent } from '@testing-library/react'; + +const codes = { + Tab: 9, + Enter: 10, + Click: 17 /* This keyboard can click, deal with it */, + Esc: 27, +}; + +export default class Keyboard { + static async type(target, input) { + await new Keyboard(target).type(input); + } + + static async typeDebug(target, input) { + await new Keyboard(target, { debug: true }).type(input); + } + + static toReadableInput(chars) { + return chars.split('').map(char => { + switch (char.charCodeAt(0)) { + case codes.Tab: + return ''; + case codes.Enter: + return ''; + case codes.Click: + return ''; + case codes.Esc: + return ''; + default: + return char; + } + }); + } + + static fromReadableInput(input) { + return input.trim().replace(/\s*<([a-zA-Z]+)>\s*/g, (match, name) => { + if (name in codes) { + return String.fromCharCode(codes[name]); + } + throw new Error(`Unknown char name: '${name}'`); + }); + } + + constructor(target, { debug = false } = {}) { + this.debug = debug; + + if (target.ownerDocument) { + this.document = target.ownerDocument; + } else if (target.baseElement) { + this.document = target.baseElement.ownerDocument; + } else { + throw TypeError('Keyboard(target): target must be DOM node or react-testing-library render() output'); + } + } + + toString() { + return `Keyboard{document=${this.document}, debug=${this.debug}}`; + } + + _log(message, ...args) { + if (this.debug) { + console.log(`[Keyboard] ${message}`, ...args); + } + } + + _pretty(element) { + const attrs = [...element.attributes].map(attr => `${attr.name}="${attr.value}"`).join(' '); + return `<${element.nodeName.toLowerCase()} ${attrs}>`; + } + + get focused() { + return this.document.activeElement; + } + + async type(input) { + this._log(`sending sequence '${input}' with initial focus ${this._pretty(this.focused)}`); + await this.send(Keyboard.fromReadableInput(input)); + } + + async send(chars) { + for (const key of chars.split('')) { + const charCode = key.charCodeAt(0); + + if (charCode === codes.Tab) { + await this.tab(); + continue; + } + + const focused = this.focused; + if (!focused || focused === this.document.body) { + throw Error(`No element focused in document while trying to type '${Keyboard.toReadableInput(chars)}'`); + } + const nextValue = (focused.value || '') + key; + + if (charCode >= 32) { + await this._sendKey(key, charCode, () => { + this._log(`sending +${key} = '${nextValue}' to ${this._pretty(focused)}`); + fireEvent.change(focused, { + target: { value: nextValue }, + bubbles: true, + cancelable: true, + }); + }); + } else if (charCode === codes.Enter) { + await this.enter(focused.value || ''); + } else if (charCode === codes.Esc) { + await this.escape(); + } else if (charCode === codes.Click) { + await this.click(); + } else { + throw new Error(`Unsupported char code, ${charCode}`); + } + } + } + + async click() { + this._log(`clicking ${this._pretty(this.focused)}`); + await act(async () => fireEvent.click(this.focused)); + } + + async tab() { + await this._sendKey('Tab', codes.Tab, () => { + const focusable = this.document.querySelectorAll( + [ + 'a[href]', + 'area[href]', + 'input:not([disabled])', + 'select:not([disabled])', + 'textarea:not([disabled])', + 'button:not([disabled])', + 'iframe', + 'object', + 'embed', + '*[tabindex]', + '*[contenteditable]', + ].join(','), + ); + + const tabbable = [...focusable].filter(el => { + return el.tabIndex >= 0; + }); + + const focused = this.document.activeElement; + const focusedIndex = tabbable.indexOf(focused); + const nextFocus = tabbable[focusedIndex + (1 % tabbable.length)]; + + this._log(`tabbing to ${this._pretty(nextFocus)} ${this.focused.textContent}`); + nextFocus.focus(); + }); + } + + async enter(value) { + this._log(`submitting '${value}' via ${this._pretty(this.focused)}`); + await act(() => + this._sendKey('Enter', codes.Enter, () => { + if (this.focused.type === 'button') { + fireEvent.click(this.focused, { target: { value } }); + } else { + fireEvent.submit(this.focused, { + target: { value }, + bubbles: true, + cancelable: true, + }); + } + }), + ); + } + + async escape() { + this._log(`escape from ${this._pretty(this.focused)}`); + await act(async () => this._sendKey('Escape', codes.Esc)); + } + + async _sendKey(key, charCode, action) { + const event = { key, charCode, keyCode: charCode, which: charCode }; + const focused = this.focused; + + if (fireEvent.keyDown(focused, event)) { + if (fireEvent.keyPress(focused, event)) { + if (action) { + action(); + } + } + } + fireEvent.keyUp(focused, event); + } +} diff --git a/frontend/packages/core/src/testUtils/Keyboard.test.js b/frontend/packages/core/src/testUtils/Keyboard.test.js new file mode 100644 index 0000000000..5ea108d8dc --- /dev/null +++ b/frontend/packages/core/src/testUtils/Keyboard.test.js @@ -0,0 +1,79 @@ +import React from 'react'; +import Keyboard from './Keyboard'; +import { render } from '@testing-library/react'; + +describe('testUtils.Keyboard', () => { + it('types into some inputs with focus and submits a form', async () => { + const typed1 = []; + const typed2 = []; + const typed3 = []; + + let submitted = false; + const handleSubmit = event => { + event.preventDefault(); + submitted = true; + }; + + const rendered = render( +
+ typed1.push(value)} /> + typed2.push(value)} autoFocus /> + typed3.push(value)} /> +
, + ); + + const keyboard = new Keyboard(rendered); + await keyboard.send('xy'); + await keyboard.tab(); + await keyboard.send('abc'); + await keyboard.enter(); + + expect(typed1).toEqual([]); + expect(typed2).toEqual(['x', 'xy']); + expect(typed3).toEqual(['a', 'ab', 'abc']); + expect(submitted).toBe(true); + }); + + it('can use Keyboard.type to send readable input', async () => { + const typed1 = []; + const typed2 = []; + const typed3 = []; + + let submitted = false; + const handleSubmit = event => { + event.preventDefault(); + submitted = true; + }; + + const rendered = render( +
+ typed1.push(value)} /> + typed2.push(value)} /> + typed3.push(value)} /> +
, + ); + + await Keyboard.type(rendered, ' a b c '); + + expect(typed1).toEqual(['1a']); + expect(typed2).toEqual(['2b']); + expect(typed3).toEqual(['3c']); + expect(submitted).toBe(true); + }); + + it('should be able to navigate a radio input with click', async () => { + const selections = []; + + const rendered = render( +
selections.push(value)}> + + + +
, + ); + + await Keyboard.type(rendered, ' '); + + expect(selections).toEqual(['a', 'c']); + }); +}); diff --git a/frontend/packages/core/src/testUtils/index.js b/frontend/packages/core/src/testUtils/index.js new file mode 100644 index 0000000000..5c5ed40741 --- /dev/null +++ b/frontend/packages/core/src/testUtils/index.js @@ -0,0 +1,281 @@ +/** + * Helpers for testing redux components and containers + */ +import { MuiThemeProvider } from '@material-ui/core'; +import { ThemeProvider } from '@material-ui/styles'; +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { ApolloProvider } from '@apollo/react-hooks'; +import { Provider } from 'react-redux'; +import { MemoryRouter } from 'react-router'; +import { Route } from 'react-router-dom'; +import configureStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; + +import { V1 } from 'core/app/Themes'; +import defaultAppState from 'core/defaultAppState'; +import store from 'core/store'; +import ErrorBoundary from 'shared/components/ErrorBoundary'; +import { SearchProvider } from 'plugins/searchPage/SearchContext'; + +import { render } from '@testing-library/react'; +import { FirestoreProvider } from 'shared/apis/firestore'; +import MockFirestoreStorage from 'shared/apis/firestore/MockFirestoreStorage'; +import { UserInfoProvider, defaultMockUser } from 'shared/apis/user'; + +export { default as Keyboard } from './Keyboard'; +export { default as mockBreakpoint } from './mockBreakpoint'; + +export function wrapInTestApp(Component, initialRouterEntries, user = defaultMockUser) { + const Wrapper = Component instanceof Function ? Component : () => Component; + + return ( + + + + + + + + + + + + ); +} + +export function wrapInThemedTestApp(component, initialRouterEntries) { + const themed = ( + + {component} + + ); + return wrapInTestApp(themed, initialRouterEntries); +} + +export function wrapInSearchTestApp(component) { + const search = {component}; + return wrapInThemedTestApp(search); +} + +export function getTestStore(state) { + const appState = state ? state : defaultAppState; + const mockStore = configureStore([thunk]); + return mockStore(appState); +} + +export function getGoogleProfile() { + return { + getName: () => 'Niklas', + getGivenName: () => 'Niklas', + getEmail: () => 'niklas@spotify.com', + getImageUrl: () => 'http://img.url', + getId: () => '123456', + }; +} + +const DEFAULT_APOLLO_STATE = { + data: { loading: true }, +}; + +export const ApolloMock = ({ children, state = DEFAULT_APOLLO_STATE }) => { + // Hideous mocking of the ApolloClient to use the ApolloProvider + // The test do not have information about the GraphQL queries made by the components + // and hence we cant use MockedProvider and use the one below. + const mockClient = { + query: async () => state, + watchQuery: () => { + return { + refetch: () => {}, + fetchMore: () => {}, + updateQuery: () => {}, + startPolling: () => {}, + stopPolling: () => {}, + subscribeToMore: () => {}, + options: { + fetchPolicy: {}, + }, + getLastResult: () => {}, + currentResult: () => state, + getCurrentResult: () => state, + resetQueryStoreErrors: () => {}, + subscribe: () => {}, + setOptions: () => Promise.resolve({}), + }; + }, + }; + return {children}; +}; + +/** + * Return a builder for a given Component which will permit fluent immutable + * configuration and access to @testing-library/react rendering functions. + * + * A full example will make the utility of this more clear: + * + * const builder = buildComponentInApp(MyComponent) + * .withProps({myProp: 'myValue'}) + * + * // MuiThemeProvider will wrap MyComponent when this is called: + * .withTheme() + * + * // MemoryRouter can be configured with initialEntries: + * .withRouterEntries(['/']) + * + * // ApolloProvider wraps MyComponent with a mock client setup to + * // provide the given data: + * .withApolloData({ + * component: {id: 'my-component', system: 'my-system'} + * }); + * + * // Test 1: Use the defaults from above when mounting + * const { getByText } = builder.render(); + * expect(getByText('Display Text').querySelectorAll('td')).toHaveLength(1); + + * // Test 2: ApolloRouter provides loading=true state instead of valid data + * const { queryByText } = builder.withApolloLoading().render(); + * queryByText('Content').toBeNull(); + */ +export function buildComponentInApp(Component) { + function createBuilder(opt = {}) { + const builder = { + withProps: (props = {}) => + createBuilder({ + ...opt, + props: Object.assign(opt.props || {}, props), + }), + withTheme: (theme = V1) => + createBuilder({ + ...opt, + theme, + }), + withRouterEntries: routerEntries => + createBuilder({ + ...opt, + routerEntries, + }), + withApolloLoading: () => + // Support `data.loading` (errorOrLoading) or just `loading` (Query) + createBuilder({ + ...opt, + apolloState: { data: { loading: true }, loading: true }, + }), + withApolloData: data => + createBuilder({ + ...opt, + apolloState: { data: { loading: false, ...data } }, + }), + withApolloError: error => + createBuilder({ + ...opt, + apolloState: { error: error }, + }), + withUser: user => + createBuilder({ + ...opt, + user, + }), + build: (argProps = {}) => { + const props = { ...(opt.props || {}), ...argProps }; + const component = ; + const routed = wrapInTestApp(component, opt.routerEntries, opt.user); + const themed = opt.theme ? {routed} : routed; + return opt.apolloState ? {themed} : themed; + }, + render: props => { + return render(builder.build(props)); + }, + renderWithEffects: async props => { + return renderWithEffects(builder.build(props)); + }, + }; + return builder; + } + return createBuilder(); +} + +// Components using useEffect to perform an asynchronous action (such as fetch) must be rendered within an async +// act call to properly get the final state, even with mocked responses. This utility method makes the signature a bit +// cleaner, since act doesn't return the result of the evaluated function. +// https://github.com/testing-library/react-testing-library/issues/281 +// https://github.com/facebook/react/pull/14853 +export async function renderWithEffects(nodes) { + let value; + await act(async () => { + value = await render(nodes); + }); + return value; +} + +export function logBoundaryErrors() { + ErrorBoundary.onError = error => console.error('ErrorBoundary received error', error); +} + +// Runs a function with all error logging captured and returned as an array. +// If the callback function is async this one will be too. +export function withLogCollector(logsToCollect, callback) { + if (typeof logsToCollect === 'function') { + callback = logsToCollect; + logsToCollect = ['log', 'warn', 'error']; + } + const logs = { + log: [], + warn: [], + error: [], + }; + + const origLog = console.log; + const origWarn = console.warn; + const origError = console.error; + + if (logsToCollect.includes('log')) { + console.log = message => { + logs.log.push(message); + }; + } + if (logsToCollect.includes('warn')) { + console.warn = message => { + logs.warn.push(message); + }; + } + if (logsToCollect.includes('error')) { + console.error = message => { + logs.error.push(message); + }; + } + + const restore = () => { + console.log = origLog; + console.warn = origWarn; + console.error = origError; + }; + + try { + const ret = callback(); + + if (!ret || !ret.then) { + restore(); + return logs; + } + + return ret.then( + () => { + restore(); + return logs; + }, + error => { + restore(); + throw error; + }, + ); + } catch (error) { + restore(); + throw error; + } +} + +export const wrapInTheme = (component, theme = V1) => ( + + {component} + +); diff --git a/frontend/packages/core/src/testUtils/mockBreakpoint.ts b/frontend/packages/core/src/testUtils/mockBreakpoint.ts new file mode 100644 index 0000000000..388c71494c --- /dev/null +++ b/frontend/packages/core/src/testUtils/mockBreakpoint.ts @@ -0,0 +1,73 @@ +import { act } from '@testing-library/react'; + +type Breakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl'; + +const queryToBreakpoint = { + '(min-width:1920px)': 'xl', + '(min-width:1280px)': 'lg', + '(min-width:960px)': 'md', + '(min-width:600px)': 'sm', + '(min-width:0px)': 'xs', +} as Record; + +function toBreakpoint(query: string) { + const breakpoint = queryToBreakpoint[query]; + if (!breakpoint) { + throw new Error(`received unknown media query in breakpoint mock: '${query}'`); + } + return breakpoint; +} + +type Listener = (event: { matches: boolean }) => void; + +interface QueryList { + addListener(listener: Listener): void; + removeListener(listener: Listener): void; + matches: boolean; +} + +interface Query { + query: string; + queryList: QueryList; + listeners: Set; +} + +export default function mockBreakpoint(initialBreakpoint: Breakpoint = 'xl') { + let currentBreakpoint = initialBreakpoint; + const queries = Array(); + + (window as any).matchMedia = (query: string): QueryList => { + const listeners = new Set(); + + const queryList: QueryList = { + addListener(listener) { + listeners.add(listener); + }, + removeListener(listener) { + listeners.delete(listener); + }, + matches: toBreakpoint(query) === currentBreakpoint, + }; + + queries.push({ query, queryList, listeners }); + + return queryList; + }; + + return { + set(breakpoint: Breakpoint) { + currentBreakpoint = breakpoint; + + act(() => { + queries.forEach(({ query, queryList, listeners }) => { + const matches = toBreakpoint(query) === breakpoint; + queryList.matches = matches; + listeners.forEach(listener => listener({ matches })); + }); + }); + }, + remove() { + delete window.matchMedia; + }, + }; +} From 4d6dff9d0d4117193006a3754c784f929813e877 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 5 Feb 2020 10:54:17 +0100 Subject: [PATCH 03/12] Remove hello proto --- protos/hello.proto | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 protos/hello.proto diff --git a/protos/hello.proto b/protos/hello.proto deleted file mode 100644 index 8b6187dd0e..0000000000 --- a/protos/hello.proto +++ /dev/null @@ -1,15 +0,0 @@ -syntax = "proto3"; - -package spotify.backstage.hello.v1alpha1; - -service Hello { - rpc Hello(HelloRequest) returns (HelloReply); -} - -message HelloRequest { - string name = 1; -} - -message HelloReply { - string message = 1; -} From c323e853a85d482bc9a25cfbc4db25684e47e748 Mon Sep 17 00:00:00 2001 From: Raghunandan Balachandran Date: Wed, 5 Feb 2020 11:35:19 +0100 Subject: [PATCH 04/12] fix tests --- frontend/packages/core/package.json | 3 +- .../core/src/assets/bursts/bursts.css | 19 -- .../core/src/assets/bursts/circle-1.svg | 1 - .../core/src/assets/bursts/circle-2.svg | 1 - .../core/src/assets/bursts/rectangle-1.svg | 1 - .../core/src/assets/bursts/rectangle-2.svg | 1 - .../core/src/assets/bursts/rectangle-3.svg | 1 - .../core/src/assets/bursts/triangle-1.svg | 1 - .../core/src/assets/bursts/triangle-2.svg | 1 - .../core/src/assets/bursts/triangle-3.svg | 1 - .../src/components/Lifecycle/Lifecycle.js | 44 +++ .../core/src/components/Lifecycle/index.js | 1 + .../core/src/components/Status/Status.js | 79 ++++++ .../core/src/components/Status/index.js | 1 + frontend/packages/core/src/index.ts | 2 +- .../packages/core/src/layout/Header/Burst.js | 40 ++- .../HeaderActionMenu/HeaderActionMenu.js | 2 +- .../layout/HeaderActionMenu/MenuVertical.tsx | 11 + .../src/layout/HeaderLabel/HeaderLabel.js | 2 +- .../layout/HeaderLabel/OwnerHeaderLabel.js | 9 +- .../HeaderLabel/OwnerHeaderLabel.test.js | 2 +- .../core/src/layout/InfoCard/BottomLink.js | 2 +- .../layout/Navigation/GcpProjectByTools.tsx | 14 - .../src/layout/Navigation/InfoSquared.tsx | 11 - .../core/src/layout/Navigation/NavItem.js | 168 ----------- .../src/layout/Navigation/NavItem.test.js | 94 ------- .../core/src/layout/Navigation/Navigation.js | 91 ------ .../src/layout/Navigation/Navigation.test.js | 75 ----- .../core/src/layout/Page/PageThemeProvider.js | 135 +-------- .../packages/core/src/layout/Page/index.ts | 2 +- .../src/layout/SupportButton/SupportButton.js | 114 -------- .../SupportButton/SupportButton.test.js | 36 --- frontend/packages/core/src/testUtils/index.js | 264 +----------------- .../packages/core/src/theme/BackstageTheme.js | 2 +- frontend/yarn.lock | 83 +++++- 35 files changed, 275 insertions(+), 1039 deletions(-) delete mode 100644 frontend/packages/core/src/assets/bursts/bursts.css delete mode 100644 frontend/packages/core/src/assets/bursts/circle-1.svg delete mode 100644 frontend/packages/core/src/assets/bursts/circle-2.svg delete mode 100644 frontend/packages/core/src/assets/bursts/rectangle-1.svg delete mode 100644 frontend/packages/core/src/assets/bursts/rectangle-2.svg delete mode 100644 frontend/packages/core/src/assets/bursts/rectangle-3.svg delete mode 100644 frontend/packages/core/src/assets/bursts/triangle-1.svg delete mode 100644 frontend/packages/core/src/assets/bursts/triangle-2.svg delete mode 100644 frontend/packages/core/src/assets/bursts/triangle-3.svg create mode 100644 frontend/packages/core/src/components/Lifecycle/Lifecycle.js create mode 100644 frontend/packages/core/src/components/Lifecycle/index.js create mode 100644 frontend/packages/core/src/components/Status/Status.js create mode 100644 frontend/packages/core/src/components/Status/index.js create mode 100644 frontend/packages/core/src/layout/HeaderActionMenu/MenuVertical.tsx delete mode 100644 frontend/packages/core/src/layout/Navigation/GcpProjectByTools.tsx delete mode 100644 frontend/packages/core/src/layout/Navigation/InfoSquared.tsx delete mode 100644 frontend/packages/core/src/layout/Navigation/NavItem.js delete mode 100644 frontend/packages/core/src/layout/Navigation/NavItem.test.js delete mode 100644 frontend/packages/core/src/layout/Navigation/Navigation.js delete mode 100644 frontend/packages/core/src/layout/Navigation/Navigation.test.js delete mode 100644 frontend/packages/core/src/layout/SupportButton/SupportButton.js delete mode 100644 frontend/packages/core/src/layout/SupportButton/SupportButton.test.js diff --git a/frontend/packages/core/package.json b/frontend/packages/core/package.json index fb5f3cc6de..7e6782bbb2 100644 --- a/frontend/packages/core/package.json +++ b/frontend/packages/core/package.json @@ -16,7 +16,8 @@ "react-dom": "^16.12.0", "react-helmet":"5.2.1", "tinycolor2": "1.4.1", - "react-addons-text-content": "0.0.4" + "react-addons-text-content": "0.0.4", + "recompose": "0.30.0" }, "scripts": { "lint": "web-scripts lint", diff --git a/frontend/packages/core/src/assets/bursts/bursts.css b/frontend/packages/core/src/assets/bursts/bursts.css deleted file mode 100644 index 92499bf056..0000000000 --- a/frontend/packages/core/src/assets/bursts/bursts.css +++ /dev/null @@ -1,19 +0,0 @@ - -.burst { - position: absolute; - top: 0; - left: 0; - height: 100%; - width: 100%; - z-index: -1; -} - -/* base style of burst shape SVGs */ -.burst-shape { - position: absolute; - right: 0; - height: 100%; - background-repeat: no-repeat; - background-size: cover; - opacity:0.1; -} diff --git a/frontend/packages/core/src/assets/bursts/circle-1.svg b/frontend/packages/core/src/assets/bursts/circle-1.svg deleted file mode 100644 index 9ee5157ab8..0000000000 --- a/frontend/packages/core/src/assets/bursts/circle-1.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/frontend/packages/core/src/assets/bursts/circle-2.svg b/frontend/packages/core/src/assets/bursts/circle-2.svg deleted file mode 100644 index 15b89e96db..0000000000 --- a/frontend/packages/core/src/assets/bursts/circle-2.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/frontend/packages/core/src/assets/bursts/rectangle-1.svg b/frontend/packages/core/src/assets/bursts/rectangle-1.svg deleted file mode 100644 index 4403b0031a..0000000000 --- a/frontend/packages/core/src/assets/bursts/rectangle-1.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/frontend/packages/core/src/assets/bursts/rectangle-2.svg b/frontend/packages/core/src/assets/bursts/rectangle-2.svg deleted file mode 100644 index 2d307d75f6..0000000000 --- a/frontend/packages/core/src/assets/bursts/rectangle-2.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/frontend/packages/core/src/assets/bursts/rectangle-3.svg b/frontend/packages/core/src/assets/bursts/rectangle-3.svg deleted file mode 100644 index 947df51df9..0000000000 --- a/frontend/packages/core/src/assets/bursts/rectangle-3.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/frontend/packages/core/src/assets/bursts/triangle-1.svg b/frontend/packages/core/src/assets/bursts/triangle-1.svg deleted file mode 100644 index 7a33cf3f5c..0000000000 --- a/frontend/packages/core/src/assets/bursts/triangle-1.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/frontend/packages/core/src/assets/bursts/triangle-2.svg b/frontend/packages/core/src/assets/bursts/triangle-2.svg deleted file mode 100644 index 184e714b89..0000000000 --- a/frontend/packages/core/src/assets/bursts/triangle-2.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/frontend/packages/core/src/assets/bursts/triangle-3.svg b/frontend/packages/core/src/assets/bursts/triangle-3.svg deleted file mode 100644 index e8b0cb1f7c..0000000000 --- a/frontend/packages/core/src/assets/bursts/triangle-3.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/frontend/packages/core/src/components/Lifecycle/Lifecycle.js b/frontend/packages/core/src/components/Lifecycle/Lifecycle.js new file mode 100644 index 0000000000..5c11599f92 --- /dev/null +++ b/frontend/packages/core/src/components/Lifecycle/Lifecycle.js @@ -0,0 +1,44 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +class Lifecycle extends Component { + static propTypes = { + isShorthand: PropTypes.bool, + fontSize: PropTypes.string, + }; +} + +const styles = { + alpha: { + color: '#d00150', + fontFamily: 'serif', + fontWeight: 'normal', + fontStyle: 'italic', + }, + beta: { + color: '#4d65cc', + fontFamily: 'serif', + fontWeight: 'normal', + fontStyle: 'italic', + }, +}; + +export class AlphaLabel extends Lifecycle { + render() { + const style = fontSize => ({ ...styles.alpha, fontSize, ...this.props.style }); + return this.props.isShorthand ? ( + α + ) : ( + Alpha + ); + } +} + +export class BetaLabel extends Lifecycle { + render() { + const fontSize = this.props.fontSize ? this.props.fontSize : this.props.isShorthand ? '120%' : '100%'; + const style = { ...styles.beta, fontSize, ...this.props.style }; + + return this.props.isShorthand ? β : Beta; + } +} diff --git a/frontend/packages/core/src/components/Lifecycle/index.js b/frontend/packages/core/src/components/Lifecycle/index.js new file mode 100644 index 0000000000..a5b40c210e --- /dev/null +++ b/frontend/packages/core/src/components/Lifecycle/index.js @@ -0,0 +1 @@ +export { AlphaLabel, BetaLabel } from './Lifecycle'; diff --git a/frontend/packages/core/src/components/Status/Status.js b/frontend/packages/core/src/components/Status/Status.js new file mode 100644 index 0000000000..04fff2a552 --- /dev/null +++ b/frontend/packages/core/src/components/Status/Status.js @@ -0,0 +1,79 @@ +import React from 'react'; +import { pure } from 'recompose'; +import { withStyles } from '@material-ui/core'; + +const styles = theme => ({ + status: { + width: 12, + height: 12, + display: 'inline-block', + marginRight: 1, + }, + ok: { + backgroundColor: theme.palette.status.ok, + borderRadius: '50%', + }, + warning: { + backgroundColor: theme.palette.status.warning, + }, + error: { + width: '0', + height: '0', + borderLeft: '7px solid transparent', + borderRight: '7px solid transparent', + borderBottom: `14px solid ${theme.palette.status.error}`, + }, + pending: { + backgroundColor: theme.palette.status.pending, + }, + failed: { + backgroundColor: 'rgba(245, 155, 35, 0.5)', + }, + running: { + animation: 'blink 0.8s step-start 0s infinite', + backgroundColor: theme.palette.status.running, + }, + '@keyframes blink': { + '50%': { + backgroundColor: theme.palette.status.background, + }, + }, +}); + +export const StatusOK = pure( + withStyles(styles)(props => ( + + )), +); + +export const StatusWarning = pure( + withStyles(styles)(props => ( + + )), +); + +export const StatusError = pure( + withStyles(styles)(props => ( + + )), +); + +export const StatusNA = pure(() => N/A); + +export const StatusPending = pure( + withStyles(styles)(props => ( + + )), +); + +export const StatusRunning = pure( + withStyles(styles)(props => ( + + )), +); + +export const StatusFailed = pure( + withStyles(styles)(props => ( + + )), +); diff --git a/frontend/packages/core/src/components/Status/index.js b/frontend/packages/core/src/components/Status/index.js new file mode 100644 index 0000000000..4b037648e3 --- /dev/null +++ b/frontend/packages/core/src/components/Status/index.js @@ -0,0 +1 @@ +export { StatusError, StatusFailed, StatusNA, StatusOK, StatusPending, StatusRunning, StatusWarning } from './Status'; diff --git a/frontend/packages/core/src/index.ts b/frontend/packages/core/src/index.ts index f741f0e384..353b29ff72 100644 --- a/frontend/packages/core/src/index.ts +++ b/frontend/packages/core/src/index.ts @@ -1,7 +1,7 @@ export * from './types'; export { default as createPlugin } from './createPlugin'; export { default as Page } from '../src/layout/Page'; -export { shapes, gradients, theme } from '../src/layout/Page'; +export { gradients, theme } from '../src/layout/Page'; export { default as Header } from '../src/layout/Header/Header'; export { default as HeaderLabel } from '../src/layout/HeaderLabel'; export { default as InfoCard } from '../src/layout/InfoCard'; diff --git a/frontend/packages/core/src/layout/Header/Burst.js b/frontend/packages/core/src/layout/Header/Burst.js index 64d95f7e07..f61da40956 100644 --- a/frontend/packages/core/src/layout/Header/Burst.js +++ b/frontend/packages/core/src/layout/Header/Burst.js @@ -1,16 +1,34 @@ -import React, { Component } from 'react'; -import '../../assets/bursts/bursts.css'; +import React from 'react'; +import { makeStyles } from '@material-ui/core'; -class Burst extends Component { - render() { - const { theme } = this.props; - - return ( -
-
-
- ); +const useStyles = makeStyles(theme => ({ + burst: { + position: 'absolute', + top: 0, + left: 0, + height: '100%', + width: '100%', + 'z-index': -1, + }, + /* base style of burst shape SVGs */ + burstShape: { + position: 'absolute', + right: 0, + height: '100%', + 'background-repeat': 'no-repeat', + 'background-size': 'cover', + opacity:0.1, } +})); + +const Burst = ({theme}) => { + const classes = useStyles(); + + return ( +
+
+
+ ); } export default Burst; diff --git a/frontend/packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.js b/frontend/packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.js index d16556d4c2..4fc30019a3 100644 --- a/frontend/packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.js +++ b/frontend/packages/core/src/layout/HeaderActionMenu/HeaderActionMenu.js @@ -1,6 +1,6 @@ import React, { Fragment } from 'react'; import { IconButton, List, ListItem, ListItemIcon, ListItemText, Popover } from '@material-ui/core'; -import { KebabMenuIcon } from 'shared/icons'; +import { default as KebabMenuIcon } from './MenuVertical'; const ActionItem = ({ label, secondaryLabel, icon, disabled = false, onClick, WrapperComponent = React.Fragment }) => { return ( diff --git a/frontend/packages/core/src/layout/HeaderActionMenu/MenuVertical.tsx b/frontend/packages/core/src/layout/HeaderActionMenu/MenuVertical.tsx new file mode 100644 index 0000000000..fa744f861e --- /dev/null +++ b/frontend/packages/core/src/layout/HeaderActionMenu/MenuVertical.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon'; + +const SvgMenuVertical = (props: SvgIconProps) => + React.createElement( + SvgIcon, + props, + , + ); + +export default SvgMenuVertical; diff --git a/frontend/packages/core/src/layout/HeaderLabel/HeaderLabel.js b/frontend/packages/core/src/layout/HeaderLabel/HeaderLabel.js index 91290412b3..72f2cc86a0 100644 --- a/frontend/packages/core/src/layout/HeaderLabel/HeaderLabel.js +++ b/frontend/packages/core/src/layout/HeaderLabel/HeaderLabel.js @@ -41,7 +41,7 @@ class HeaderLabel extends Component { return ( {label} - {url ? {content} : content} + {url ? {content} : content} ); } diff --git a/frontend/packages/core/src/layout/HeaderLabel/OwnerHeaderLabel.js b/frontend/packages/core/src/layout/HeaderLabel/OwnerHeaderLabel.js index 5c71c1b6ea..01a694e99a 100644 --- a/frontend/packages/core/src/layout/HeaderLabel/OwnerHeaderLabel.js +++ b/frontend/packages/core/src/layout/HeaderLabel/OwnerHeaderLabel.js @@ -1,10 +1,9 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { Tooltip, withStyles } from '@material-ui/core'; +import { Tooltip, Link, withStyles } from '@material-ui/core'; -import { StatusError } from 'shared/components/Status'; -import Link from 'shared/components/Link'; -import HeaderLabel from 'shared/components/layout/HeaderLabel'; +import { StatusError } from '../../components/Status'; +import HeaderLabel from './HeaderLabel'; const style = theme => ({ notVerified: { @@ -33,7 +32,7 @@ class OwnerHeaderLabel extends Component { const isBadSquad = owner.type !== 'squad'; const notVerified = isBadSquad && ( - + Squad not verified! diff --git a/frontend/packages/core/src/layout/HeaderLabel/OwnerHeaderLabel.test.js b/frontend/packages/core/src/layout/HeaderLabel/OwnerHeaderLabel.test.js index b4e64f64b0..8ced9035e0 100644 --- a/frontend/packages/core/src/layout/HeaderLabel/OwnerHeaderLabel.test.js +++ b/frontend/packages/core/src/layout/HeaderLabel/OwnerHeaderLabel.test.js @@ -1,5 +1,5 @@ import React from 'react'; -import { render } from '@testing-library/react'; +import { render, prettyDOM } from '@testing-library/react'; import OwnerHeaderLabel from './OwnerHeaderLabel'; import { wrapInThemedTestApp } from '../../testUtils'; diff --git a/frontend/packages/core/src/layout/InfoCard/BottomLink.js b/frontend/packages/core/src/layout/InfoCard/BottomLink.js index c6b259cb72..d248da6063 100644 --- a/frontend/packages/core/src/layout/InfoCard/BottomLink.js +++ b/frontend/packages/core/src/layout/InfoCard/BottomLink.js @@ -17,7 +17,7 @@ export default class BottomLink extends Component { return (
- + diff --git a/frontend/packages/core/src/layout/Navigation/GcpProjectByTools.tsx b/frontend/packages/core/src/layout/Navigation/GcpProjectByTools.tsx deleted file mode 100644 index 104fcee7d3..0000000000 --- a/frontend/packages/core/src/layout/Navigation/GcpProjectByTools.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon'; - -const SvgGcpProjectByTools = (props: SvgIconProps) => - React.createElement( - SvgIcon, - props, - , - ); - -export default SvgGcpProjectByTools; diff --git a/frontend/packages/core/src/layout/Navigation/InfoSquared.tsx b/frontend/packages/core/src/layout/Navigation/InfoSquared.tsx deleted file mode 100644 index 0f51727069..0000000000 --- a/frontend/packages/core/src/layout/Navigation/InfoSquared.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; -import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon'; - -const SvgInfoSquared = (props: SvgIconProps) => - React.createElement( - SvgIcon, - props, - , - ); - -export default SvgInfoSquared; diff --git a/frontend/packages/core/src/layout/Navigation/NavItem.js b/frontend/packages/core/src/layout/Navigation/NavItem.js deleted file mode 100644 index b6d47ab9f6..0000000000 --- a/frontend/packages/core/src/layout/Navigation/NavItem.js +++ /dev/null @@ -1,168 +0,0 @@ -import React, { Component, Fragment } from 'react'; -import { matchPath } from 'react-router'; -import { ListItem, ListItemIcon, ListItemText, Typography, withStyles, Tooltip, IconButton } from '@material-ui/core'; -import { AlphaLabel, BetaLabel } from 'shared/components/Lifecycle'; -import Link from 'shared/components/Link'; -import { Theme } from 'shared/components/layout/Page'; -import ArrowDropUp from '@material-ui/icons/ArrowDropUp'; -import ArrowDropDown from '@material-ui/icons/ArrowDropDown'; - -const styles = theme => ({ - root: { - display: 'block', - overflow: 'hidden', - '&:first-child': { - borderTopRightRadius: 6, - borderTopLeftRadius: 6, - }, - '&:last-child': { - borderBottomRightRadius: 6, - borderBottomLeftRadius: 6, - }, - }, - child: { - paddingLeft: theme.spacing(7), - '&:last-child': { - borderBottom: '1px solid #727272', - }, - }, - boldLabel: { - color: '#FFFFFF', - fontWeight: 'bolder', - }, - childLabel: { - color: theme.palette.textVerySubtle, - fontSize: 14, - }, - label: { - whiteSpace: 'nowrap', - lineHeight: 1.0, - }, - iconImg: { - width: 24, - height: 24, - }, - icon: { - margin: theme.spacing(0.5, 2, 0.5, 0), - minWidth: 0, - }, - expand: { - color: 'white', - }, -}); - -class NavItem extends Component { - static defaultProps = { - title: 'Mystery Link', - href: '/', - exact: true, // Only used for react-router matchPath to see if this sidebar item is active - icon: null, - isAlpha: false, - isBeta: false, - type: 'other', // Provided by Navigation - child: false, // Whether this is a child of another NavItem (for indenting) - }; - - // If NavItem has React children, the sub-navigation can be expanded or collapsed - state = { - expanded: false, - }; - - static isActive(href, exact) { - return matchPath(window.location.pathname, { path: href, exact: exact }); - } - - maybeActiveNavStyle(theme) { - return NavItem.isActive(this.props.href, this.props.exact) - ? { - background: `linear-gradient(to right, ${theme.activeNavLinkColor} 0px, ${theme.activeNavLinkColor} 6px, #4F4F4F 6px, #4F4F4F 100%)`, - } - : {}; - } - - renderIcon = (condensed, iconElement, title, classes) => { - if (!iconElement) return null; - const listItemIcon = {iconElement}; - return condensed ? ( - - {listItemIcon} - - ) : ( - listItemIcon - ); - }; - - render() { - const { title, href, icon, isAlpha, isBeta, classes, condensed, child, children } = this.props; - const { expanded } = this.state; - const isSection = !!children; - - // Automatically expand nested nav if one of the child elements is active - const isChildActive = React.Children.toArray(children).some(c => NavItem.isActive(c.props.href, c.props.exact)); - const isExpanded = expanded || isChildActive; - const selfAwareChildren = children && React.cloneElement(children, { child: true }); - - let iconElement; - - if (icon) { - iconElement = typeof icon === 'object' ? icon : {title}; - } - - return ( - - {theme => ( - <> - - - {this.renderIcon(condensed, iconElement, title, classes)} - {!condensed && ( - <> - - {title} - {isAlpha && ( - -   - - - )} - {isBeta && ( - -   - - - )} - - } - disableTypography - /> - {isSection && ( - this.setState(prev => ({ expanded: !prev.expanded }))} - style={{ padding: 4 }} - data-testid="expand-toggle" - > - {isExpanded ? ( - - ) : ( - - )} - - )} - - )} - - - {isExpanded && selfAwareChildren} - - )} - - ); - } -} - -export default withStyles(styles)(NavItem); diff --git a/frontend/packages/core/src/layout/Navigation/NavItem.test.js b/frontend/packages/core/src/layout/Navigation/NavItem.test.js deleted file mode 100644 index 3cc95b635b..0000000000 --- a/frontend/packages/core/src/layout/Navigation/NavItem.test.js +++ /dev/null @@ -1,94 +0,0 @@ -import React from 'react'; -import { matchPath } from 'react-router'; -import { fireEvent, render } from '@testing-library/react'; -import NavItem from './NavItem'; -import { wrapInThemedTestApp } from '../../testUtils'; - -jest.mock('react-router', () => ({ - ...jest.requireActual('react-router'), - matchPath: jest.fn(), -})); - -const minProps = { - title: 'A link title', - href: '/mocked', -}; - -const minChildProps = { - title: 'A child title', - href: '/child', -}; - -describe('', () => { - it('renders without exploding', () => { - const rendered = render(wrapInThemedTestApp()); - expect(rendered.getByText('A link title')).toBeInTheDocument(); - }); - - it('should have an anchor with correct href', () => { - const rendered = render(wrapInThemedTestApp()); - const anchor = rendered.container.querySelector('a'); - expect(anchor.href).toBe('http://localhost/mocked'); - }); - - it('renders with icon src', () => { - const rendered = render(wrapInThemedTestApp()); - const icon = rendered.getByAltText('A link title'); - expect(icon.src).toBe('http://localhost/mocked-icon.png'); - }); - - it('renders with icon component', () => { - const Falafel = () =>
falafel
; - const rendered = render(wrapInThemedTestApp(} {...minProps} />)); - const icon = rendered.getByText('falafel'); - expect(icon).toBeInTheDocument(); - }); - - it('shows alpha icon', () => { - const rendered = render(wrapInThemedTestApp()); - expect(rendered.getByText('α')).toBeInTheDocument(); - }); - - it('shows beta icon', () => { - const rendered = render(wrapInThemedTestApp()); - expect(rendered.getByText('β')).toBeInTheDocument(); - }); - - it('renders nested navigation without exploding', () => { - const { getByText, getByTestId } = render( - wrapInThemedTestApp( - - - , - ), - ); - expect(getByText('A link title')).toBeInTheDocument(); - expect(getByTestId('expand-toggle')).toBeInTheDocument(); - }); - - it('toggles nested navigation', () => { - const { getByText, getByTestId, queryByText } = render( - wrapInThemedTestApp( - - - , - ), - ); - expect(queryByText('A child link')).not.toBeInTheDocument(); - fireEvent.click(getByTestId('expand-toggle')); - expect(getByText('A child title')).toBeInTheDocument(); - }); - - it('shows nested navigation automatically if the child is active', () => { - matchPath.mockReturnValue(true); - const { getByText } = render( - wrapInThemedTestApp( - - - , - ), - ); - expect(getByText('A link title')).toBeInTheDocument(); - expect(getByText('A child title')).toBeInTheDocument(); - }); -}); diff --git a/frontend/packages/core/src/layout/Navigation/Navigation.js b/frontend/packages/core/src/layout/Navigation/Navigation.js deleted file mode 100644 index fc5c15340f..0000000000 --- a/frontend/packages/core/src/layout/Navigation/Navigation.js +++ /dev/null @@ -1,91 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { List, withStyles } from '@material-ui/core'; -import ChevronLeft from '@material-ui/icons/ChevronLeft'; -import ChevronRight from '@material-ui/icons/ChevronRight'; - -import { IconButton } from 'shared/components'; -import { getFromStore, saveToStore } from 'shared/apis/storage/persistentStore'; - -const styles = theme => ({ - nav: { - gridArea: 'pageNav', - width: 62, - marginLeft: theme.spacing(3), - marginTop: theme.spacing(3), - transition: 'width 0.07s, height 0s', - transitionTimingFunction: 'ease-in', - }, - item: { - display: 'block', - }, - list: { - padding: 0, - border: '0px solid #333333', - borderRadius: 6, - backgroundColor: '#333333', - }, - buttonContainer: { - marginLeft: 'auto', - marginRight: 'auto', - marginTop: theme.spacing(0.5), - width: 48, - }, - button: { - color: '#888', - }, -}); - -class Navigation extends Component { - static propTypes = { - condensable: PropTypes.bool, - }; - - state = { - condensed: false, - }; - - constructor(props) { - super(props); - - const condensedPref = getFromStore('navigation', 'condensed', false); - this.state.condensed = this.props.condensable && condensedPref; - } - - buttonClicked = () => { - const condensed = !this.state.condensed; - saveToStore('navigation', 'condensed', condensed); - this.setState({ condensed }); - }; - - render() { - const { classes, condensable } = this.props; - const { condensed } = this.state; - - const ExpandButton = condensable && ( -
- - {condensed ? : } - -
- ); - - const children = React.Children.map(this.props.children, child => { - if (!child) return null; - return React.cloneElement(child, { - condensed: condensed, - }); - }); - - return ( - - ); - } -} - -export default withStyles(styles)(Navigation); diff --git a/frontend/packages/core/src/layout/Navigation/Navigation.test.js b/frontend/packages/core/src/layout/Navigation/Navigation.test.js deleted file mode 100644 index ed18457637..0000000000 --- a/frontend/packages/core/src/layout/Navigation/Navigation.test.js +++ /dev/null @@ -1,75 +0,0 @@ -import React from 'react'; -import { render, fireEvent } from '@testing-library/react'; - -import Navigation from './Navigation'; -import NavItem from './NavItem'; -import { wrapInThemedTestApp } from '../../testUtils'; - -import { default as GcpProjectIcon } from './GcpProjectByTools'; -import { default as OverviewIcon } from './InfoSquared'; - -describe('', () => { - it('renders without exploding', () => { - const nav = ( - - } href={'/'} /> - } - href={'/projects'} - /> - - ); - const rendered = render(wrapInThemedTestApp(nav)); - expect(rendered.getByText('Overview')).toBeInTheDocument(); - expect(rendered.getByText('Projects')).toBeInTheDocument(); - }); - - it('renders a condenseable navigation bar', () => { - const nav = ( - - } href={'/'} /> - } - href={'/projects'} - /> - - ); - const rendered = render(wrapInThemedTestApp(nav)); - const btn = rendered.getByTestId('nav-expand-toggle'); - expect(btn).toBeInTheDocument(); - }); - - it('is condenseable', () => { - const nav = ( - - } href={'/'} /> - } - href={'/projects'} - /> - - ); - const rendered = render(wrapInThemedTestApp(nav)); - const btn = rendered.getByTestId('nav-expand-toggle'); - fireEvent.click(btn); - expect(rendered.queryByText('Overview')).not.toBeInTheDocument(); - }); - - it('should have an anchor with correct href', () => { - const nav = ( - - } - href={'/projects'} - /> - - ); - const rendered = render(wrapInThemedTestApp(nav)); - const anchor = rendered.container.querySelector('a'); - expect(anchor.href).toBe('http://localhost/projects'); - }); -}); diff --git a/frontend/packages/core/src/layout/Page/PageThemeProvider.js b/frontend/packages/core/src/layout/Page/PageThemeProvider.js index d6832b82d3..60fef3487e 100644 --- a/frontend/packages/core/src/layout/Page/PageThemeProvider.js +++ b/frontend/packages/core/src/layout/Page/PageThemeProvider.js @@ -1,52 +1,3 @@ -import '../../assets/bursts/bursts.css'; -import circle1 from '../../assets/bursts/circle-1.svg'; -import circle2 from '../../assets/bursts/circle-2.svg'; -import triangle1 from '../../assets/bursts/triangle-1.svg'; -import triangle2 from '../../assets/bursts/triangle-2.svg'; -import triangle3 from '../../assets/bursts/triangle-3.svg'; -import rectangle1 from '../../assets/bursts/rectangle-1.svg'; -import rectangle2 from '../../assets/bursts/rectangle-2.svg'; - -// TODO move this file to shared and out of core - -export const shapes = { - circle1: { - backgroundImage: `url(${circle1})`, - width: 'calc(400px + 20%)', - backgroundPosition: '100px 60%', - }, - circle2: { - backgroundImage: `url(${circle2})`, - width: 'calc(400px + 20%)', - backgroundPosition: '100px 58%', - }, - rectangle1: { - backgroundImage: `url(${rectangle1})`, - width: 'calc(400px + 20%)', - backgroundPosition: '100px 60%', - }, - rectangle2: { - backgroundImage: `url(${rectangle2})`, - width: 'calc(400px + 20%)', - backgroundPosition: '30px 35%', - }, - triangle1: { - backgroundImage: `url(${triangle1})`, - width: 'calc(500px + 30%)', - backgroundPosition: '100px calc(50px + 45%)', - }, - triangle2: { - backgroundImage: `url(${triangle2})`, - width: 'calc(400px + 20%)', - backgroundPosition: '100px 38%', - }, - triangle3: { - backgroundImage: `url(${triangle3})`, - width: 'calc(400px + 20%)', - backgroundPosition: '100px 76%', - }, -}; - export const gradients = { blue: 'linear-gradient(135deg, #2D46B9 0%, #509BF5 100%)', darkBlue: 'linear-gradient(44deg, #1E3264 0%, #A0C3D2 100%)', @@ -71,102 +22,26 @@ export const theme = { service: { activeNavLinkColor: '#1D7F6E', gradient: gradients.tealGreen, - burstShape: shapes.rectangle2, - }, - appFeature: { - activeNavLinkColor: '#FF6437', - gradient: gradients.orangeYellow, - burstShape: shapes.triangle1, - }, - app: { - activeNavLinkColor: '#A72525', - gradient: gradients.redOrange, - burstShape: shapes.triangle1, - }, - endpoint: { - activeNavLinkColor: '#9315b0', - gradient: gradients.purpleBlue, - burstShape: shapes.circle1, - }, - pipeline: { - activeNavLinkColor: '#9315b0', - gradient: gradients.purpleBlue, - burstShape: shapes.circle1, + burstShape: null, }, website: { activeNavLinkColor: '#765d90', gradient: gradients.purple, - burstShape: shapes.rectangle2, - }, - workflow: { - activeNavLinkColor: '#AF2996', - secondary: '#9315b0', - gradient: gradients.purpleBlue, - burstShape: shapes.circle1, - }, - library: { - activeNavLinkColor: '#B39AC8', - gradient: gradients.sunset, - burstShape: shapes.triangle3, - }, - other: { - activeNavLinkColor: '#8f6858', - gradient: gradients.brown, - burstShape: shapes.rectangle1, - }, - system: { - activeNavLinkColor: '#526C90', - gradient: gradients.darkBlue, - burstShape: shapes.triangle2, - }, - project: { - activeNavLinkColor: '#F13DA2', - gradient: gradients.pinkOrange, - burstShape: shapes.triangle3, - }, - tool: { - activeNavLinkColor: '#B39AC8', - gradient: gradients.violetPeach, - burstShape: shapes.rectangle1, + burstShape: null, }, home: { activeNavLinkColor: '#00814e', gradient: gradients.green, - burstShape: shapes.triangle2, + burstShape: null, }, org: { activeNavLinkColor: '#6044ef', gradient: gradients.violetGreen, - burstShape: shapes.triangle3, + burstShape: null, }, documentation: { activeNavLinkColor: '#04c2ba', gradient: gradients.tpm, - burstShape: shapes.circle2, - }, - blogPost: { - activeNavLinkColor: '#FFF', - gradient: 'linear-gradient(90deg, #033A45 0%, #033A45 100%)', - burstShape: shapes.circle2, - }, - machineLearning: { - activeNavLinkColor: '#0033DD', - gradient: gradients.royalBlue, - burstShape: shapes.circle1, - }, - partnership: { - activeNavLinkColor: '#000000', - gradient: gradients.grey, - burstShape: shapes.rectangle2, - }, - ml: { - activeNavLinkColor: '#B39AC8', - gradient: gradients.blue, - burstShape: shapes.circle1, - }, - scienceBox: { - activeNavLinkColor: '#69B9FF', // matches sky gradient beginning point - gradient: gradients.sky, - burstShape: shapes.circle1, + burstShape: null, }, }; \ No newline at end of file diff --git a/frontend/packages/core/src/layout/Page/index.ts b/frontend/packages/core/src/layout/Page/index.ts index 889778f7e8..34a8b93753 100644 --- a/frontend/packages/core/src/layout/Page/index.ts +++ b/frontend/packages/core/src/layout/Page/index.ts @@ -1,2 +1,2 @@ export { default } from './Page'; -export { shapes, gradients, theme } from './PageThemeProvider'; \ No newline at end of file +export { gradients, theme } from './PageThemeProvider'; \ No newline at end of file diff --git a/frontend/packages/core/src/layout/SupportButton/SupportButton.js b/frontend/packages/core/src/layout/SupportButton/SupportButton.js deleted file mode 100644 index b8663fcb77..0000000000 --- a/frontend/packages/core/src/layout/SupportButton/SupportButton.js +++ /dev/null @@ -1,114 +0,0 @@ -import React, { Component, Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { compose } from 'recompose'; -import { List, ListItem, ListItemIcon, Popover, Typography, withStyles } from '@material-ui/core'; -import { withPluginContext } from 'core/app/withPluginContext'; -import { EmailIcon, SlackIcon, SupportIcon } from 'shared/icons'; -import { Button, Link } from 'shared/components'; -import { StackOverflow, StackOverflowTag } from 'shared/components/layout'; - -class SupportButton extends Component { - static defaultProps = { - slackChannel: '#backstage', - }; - - static propTypes = { - slackChannel: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]), - }; - - state = { - popoverOpen: false, - anchorEl: null, - }; - - button_clickHandler = event => { - this.setState({ anchorEl: event.currentTarget, popoverOpen: true }); - }; - - popover_closeHandler = () => { - this.setState({ popoverOpen: false }); - }; - - render() { - const { slackChannel, email, children, classes, plugin } = this.props; - const tags = plugin ? plugin.stackoverflowTags : undefined; // plugin is undefined on base routes - const { popoverOpen, anchorEl } = this.state; - const slackChannels = slackChannel ? (Array.isArray(slackChannel) ? slackChannel : [slackChannel]) : null; - const contactEmails = email ? (Array.isArray(email) ? email : [email]) : null; - - return ( - - - - - {React.Children.map(children, (child, index) => ( - - {child} - - ))} - {tags && tags.length > 0 && ( - - - {tags.map((tag, idx) => ( - - ))} - - - )} - {slackChannels && ( - - - - -
- Support - {slackChannels.map((channel, index) => ( - - {channel} - - ))} -
-
- )} - {contactEmails && ( - - - - -
- Contact - {contactEmails.map((email, index) => ( - - {email} - - ))} -
-
- )} -
-
-
- ); - } -} - -const styles = theme => ({ - leftIcon: { - marginRight: theme.spacing(1), - }, - popoverList: { - minWidth: 260, - maxWidth: 320, - }, -}); - -export default compose(withStyles(styles), withPluginContext)(SupportButton); diff --git a/frontend/packages/core/src/layout/SupportButton/SupportButton.test.js b/frontend/packages/core/src/layout/SupportButton/SupportButton.test.js deleted file mode 100644 index d008fc42b6..0000000000 --- a/frontend/packages/core/src/layout/SupportButton/SupportButton.test.js +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; -import { render, fireEvent, waitForElement } from '@testing-library/react'; - -import SupportButton from './SupportButton'; -import { wrapInThemedTestApp, Keyboard } from '../../testUtils'; - -describe('', () => { - it('renders without exploding', () => { - const rendered = render(); - rendered.getByText('Support'); - - fireEvent.click(rendered.baseElement); - }); - - it('should show popover', () => { - const rendered = render( - wrapInThemedTestApp(), - ); - - expect(rendered.queryByText('Contact')).not.toBeInTheDocument(); - fireEvent.click(rendered.getByText('Support')); - expect(rendered.queryByText('Contact')).toBeInTheDocument(); - rendered.getByText('a@b.com'); - rendered.getByText('a-tag'); - }); - - it('should close popover when hitting escape', async () => { - const rendered = render(wrapInThemedTestApp()); - - fireEvent.click(rendered.getByText('Support')); - expect(rendered.queryByText('Contact')).toBeInTheDocument(); - await Keyboard.type(rendered, ''); - await waitForElement(() => !rendered.queryByText('Contact')); - expect(rendered.queryByText('Contact')).not.toBeInTheDocument(); - }); -}); diff --git a/frontend/packages/core/src/testUtils/index.js b/frontend/packages/core/src/testUtils/index.js index 5c5ed40741..ae521780e7 100644 --- a/frontend/packages/core/src/testUtils/index.js +++ b/frontend/packages/core/src/testUtils/index.js @@ -1,46 +1,31 @@ /** - * Helpers for testing redux components and containers + * Helpers for testing components */ +import React from 'react'; import { MuiThemeProvider } from '@material-ui/core'; import { ThemeProvider } from '@material-ui/styles'; -import React from 'react'; -import { act } from 'react-dom/test-utils'; -import { ApolloProvider } from '@apollo/react-hooks'; -import { Provider } from 'react-redux'; + import { MemoryRouter } from 'react-router'; import { Route } from 'react-router-dom'; -import configureStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; -import { V1 } from 'core/app/Themes'; -import defaultAppState from 'core/defaultAppState'; -import store from 'core/store'; -import ErrorBoundary from 'shared/components/ErrorBoundary'; -import { SearchProvider } from 'plugins/searchPage/SearchContext'; - -import { render } from '@testing-library/react'; -import { FirestoreProvider } from 'shared/apis/firestore'; -import MockFirestoreStorage from 'shared/apis/firestore/MockFirestoreStorage'; -import { UserInfoProvider, defaultMockUser } from 'shared/apis/user'; +import { V1 } from '../theme/BackstageTheme'; +import ErrorBoundary from '../layout/ErrorBoundary'; export { default as Keyboard } from './Keyboard'; export { default as mockBreakpoint } from './mockBreakpoint'; -export function wrapInTestApp(Component, initialRouterEntries, user = defaultMockUser) { +export function wrapInTestApp( + Component, + initialRouterEntries, +) { const Wrapper = Component instanceof Function ? Component : () => Component; return ( - - - - - - - - - - - + + + + + ); } @@ -53,227 +38,6 @@ export function wrapInThemedTestApp(component, initialRouterEntries) { return wrapInTestApp(themed, initialRouterEntries); } -export function wrapInSearchTestApp(component) { - const search = {component}; - return wrapInThemedTestApp(search); -} - -export function getTestStore(state) { - const appState = state ? state : defaultAppState; - const mockStore = configureStore([thunk]); - return mockStore(appState); -} - -export function getGoogleProfile() { - return { - getName: () => 'Niklas', - getGivenName: () => 'Niklas', - getEmail: () => 'niklas@spotify.com', - getImageUrl: () => 'http://img.url', - getId: () => '123456', - }; -} - -const DEFAULT_APOLLO_STATE = { - data: { loading: true }, -}; - -export const ApolloMock = ({ children, state = DEFAULT_APOLLO_STATE }) => { - // Hideous mocking of the ApolloClient to use the ApolloProvider - // The test do not have information about the GraphQL queries made by the components - // and hence we cant use MockedProvider and use the one below. - const mockClient = { - query: async () => state, - watchQuery: () => { - return { - refetch: () => {}, - fetchMore: () => {}, - updateQuery: () => {}, - startPolling: () => {}, - stopPolling: () => {}, - subscribeToMore: () => {}, - options: { - fetchPolicy: {}, - }, - getLastResult: () => {}, - currentResult: () => state, - getCurrentResult: () => state, - resetQueryStoreErrors: () => {}, - subscribe: () => {}, - setOptions: () => Promise.resolve({}), - }; - }, - }; - return {children}; -}; - -/** - * Return a builder for a given Component which will permit fluent immutable - * configuration and access to @testing-library/react rendering functions. - * - * A full example will make the utility of this more clear: - * - * const builder = buildComponentInApp(MyComponent) - * .withProps({myProp: 'myValue'}) - * - * // MuiThemeProvider will wrap MyComponent when this is called: - * .withTheme() - * - * // MemoryRouter can be configured with initialEntries: - * .withRouterEntries(['/']) - * - * // ApolloProvider wraps MyComponent with a mock client setup to - * // provide the given data: - * .withApolloData({ - * component: {id: 'my-component', system: 'my-system'} - * }); - * - * // Test 1: Use the defaults from above when mounting - * const { getByText } = builder.render(); - * expect(getByText('Display Text').querySelectorAll('td')).toHaveLength(1); - - * // Test 2: ApolloRouter provides loading=true state instead of valid data - * const { queryByText } = builder.withApolloLoading().render(); - * queryByText('Content').toBeNull(); - */ -export function buildComponentInApp(Component) { - function createBuilder(opt = {}) { - const builder = { - withProps: (props = {}) => - createBuilder({ - ...opt, - props: Object.assign(opt.props || {}, props), - }), - withTheme: (theme = V1) => - createBuilder({ - ...opt, - theme, - }), - withRouterEntries: routerEntries => - createBuilder({ - ...opt, - routerEntries, - }), - withApolloLoading: () => - // Support `data.loading` (errorOrLoading) or just `loading` (Query) - createBuilder({ - ...opt, - apolloState: { data: { loading: true }, loading: true }, - }), - withApolloData: data => - createBuilder({ - ...opt, - apolloState: { data: { loading: false, ...data } }, - }), - withApolloError: error => - createBuilder({ - ...opt, - apolloState: { error: error }, - }), - withUser: user => - createBuilder({ - ...opt, - user, - }), - build: (argProps = {}) => { - const props = { ...(opt.props || {}), ...argProps }; - const component = ; - const routed = wrapInTestApp(component, opt.routerEntries, opt.user); - const themed = opt.theme ? {routed} : routed; - return opt.apolloState ? {themed} : themed; - }, - render: props => { - return render(builder.build(props)); - }, - renderWithEffects: async props => { - return renderWithEffects(builder.build(props)); - }, - }; - return builder; - } - return createBuilder(); -} - -// Components using useEffect to perform an asynchronous action (such as fetch) must be rendered within an async -// act call to properly get the final state, even with mocked responses. This utility method makes the signature a bit -// cleaner, since act doesn't return the result of the evaluated function. -// https://github.com/testing-library/react-testing-library/issues/281 -// https://github.com/facebook/react/pull/14853 -export async function renderWithEffects(nodes) { - let value; - await act(async () => { - value = await render(nodes); - }); - return value; -} - -export function logBoundaryErrors() { - ErrorBoundary.onError = error => console.error('ErrorBoundary received error', error); -} - -// Runs a function with all error logging captured and returned as an array. -// If the callback function is async this one will be too. -export function withLogCollector(logsToCollect, callback) { - if (typeof logsToCollect === 'function') { - callback = logsToCollect; - logsToCollect = ['log', 'warn', 'error']; - } - const logs = { - log: [], - warn: [], - error: [], - }; - - const origLog = console.log; - const origWarn = console.warn; - const origError = console.error; - - if (logsToCollect.includes('log')) { - console.log = message => { - logs.log.push(message); - }; - } - if (logsToCollect.includes('warn')) { - console.warn = message => { - logs.warn.push(message); - }; - } - if (logsToCollect.includes('error')) { - console.error = message => { - logs.error.push(message); - }; - } - - const restore = () => { - console.log = origLog; - console.warn = origWarn; - console.error = origError; - }; - - try { - const ret = callback(); - - if (!ret || !ret.then) { - restore(); - return logs; - } - - return ret.then( - () => { - restore(); - return logs; - }, - error => { - restore(); - throw error; - }, - ); - } catch (error) { - restore(); - throw error; - } -} - export const wrapInTheme = (component, theme = V1) => ( {component} diff --git a/frontend/packages/core/src/theme/BackstageTheme.js b/frontend/packages/core/src/theme/BackstageTheme.js index d69a86ac37..5185aa8cd7 100644 --- a/frontend/packages/core/src/theme/BackstageTheme.js +++ b/frontend/packages/core/src/theme/BackstageTheme.js @@ -355,4 +355,4 @@ const styles = theme => ({ }); export const withGlobalStyles = withStyles(styles); -export default BackstageTheme; \ No newline at end of file +export default BackstageTheme; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 64df4b69f2..cd1327d08d 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -3596,7 +3596,7 @@ arrify@^1.0.1: resolved "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= -asap@^2.0.0, asap@~2.0.6: +asap@^2.0.0, asap@~2.0.3, asap@~2.0.6: version "2.0.6" resolved "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= @@ -4452,6 +4452,11 @@ chalk@^1.0.0, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" +change-emitter@^0.1.2: + version "0.1.6" + resolved "https://registry.npmjs.org/change-emitter/-/change-emitter-0.1.6.tgz#e8b2fe3d7f1ab7d69a32199aff91ea6931409515" + integrity sha1-6LL+PX8at9aaMhma/5HqaTFAlRU= + chardet@^0.7.0: version "0.7.0" resolved "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" @@ -5123,6 +5128,11 @@ core-js-pure@^3.0.0: resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.6.4.tgz#4bf1ba866e25814f149d4e9aaa08c36173506e3a" integrity sha512-epIhRLkXdgv32xIUFaaAry2wdxZYBi6bgM7cB136dzzXXa+dFyRLTZeLUJxnd8ShrmyVXBub63n2NHo2JAt8Cw== +core-js@^1.0.0: + version "1.2.7" + resolved "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" + integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY= + core-js@^2.4.0, core-js@^2.5.0: version "2.6.11" resolved "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c" @@ -6776,6 +6786,19 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" +fbjs@^0.8.1: + version "0.8.17" + resolved "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd" + integrity sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90= + dependencies: + core-js "^1.0.0" + isomorphic-fetch "^2.1.1" + loose-envify "^1.0.0" + object-assign "^4.1.0" + promise "^7.1.1" + setimmediate "^1.0.5" + ua-parser-js "^0.7.18" + figgy-pudding@^3.4.1, figgy-pudding@^3.5.1: version "3.5.1" resolved "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790" @@ -7685,6 +7708,11 @@ hmac-drbg@^1.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" +hoist-non-react-statics@^2.3.1: + version "2.5.5" + 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: version "3.3.2" resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" @@ -8572,7 +8600,7 @@ is-ssh@^1.3.0: dependencies: protocols "^1.1.0" -is-stream@^1.0.0, is-stream@^1.1.0: +is-stream@^1.0.0, is-stream@^1.0.1, is-stream@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= @@ -8665,6 +8693,14 @@ isobject@^4.0.0: resolved "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz#3f1c9155e73b192022a80819bacd0343711697b0" integrity sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA== +isomorphic-fetch@^2.1.1: + version "2.2.1" + resolved "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" + integrity sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk= + dependencies: + node-fetch "^1.0.1" + whatwg-fetch ">=0.10.0" + isstream@~0.1.2: version "0.1.2" resolved "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -11053,6 +11089,14 @@ node-fetch-npm@^2.0.2: json-parse-better-errors "^1.0.0" safe-buffer "^5.1.1" +node-fetch@^1.0.1: + version "1.7.3" + resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" + integrity sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ== + dependencies: + encoding "^0.1.11" + is-stream "^1.0.1" + node-fetch@^2.3.0, node-fetch@^2.5.0: version "2.6.0" resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" @@ -13015,6 +13059,13 @@ promise-retry@^1.1.1: err-code "^1.0.0" retry "^0.10.0" +promise@^7.1.1: + version "7.3.1" + resolved "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" + integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg== + dependencies: + asap "~2.0.3" + promise@^8.0.3: version "8.0.3" resolved "https://registry.npmjs.org/promise/-/promise-8.0.3.tgz#f592e099c6cddc000d538ee7283bb190452b0bf6" @@ -13329,6 +13380,11 @@ react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.0, react-i resolved "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c" integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q== +react-lifecycles-compat@^3.0.2: + version "3.0.4" + resolved "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" + integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== + react-router-dom@^5.1.2: version "5.1.2" resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.1.2.tgz#06701b834352f44d37fbb6311f870f84c76b9c18" @@ -13583,6 +13639,18 @@ rechoir@^0.6.2: dependencies: resolve "^1.1.6" +recompose@0.30.0: + version "0.30.0" + resolved "https://registry.npmjs.org/recompose/-/recompose-0.30.0.tgz#82773641b3927e8c7d24a0d87d65aeeba18aabd0" + integrity sha512-ZTrzzUDa9AqUIhRk4KmVFihH0rapdCSMFXjhHbNrjAWxBuUD/guYlyysMnuHjlZC/KRiOKRtB4jf96yYSkKE8w== + dependencies: + "@babel/runtime" "^7.0.0" + change-emitter "^0.1.2" + fbjs "^0.8.1" + hoist-non-react-statics "^2.3.1" + react-lifecycles-compat "^3.0.2" + symbol-observable "^1.0.4" + recursive-readdir@2.2.2: version "2.2.2" resolved "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f" @@ -14287,7 +14355,7 @@ set-value@^2.0.0, set-value@^2.0.1: is-plain-object "^2.0.3" split-string "^3.0.1" -setimmediate@^1.0.4: +setimmediate@^1.0.4, setimmediate@^1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= @@ -15093,7 +15161,7 @@ svgo@^1.0.0, svgo@^1.2.2: unquote "~1.1.1" util.promisify "~1.0.0" -symbol-observable@^1.1.0: +symbol-observable@^1.0.4, symbol-observable@^1.1.0: version "1.2.0" resolved "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== @@ -15548,6 +15616,11 @@ typescript@^3.7.4, typescript@^3.7.5: resolved "https://registry.npmjs.org/typescript/-/typescript-3.7.5.tgz#0692e21f65fd4108b9330238aac11dd2e177a1ae" integrity sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw== +ua-parser-js@^0.7.18: + version "0.7.21" + resolved "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.21.tgz#853cf9ce93f642f67174273cc34565ae6f308777" + integrity sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ== + uglify-js@3.4.x: version "3.4.10" resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.10.tgz#9ad9563d8eb3acdfb8d38597d2af1d815f6a755f" @@ -16057,7 +16130,7 @@ whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3, whatwg-encoding@^1.0.5: dependencies: iconv-lite "0.4.24" -whatwg-fetch@^3.0.0: +whatwg-fetch@>=0.10.0, whatwg-fetch@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb" integrity sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q== From e5472c46fc64d323a15b50bd4c1468a7ed0a2c7e Mon Sep 17 00:00:00 2001 From: Raghunandan Balachandran Date: Wed, 5 Feb 2020 13:53:20 +0100 Subject: [PATCH 05/12] remove tinycolor. remove muithemeprovider --- frontend/packages/app/src/App.tsx | 40 +++++++------------ frontend/packages/core/package.json | 1 - frontend/packages/core/src/testUtils/index.js | 15 ++----- .../packages/core/src/theme/BackstageTheme.js | 5 +-- 4 files changed, 19 insertions(+), 42 deletions(-) diff --git a/frontend/packages/app/src/App.tsx b/frontend/packages/app/src/App.tsx index 0d82f79aca..cd85cb85eb 100644 --- a/frontend/packages/app/src/App.tsx +++ b/frontend/packages/app/src/App.tsx @@ -12,15 +12,9 @@ import { Link as RouterLink, } from 'react-router-dom'; import { BackstageTheme, withGlobalStyles, theme } from '@backstage/core'; -import { CssBaseline, MuiThemeProvider, makeStyles } from '@material-ui/core'; -import { ThemeProvider } from '@material-ui/styles'; -import { StylesProvider, createGenerateClassName } from '@material-ui/styles'; +import { CssBaseline, ThemeProvider, makeStyles } from '@material-ui/core'; import HomePageTimer from './components/HomepageTimer'; -const generateClassName = createGenerateClassName({ - productionPrefix: 'stajl', -}); - const useStyles = makeStyles(theme => ({ root: { display: 'grid', @@ -45,24 +39,20 @@ const useStyles = makeStyles(theme => ({ const App: FC<{}> = () => { return ( - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + ); }; diff --git a/frontend/packages/core/package.json b/frontend/packages/core/package.json index 7e6782bbb2..9c1016141c 100644 --- a/frontend/packages/core/package.json +++ b/frontend/packages/core/package.json @@ -15,7 +15,6 @@ "react": "^16.12.0", "react-dom": "^16.12.0", "react-helmet":"5.2.1", - "tinycolor2": "1.4.1", "react-addons-text-content": "0.0.4", "recompose": "0.30.0" }, diff --git a/frontend/packages/core/src/testUtils/index.js b/frontend/packages/core/src/testUtils/index.js index ae521780e7..99e94c624d 100644 --- a/frontend/packages/core/src/testUtils/index.js +++ b/frontend/packages/core/src/testUtils/index.js @@ -14,10 +14,7 @@ import ErrorBoundary from '../layout/ErrorBoundary'; export { default as Keyboard } from './Keyboard'; export { default as mockBreakpoint } from './mockBreakpoint'; -export function wrapInTestApp( - Component, - initialRouterEntries, -) { +export function wrapInTestApp(Component, initialRouterEntries) { const Wrapper = Component instanceof Function ? Component : () => Component; return ( @@ -30,16 +27,10 @@ export function wrapInTestApp( } export function wrapInThemedTestApp(component, initialRouterEntries) { - const themed = ( - - {component} - - ); + const themed = {component}; return wrapInTestApp(themed, initialRouterEntries); } export const wrapInTheme = (component, theme = V1) => ( - - {component} - + {component} ); diff --git a/frontend/packages/core/src/theme/BackstageTheme.js b/frontend/packages/core/src/theme/BackstageTheme.js index 5185aa8cd7..fbf521abe0 100644 --- a/frontend/packages/core/src/theme/BackstageTheme.js +++ b/frontend/packages/core/src/theme/BackstageTheme.js @@ -1,14 +1,11 @@ import { createMuiTheme, withStyles } from '@material-ui/core'; import { darken, lighten } from '@material-ui/core/styles/colorManipulator'; import { blue, yellow } from '@material-ui/core/colors'; -import tinycolor from 'tinycolor2'; export const COLORS = { PAGE_BACKGROUND: '#F8F8F8', DEFAULT_PAGE_THEME_COLOR: '#7C3699', - DEFAULT_PAGE_THEME_LIGHT_COLOR: tinycolor('#7C3699') - .lighten(50) - .toString(), + DEFAULT_PAGE_THEME_LIGHT_COLOR: '#ECDBF2', ERROR_BACKGROUND_COLOR: '#FFEBEE', ERROR_TEXT_COLOR: '#CA001B', INFO_TEXT_COLOR: '#004e8a', From 0d4a12e7f564b9bf1aa5e7d13d995a6cbb4286c0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 5 Feb 2020 13:46:00 +0100 Subject: [PATCH 06/12] frontend/core: add some kinda app api --- frontend/packages/app/src/App.tsx | 79 +++++++---- .../app/src/entities/MockEntityCard.tsx | 9 ++ .../app/src/entities/MockEntityPage.tsx | 9 ++ frontend/packages/app/src/entities/index.ts | 32 +++++ frontend/packages/core/package.json | 2 + .../packages/core/src/appApi/AppBuilder.tsx | 107 +++++++++++++++ .../packages/core/src/appApi/AppContext.tsx | 20 +++ .../core/src/appApi/EntityContext.tsx | 42 ++++++ .../packages/core/src/appApi/EntityKind.ts | 5 + .../packages/core/src/appApi/EntityLink.tsx | 43 ++++++ .../core/src/appApi/EntityViewPageBuilder.tsx | 124 ++++++++++++++++++ .../core/src/appApi/OverviewPageBuilder.tsx | 50 +++++++ frontend/packages/core/src/appApi/api.ts | 21 +++ frontend/packages/core/src/appApi/index.ts | 5 + frontend/packages/core/src/appApi/types.ts | 36 +++++ frontend/packages/core/src/index.ts | 1 + 16 files changed, 556 insertions(+), 29 deletions(-) create mode 100644 frontend/packages/app/src/entities/MockEntityCard.tsx create mode 100644 frontend/packages/app/src/entities/MockEntityPage.tsx create mode 100644 frontend/packages/app/src/entities/index.ts create mode 100644 frontend/packages/core/src/appApi/AppBuilder.tsx create mode 100644 frontend/packages/core/src/appApi/AppContext.tsx create mode 100644 frontend/packages/core/src/appApi/EntityContext.tsx create mode 100644 frontend/packages/core/src/appApi/EntityKind.ts create mode 100644 frontend/packages/core/src/appApi/EntityLink.tsx create mode 100644 frontend/packages/core/src/appApi/EntityViewPageBuilder.tsx create mode 100644 frontend/packages/core/src/appApi/OverviewPageBuilder.tsx create mode 100644 frontend/packages/core/src/appApi/api.ts create mode 100644 frontend/packages/core/src/appApi/index.ts create mode 100644 frontend/packages/core/src/appApi/types.ts diff --git a/frontend/packages/app/src/App.tsx b/frontend/packages/app/src/App.tsx index cd85cb85eb..0f22c76671 100644 --- a/frontend/packages/app/src/App.tsx +++ b/frontend/packages/app/src/App.tsx @@ -1,19 +1,28 @@ -import React, { FC } from 'react'; +import { + BackstageTheme, + createApp, + EntityLink, + Header, + InfoCard, + Page, + theme, + withGlobalStyles, +} from '@backstage/core'; import helloWorld, { MyComponent } from '@backstage/plugin-hello-world'; -import Typography from '@material-ui/core/Typography'; -import SideBar from './components/SideBar'; //import PageHeader from './components/PageHeader'; -import { Header, Page, InfoCard } from '@backstage/core'; import { LoginComponent } from '@backstage/plugin-login'; +import { CssBaseline, makeStyles, ThemeProvider } from '@material-ui/core'; +import Typography from '@material-ui/core/Typography'; +import React, { FC } from 'react'; import { BrowserRouter as Router, - Switch, - Route, Link as RouterLink, + Route, + Switch, } from 'react-router-dom'; -import { BackstageTheme, withGlobalStyles, theme } from '@backstage/core'; -import { CssBaseline, ThemeProvider, makeStyles } from '@material-ui/core'; import HomePageTimer from './components/HomepageTimer'; +import SideBar from './components/SideBar'; +import entities from './entities'; const useStyles = makeStyles(theme => ({ root: { @@ -36,27 +45,6 @@ const useStyles = makeStyles(theme => ({ }, })); -const App: FC<{}> = () => { - return ( - - - - - - - - - - - - - - - - - ); -}; - const Home: FC<{}> = () => { return ( @@ -67,6 +55,12 @@ const Home: FC<{}> = () => {
Go to Login + + Backstage Backend + + + Backstage LB CI/CD +
); @@ -103,4 +97,31 @@ const AppShell: FC<{}> = ({ children }) => { const AppContent = withGlobalStyles(AppShell); +const app = createApp(); + +app.registerEntityKind(...entities); +app.setHomePage(Home); +const AppComponent = app.build(); + +const App: FC<{}> = () => { + return ( + + + + + + + + + + + + + + + + + ); +}; + export default App; diff --git a/frontend/packages/app/src/entities/MockEntityCard.tsx b/frontend/packages/app/src/entities/MockEntityCard.tsx new file mode 100644 index 0000000000..97a3f4073d --- /dev/null +++ b/frontend/packages/app/src/entities/MockEntityCard.tsx @@ -0,0 +1,9 @@ +import React, { FC } from 'react'; +import { useEntityUri } from '@backstage/core'; + +const MockEntityPage: FC<{}> = () => { + const uri = useEntityUri(); + return Mock card for {uri}, replace with some userful plugin; +}; + +export default MockEntityPage; diff --git a/frontend/packages/app/src/entities/MockEntityPage.tsx b/frontend/packages/app/src/entities/MockEntityPage.tsx new file mode 100644 index 0000000000..091906f31a --- /dev/null +++ b/frontend/packages/app/src/entities/MockEntityPage.tsx @@ -0,0 +1,9 @@ +import React, { FC } from 'react'; +import { useEntityUri } from '@backstage/core'; + +const MockEntityPage: FC<{}> = () => { + const uri = useEntityUri(); + return Mock page for {uri}, replace with some userful plugin; +}; + +export default MockEntityPage; diff --git a/frontend/packages/app/src/entities/index.ts b/frontend/packages/app/src/entities/index.ts new file mode 100644 index 0000000000..47c55a3c57 --- /dev/null +++ b/frontend/packages/app/src/entities/index.ts @@ -0,0 +1,32 @@ +import { + createEntityKind, + createOverviewPage, + createEntityView, +} from '@backstage/core'; +import ComputerIcon from '@material-ui/icons/Computer'; +import MockEntityPage from './MockEntityPage'; +import MockEntityCard from './MockEntityCard'; + +/* SERVICE */ +const serviceOverviewPage = createOverviewPage() + .addComponent(MockEntityCard) + .addComponent(MockEntityCard); + +const serviceView = createEntityView() + .addPage('Overview', 'overview', serviceOverviewPage) + .addComponent('CI/CD', 'ci-cd', MockEntityPage); + +const serviceEntity = createEntityKind({ + kind: 'service', + title: 'Service', + color: { + primary: '#f00', + secondary: '#ba5', + }, + icon: ComputerIcon, + pages: { + view: serviceView, + }, +}); + +export default [serviceEntity]; diff --git a/frontend/packages/core/package.json b/frontend/packages/core/package.json index 9c1016141c..b2155fd61a 100644 --- a/frontend/packages/core/package.json +++ b/frontend/packages/core/package.json @@ -12,10 +12,12 @@ "@types/node": "^12.0.0", "@types/react": "^16.9.0", "@types/react-dom": "^16.9.0", + "@types/react-router-dom": "^5.1.3", "react": "^16.12.0", "react-dom": "^16.12.0", "react-helmet":"5.2.1", "react-addons-text-content": "0.0.4", + "react-router-dom": "^5.1.2", "recompose": "0.30.0" }, "scripts": { diff --git a/frontend/packages/core/src/appApi/AppBuilder.tsx b/frontend/packages/core/src/appApi/AppBuilder.tsx new file mode 100644 index 0000000000..f9045bbbfd --- /dev/null +++ b/frontend/packages/core/src/appApi/AppBuilder.tsx @@ -0,0 +1,107 @@ +import React, { ComponentType, FC } from 'react'; +import { AppContextProvider } from './AppContext'; +import { App, EntityConfig, AppComponentBuilder } from './types'; +import { Route, Switch, useParams } from 'react-router-dom'; +import EntityKind from './EntityKind'; +import { EntityContextProvider } from './EntityContext'; + +const DefaultHomePage: FC<{}> = () => { + return Hello! I am default home page; +}; + +class AppImpl implements App { + constructor(private readonly entities: Map) {} + + getEntityConfig(kind: string): EntityConfig { + const entity = this.entities.get(kind); + if (!entity) { + throw new Error('EntityKind not found'); + } + return entity.config; + } +} + +function builtComponent( + app: App, + component: ComponentType | AppComponentBuilder, +) { + if (component instanceof AppComponentBuilder) { + return component.build(app); + } + return component; +} + +export default class AppBuilder { + private readonly entities = new Map(); + private homePage: ComponentType = DefaultHomePage; + + registerEntityKind(...entity: EntityKind[]) { + for (const e of entity) { + const { kind } = e.config; + if (this.entities.has(e.config.kind)) { + throw new Error(`EntityKind '${kind}' is already registered`); + } + this.entities.set(e.config.kind, e); + } + } + + setHomePage(page: ComponentType<{}>) { + this.homePage = page; + } + + build(): ComponentType<{}> { + const app = new AppImpl(this.entities); + + const entityRoutes = []; + + for (const { config } of this.entities.values()) { + const { kind, pages } = config; + const basePath = `/entity/${kind}`; + + if (pages.list) { + const ListComponent = builtComponent(app, pages.list); + + const Component: FC<{}> = () => ( + + + + ); + + const path = basePath; + entityRoutes.push( + , + ); + } + + if (pages.view) { + const ViewComponent = builtComponent(app, pages.view); + + const Component: FC<{}> = () => { + const { entityId } = useParams<{ entityId: string }>(); + return ( + + + + ); + }; + + const path = `${basePath}/:entityId`; + entityRoutes.push( + , + ); + } + } + + const routes = [...entityRoutes]; + + return () => ( + + + {routes} + + 404 Not Found} /> + + + ); + } +} diff --git a/frontend/packages/core/src/appApi/AppContext.tsx b/frontend/packages/core/src/appApi/AppContext.tsx new file mode 100644 index 0000000000..e8acb424cd --- /dev/null +++ b/frontend/packages/core/src/appApi/AppContext.tsx @@ -0,0 +1,20 @@ +import React, { createContext, useContext, FC } from 'react'; +import { App } from './types'; + +const Context = createContext(undefined); + +type Props = { + app: App; +}; + +export const AppContextProvider: FC = ({ app, children }) => ( + +); + +export const useApp = (): App => { + const app = useContext(Context); + if (!app) { + throw new Error('No app context available'); + } + return app; +}; diff --git a/frontend/packages/core/src/appApi/EntityContext.tsx b/frontend/packages/core/src/appApi/EntityContext.tsx new file mode 100644 index 0000000000..5efe917f4d --- /dev/null +++ b/frontend/packages/core/src/appApi/EntityContext.tsx @@ -0,0 +1,42 @@ +import React, { createContext, useContext, FC } from 'react'; +import { EntityConfig } from './types'; + +type Value = { + config: EntityConfig; + id?: string; +}; + +const Context = createContext(undefined); + +type Props = { + config: EntityConfig; + id?: string; +}; + +export const EntityContextProvider: FC = ({ config, id, children }) => ( + +); + +export const useEntity = (): { kind: string; id: string } => { + const value = useContext(Context); + if (!value) { + throw new Error('No entity context available'); + } + if (!value.id) { + throw new Error('Entity context does not contain entity id'); + } + return { kind: value.config.kind, id: value.id }; +}; + +export const useEntityConfig = (): EntityConfig => { + const value = useContext(Context); + if (!value) { + throw new Error('No entity context available'); + } + return value.config; +}; + +export const useEntityUri = (): string => { + const { kind, id } = useEntity(); + return `entity:${kind}:${id}`; +}; diff --git a/frontend/packages/core/src/appApi/EntityKind.ts b/frontend/packages/core/src/appApi/EntityKind.ts new file mode 100644 index 0000000000..058fdcef6d --- /dev/null +++ b/frontend/packages/core/src/appApi/EntityKind.ts @@ -0,0 +1,5 @@ +import { EntityConfig } from './types'; + +export default class EntityKind { + constructor(readonly config: EntityConfig) {} +} diff --git a/frontend/packages/core/src/appApi/EntityLink.tsx b/frontend/packages/core/src/appApi/EntityLink.tsx new file mode 100644 index 0000000000..c26972c467 --- /dev/null +++ b/frontend/packages/core/src/appApi/EntityLink.tsx @@ -0,0 +1,43 @@ +import React, { FC } from 'react'; +import { Link } from 'react-router-dom'; + +type Props = { + subPath?: string; +} & ( + | { + kind: string; + id?: string; + } + | { + uri: string; + } +); + +function buildPath(kind: string, id?: string, subPath?: string) { + if (id) { + if (subPath) { + return `/entity/${kind}/${id}/${subPath}`; + } + return `/entity/${kind}/${id}`; + } + return `/entity/${kind}`; +} + +const EntityLink: FC = ({ subPath, children, ...props }) => { + if ('kind' in props) { + const { kind, id } = props; + return {children}; + } else { + const match = props.uri.match(/entity:([^:]+)(:[^:]+)?/); + if (!match) { + throw new TypeError(`Invalid entity uri: '${props.uri}'`); + } + + const [, kind, maybeId] = match; + const id = maybeId ? maybeId.slice(1) : undefined; + + return {children}; + } +}; + +export default EntityLink; diff --git a/frontend/packages/core/src/appApi/EntityViewPageBuilder.tsx b/frontend/packages/core/src/appApi/EntityViewPageBuilder.tsx new file mode 100644 index 0000000000..b323cd9504 --- /dev/null +++ b/frontend/packages/core/src/appApi/EntityViewPageBuilder.tsx @@ -0,0 +1,124 @@ +import React, { ComponentType, FC } from 'react'; +import { AppComponentBuilder, App } from './types'; +import { useEntity, useEntityUri, useEntityConfig } from './EntityContext'; +import { Route, Redirect, Switch } from 'react-router-dom'; +import List from '@material-ui/core/List'; +import ListItem from '@material-ui/core/ListItem'; +import EntityLink from './EntityLink'; + +const EntityLayout: FC<{}> = ({ children }) => { + const config = useEntityConfig(); + return ( +
{children}
+ ); +}; + +const EntitySidebar: FC<{}> = ({ children }) => { + return {children}; +}; + +const EntitySidebarItem: FC<{ title: string; path: string }> = ({ + title, + path, +}) => { + const entityUri = useEntityUri(); + + return ( + + + {title} + + + ); +}; + +type EntityViewPage = { + title: string; + path: string; + component: ComponentType; +}; + +type Props = { + pages: EntityViewPage[]; +}; + +const EntityViewComponent: FC = ({ pages }) => { + const { kind, id } = useEntity(); + const basePath = `/entity/${kind}/${id}`; + + return ( + + + {pages.map(({ title, path }) => ( + + ))} + + + {pages.map(({ path, component }) => ( + + ))} + + + + ); +}; + +type EntityViewRegistration = + | { + type: 'page'; + title: string; + path: string; + page: AppComponentBuilder; + } + | { + type: 'component'; + title: string; + path: string; + component: ComponentType; + }; + +export default class EntityViewBuilder extends AppComponentBuilder { + private readonly registrations = new Array(); + + addPage( + title: string, + path: string, + page: AppComponentBuilder, + ): EntityViewBuilder { + this.registrations.push({ type: 'page', title, path, page }); + return this; + } + + addComponent( + title: string, + path: string, + component: ComponentType, + ): EntityViewBuilder { + this.registrations.push({ type: 'component', title, path, component }); + return this; + } + + build(app: App): ComponentType { + const pages = this.registrations.map(registration => { + switch (registration.type) { + case 'page': { + const { title, path, page } = registration; + return { title, path, component: page.build(app) }; + } + case 'component': { + const { title, path, component } = registration; + return { title, path, component }; + } + default: + throw new Error(`Unknown EntityViewBuilder registration`); + } + }); + + return () => ; + } +} diff --git a/frontend/packages/core/src/appApi/OverviewPageBuilder.tsx b/frontend/packages/core/src/appApi/OverviewPageBuilder.tsx new file mode 100644 index 0000000000..67a0138436 --- /dev/null +++ b/frontend/packages/core/src/appApi/OverviewPageBuilder.tsx @@ -0,0 +1,50 @@ +import React, { ComponentType, FC } from 'react'; +import { App, AppComponentBuilder } from './types'; + +type Props = { + app: App; + cards: ComponentType[]; +}; + +const OverviewPageComponent: FC = ({ cards }) => { + return ( +
+ {cards.map(CardComponent => ( + + ))} +
+ ); +}; + +type OverviewPageRegistration = { + type: 'component'; + component: ComponentType; +}; + +export default class OverviewPageBuilder extends AppComponentBuilder { + private readonly registrations = new Array(); + private output?: ComponentType; + + addComponent(component: ComponentType): OverviewPageBuilder { + this.registrations.push({ type: 'component', component }); + return this; + } + + build(app: App): ComponentType { + if (this.output) { + return this.output; + } + + const cards = this.registrations.map(reg => { + switch (reg.type) { + case 'component': + return reg.component; + default: + throw new Error(`Unknown OverviewPageBuilder registration`); + } + }); + + this.output = () => ; + return this.output; + } +} diff --git a/frontend/packages/core/src/appApi/api.ts b/frontend/packages/core/src/appApi/api.ts new file mode 100644 index 0000000000..4d657ee606 --- /dev/null +++ b/frontend/packages/core/src/appApi/api.ts @@ -0,0 +1,21 @@ +import AppBuilder from './AppBuilder'; +import { EntityConfig } from './types'; +import EntityKind from './EntityKind'; +import OverviewPageBuilder from './OverviewPageBuilder'; +import EntityViewBuilder from './EntityViewPageBuilder'; + +export function createApp() { + return new AppBuilder(); +} + +export function createEntityKind(config: EntityConfig) { + return new EntityKind(config); +} + +export function createOverviewPage() { + return new OverviewPageBuilder(); +} + +export function createEntityView() { + return new EntityViewBuilder(); +} diff --git a/frontend/packages/core/src/appApi/index.ts b/frontend/packages/core/src/appApi/index.ts new file mode 100644 index 0000000000..d198cb66e4 --- /dev/null +++ b/frontend/packages/core/src/appApi/index.ts @@ -0,0 +1,5 @@ +export * from './types'; +export * from './api'; +export { useApp } from './AppContext'; +export { useEntity, useEntityConfig, useEntityUri } from './EntityContext'; +export { default as EntityLink } from './EntityLink'; diff --git a/frontend/packages/core/src/appApi/types.ts b/frontend/packages/core/src/appApi/types.ts new file mode 100644 index 0000000000..f37848aca0 --- /dev/null +++ b/frontend/packages/core/src/appApi/types.ts @@ -0,0 +1,36 @@ +import { ComponentType } from 'react'; + +export type EntityConfig = { + kind: string; + title: string; + icon: React.ComponentType<{ fontSize: number }>; + color: { + primary: string; + secondary: string; + }; + pages: { + list?: ComponentType<{}> | AppComponentBuilder; + view?: ComponentType<{}> | AppComponentBuilder; + }; +}; + +export type App = { + getEntityConfig(kind: string): EntityConfig; +}; + +export class AppComponentBuilder { + build(_app: App): ComponentType { + throw new Error('Must override build() in AppComponentBuilder'); + } +} + +export type User = { + id: string; + email: string; +}; + +export type UserApi = { + isLoggedIn(): Promise; + + getUser(): Promise; +}; diff --git a/frontend/packages/core/src/index.ts b/frontend/packages/core/src/index.ts index 353b29ff72..45110734fb 100644 --- a/frontend/packages/core/src/index.ts +++ b/frontend/packages/core/src/index.ts @@ -1,3 +1,4 @@ +export * from './appApi'; export * from './types'; export { default as createPlugin } from './createPlugin'; export { default as Page } from '../src/layout/Page'; From b21af5d883f941034ba502af008dee56be886245 Mon Sep 17 00:00:00 2001 From: Patrick Balestra Date: Wed, 5 Feb 2020 15:03:17 +0100 Subject: [PATCH 07/12] Add iOS Swift template --- .../ios-swift-app-template/cookiecutter.json | 5 + .../ios-swift-app-template/custom-fields.json | 41 ++ .../{{cookiecutter.component_id}}/README.md | 7 + .../{{cookiecutter.component_id}}/build.sh | 21 + .../project.pbxproj | 470 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../UserInterfaceState.xcuserstate | Bin 0 -> 12954 bytes .../xcschemes/xcschememanagement.plist | 14 + .../AppDelegate.swift | 36 ++ .../AppIcon.appiconset/Contents.json | 98 ++++ .../Assets.xcassets/Contents.json | 6 + .../Base.lproj/LaunchScreen.storyboard | 25 + .../Base.lproj/Main.storyboard | 24 + .../{{cookiecutter.component_id}}/Info.plist | 64 +++ .../SceneDelegate.swift | 52 ++ .../ViewController.swift | 19 + .../Info.plist | 22 + .../{{cookiecutter.component_id}}Tests.swift | 33 ++ 19 files changed, 952 insertions(+) create mode 100644 backend/scaffolder/templates/ios-swift-app-template/cookiecutter.json create mode 100644 backend/scaffolder/templates/ios-swift-app-template/custom-fields.json create mode 100644 backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/README.md create mode 100755 backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/build.sh create mode 100644 backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}.xcodeproj/project.pbxproj create mode 100644 backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}.xcodeproj/project.xcworkspace/xcuserdata/patrickb.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}.xcodeproj/xcuserdata/patrickb.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}/AppDelegate.swift create mode 100644 backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}/Assets.xcassets/Contents.json create mode 100644 backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}/Base.lproj/LaunchScreen.storyboard create mode 100644 backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}/Base.lproj/Main.storyboard create mode 100644 backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}/Info.plist create mode 100644 backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}/SceneDelegate.swift create mode 100644 backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}/ViewController.swift create mode 100644 backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}Tests/Info.plist create mode 100644 backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}Tests/{{cookiecutter.component_id}}Tests.swift diff --git a/backend/scaffolder/templates/ios-swift-app-template/cookiecutter.json b/backend/scaffolder/templates/ios-swift-app-template/cookiecutter.json new file mode 100644 index 0000000000..d1813da0fa --- /dev/null +++ b/backend/scaffolder/templates/ios-swift-app-template/cookiecutter.json @@ -0,0 +1,5 @@ +{ + "component_id": "", + "owner": "", + "description": "We promise to update this description /{{cookiecutter.owner}}" +} diff --git a/backend/scaffolder/templates/ios-swift-app-template/custom-fields.json b/backend/scaffolder/templates/ios-swift-app-template/custom-fields.json new file mode 100644 index 0000000000..bff05e95c7 --- /dev/null +++ b/backend/scaffolder/templates/ios-swift-app-template/custom-fields.json @@ -0,0 +1,41 @@ +{ + "schema": [ + { + "id": "component_id", + "title": "Project name", + "type": "text", + "description": "Project name used to generate Xcode files and class names. CamelCase e.g. 'MyProject'", + "validators": [ + { + "type": "required" + }, + { + "type": "regex", + "match": "^[A-Z][A-Za-z]+$", + "message": "Project name must be CamelCase" + } + ] + }, + { + "id": "owner", + "title": "owner", + "type": "text", + "description": "The owner name used to identify the owner of this component", + "validators": [ + { + "type": "required" + }, + { + "type": "string-range", + "min": 4, + "max": 33 + }, + { + "type": "regex", + "match": "^[a-z][a-z0-9]+$", + "message": "Owner names must consist only of lowercase letters" + } + ] + } + ] +} diff --git a/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/README.md b/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/README.md new file mode 100644 index 0000000000..20dd1dad67 --- /dev/null +++ b/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/README.md @@ -0,0 +1,7 @@ +# iOS App + +This project provides a template project to generate an iOS App written in Swift. + +## Building and Testing + +You can directly open the `xcodeproj` and build from Xcode or run `build.sh` from the command line. \ No newline at end of file diff --git a/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/build.sh b/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/build.sh new file mode 100755 index 0000000000..b40a44923e --- /dev/null +++ b/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/build.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +PLATFORM="platform=iOS Simulator,OS=13.0,name=iPhone 11" +SDK="iphonesimulator" + +set -e +function trap_handler() { + echo -e "\n\nFailed to build project" + exit 255 +} +trap trap_handler INT TERM EXIT + +echo "Building {{cookiecutter.component_id}}." +xcodebuild \ + -project {{cookiecutter.component_id}}.xcodeproj \ + -scheme {{cookiecutter.component_id}} \ + -sdk "$SDK" \ + -destination "$PLATFORM" \ + build +trap - EXIT +exit 0 diff --git a/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}.xcodeproj/project.pbxproj b/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..b48e86d615 --- /dev/null +++ b/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}.xcodeproj/project.pbxproj @@ -0,0 +1,470 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + C5B1973423EAFA6D007C9A28 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5B1973323EAFA6D007C9A28 /* AppDelegate.swift */; }; + C5B1973623EAFA6D007C9A28 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5B1973523EAFA6D007C9A28 /* SceneDelegate.swift */; }; + C5B1973823EAFA6D007C9A28 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5B1973723EAFA6D007C9A28 /* ViewController.swift */; }; + C5B1973B23EAFA6D007C9A28 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C5B1973923EAFA6D007C9A28 /* Main.storyboard */; }; + C5B1973D23EAFA6E007C9A28 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C5B1973C23EAFA6E007C9A28 /* Assets.xcassets */; }; + C5B1974023EAFA6E007C9A28 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C5B1973E23EAFA6E007C9A28 /* LaunchScreen.storyboard */; }; + C5B1974B23EAFA6E007C9A28 /* {{cookiecutter.component_id}}Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5B1974A23EAFA6E007C9A28 /* {{cookiecutter.component_id}}Tests.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + C5B1974723EAFA6E007C9A28 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C5B1972823EAFA6D007C9A28 /* Project object */; + proxyType = 1; + remoteGlobalIDString = C5B1972F23EAFA6D007C9A28; + remoteInfo = {{cookiecutter.component_id}}; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + C5B1973023EAFA6D007C9A28 /* {{cookiecutter.component_id}}.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = {{cookiecutter.component_id}}.app; sourceTree = BUILT_PRODUCTS_DIR; }; + C5B1973323EAFA6D007C9A28 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + C5B1973523EAFA6D007C9A28 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + C5B1973723EAFA6D007C9A28 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + C5B1973A23EAFA6D007C9A28 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + C5B1973C23EAFA6E007C9A28 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + C5B1973F23EAFA6E007C9A28 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + C5B1974123EAFA6E007C9A28 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + C5B1974623EAFA6E007C9A28 /* {{cookiecutter.component_id}}Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = {{cookiecutter.component_id}}Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + C5B1974A23EAFA6E007C9A28 /* {{cookiecutter.component_id}}Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = {{cookiecutter.component_id}}Tests.swift; sourceTree = ""; }; + C5B1974C23EAFA6E007C9A28 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + C5B1972D23EAFA6D007C9A28 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C5B1974323EAFA6E007C9A28 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + C5B1972723EAFA6D007C9A28 = { + isa = PBXGroup; + children = ( + C5B1973223EAFA6D007C9A28 /* {{cookiecutter.component_id}} */, + C5B1974923EAFA6E007C9A28 /* {{cookiecutter.component_id}}Tests */, + C5B1973123EAFA6D007C9A28 /* Products */, + ); + sourceTree = ""; + }; + C5B1973123EAFA6D007C9A28 /* Products */ = { + isa = PBXGroup; + children = ( + C5B1973023EAFA6D007C9A28 /* {{cookiecutter.component_id}}.app */, + C5B1974623EAFA6E007C9A28 /* {{cookiecutter.component_id}}Tests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + C5B1973223EAFA6D007C9A28 /* {{cookiecutter.component_id}} */ = { + isa = PBXGroup; + children = ( + C5B1973323EAFA6D007C9A28 /* AppDelegate.swift */, + C5B1973523EAFA6D007C9A28 /* SceneDelegate.swift */, + C5B1973723EAFA6D007C9A28 /* ViewController.swift */, + C5B1973923EAFA6D007C9A28 /* Main.storyboard */, + C5B1973C23EAFA6E007C9A28 /* Assets.xcassets */, + C5B1973E23EAFA6E007C9A28 /* LaunchScreen.storyboard */, + C5B1974123EAFA6E007C9A28 /* Info.plist */, + ); + path = {{cookiecutter.component_id}}; + sourceTree = ""; + }; + C5B1974923EAFA6E007C9A28 /* {{cookiecutter.component_id}}Tests */ = { + isa = PBXGroup; + children = ( + C5B1974A23EAFA6E007C9A28 /* {{cookiecutter.component_id}}Tests.swift */, + C5B1974C23EAFA6E007C9A28 /* Info.plist */, + ); + path = {{cookiecutter.component_id}}Tests; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + C5B1972F23EAFA6D007C9A28 /* {{cookiecutter.component_id}} */ = { + isa = PBXNativeTarget; + buildConfigurationList = C5B1974F23EAFA6E007C9A28 /* Build configuration list for PBXNativeTarget "{{cookiecutter.component_id}}" */; + buildPhases = ( + C5B1972C23EAFA6D007C9A28 /* Sources */, + C5B1972D23EAFA6D007C9A28 /* Frameworks */, + C5B1972E23EAFA6D007C9A28 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = {{cookiecutter.component_id}}; + productName = {{cookiecutter.component_id}}; + productReference = C5B1973023EAFA6D007C9A28 /* {{cookiecutter.component_id}}.app */; + productType = "com.apple.product-type.application"; + }; + C5B1974523EAFA6E007C9A28 /* {{cookiecutter.component_id}}Tests */ = { + isa = PBXNativeTarget; + buildConfigurationList = C5B1975223EAFA6E007C9A28 /* Build configuration list for PBXNativeTarget "{{cookiecutter.component_id}}Tests" */; + buildPhases = ( + C5B1974223EAFA6E007C9A28 /* Sources */, + C5B1974323EAFA6E007C9A28 /* Frameworks */, + C5B1974423EAFA6E007C9A28 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + C5B1974823EAFA6E007C9A28 /* PBXTargetDependency */, + ); + name = {{cookiecutter.component_id}}Tests; + productName = {{cookiecutter.component_id}}Tests; + productReference = C5B1974623EAFA6E007C9A28 /* {{cookiecutter.component_id}}Tests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + C5B1972823EAFA6D007C9A28 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1100; + LastUpgradeCheck = 1100; + ORGANIZATIONNAME = Organization; + TargetAttributes = { + C5B1972F23EAFA6D007C9A28 = { + CreatedOnToolsVersion = 11.0; + }; + C5B1974523EAFA6E007C9A28 = { + CreatedOnToolsVersion = 11.0; + TestTargetID = C5B1972F23EAFA6D007C9A28; + }; + }; + }; + buildConfigurationList = C5B1972B23EAFA6D007C9A28 /* Build configuration list for PBXProject "{{cookiecutter.component_id}}" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = C5B1972723EAFA6D007C9A28; + productRefGroup = C5B1973123EAFA6D007C9A28 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + C5B1972F23EAFA6D007C9A28 /* {{cookiecutter.component_id}} */, + C5B1974523EAFA6E007C9A28 /* {{cookiecutter.component_id}}Tests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + C5B1972E23EAFA6D007C9A28 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C5B1974023EAFA6E007C9A28 /* LaunchScreen.storyboard in Resources */, + C5B1973D23EAFA6E007C9A28 /* Assets.xcassets in Resources */, + C5B1973B23EAFA6D007C9A28 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C5B1974423EAFA6E007C9A28 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + C5B1972C23EAFA6D007C9A28 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C5B1973823EAFA6D007C9A28 /* ViewController.swift in Sources */, + C5B1973423EAFA6D007C9A28 /* AppDelegate.swift in Sources */, + C5B1973623EAFA6D007C9A28 /* SceneDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C5B1974223EAFA6E007C9A28 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C5B1974B23EAFA6E007C9A28 /* {{cookiecutter.component_id}}Tests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + C5B1974823EAFA6E007C9A28 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = C5B1972F23EAFA6D007C9A28 /* {{cookiecutter.component_id}} */; + targetProxy = C5B1974723EAFA6E007C9A28 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + C5B1973923EAFA6D007C9A28 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + C5B1973A23EAFA6D007C9A28 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + C5B1973E23EAFA6E007C9A28 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + C5B1973F23EAFA6E007C9A28 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + C5B1974D23EAFA6E007C9A28 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + C5B1974E23EAFA6E007C9A28 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + C5B1975023EAFA6E007C9A28 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = {{cookiecutter.component_id}}/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.organization.{{cookiecutter.component_id}}; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + C5B1975123EAFA6E007C9A28 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = {{cookiecutter.component_id}}/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.organization.{{cookiecutter.component_id}}; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + C5B1975323EAFA6E007C9A28 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = {{cookiecutter.component_id}}Tests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.organization.{{cookiecutter.component_id}}Tests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/{{cookiecutter.component_id}}.app/{{cookiecutter.component_id}}"; + }; + name = Debug; + }; + C5B1975423EAFA6E007C9A28 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = {{cookiecutter.component_id}}Tests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.organization.{{cookiecutter.component_id}}Tests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/{{cookiecutter.component_id}}.app/{{cookiecutter.component_id}}"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + C5B1972B23EAFA6D007C9A28 /* Build configuration list for PBXProject "{{cookiecutter.component_id}}" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C5B1974D23EAFA6E007C9A28 /* Debug */, + C5B1974E23EAFA6E007C9A28 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + C5B1974F23EAFA6E007C9A28 /* Build configuration list for PBXNativeTarget "{{cookiecutter.component_id}}" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C5B1975023EAFA6E007C9A28 /* Debug */, + C5B1975123EAFA6E007C9A28 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + C5B1975223EAFA6E007C9A28 /* Build configuration list for PBXNativeTarget "{{cookiecutter.component_id}}Tests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C5B1975323EAFA6E007C9A28 /* Debug */, + C5B1975423EAFA6E007C9A28 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = C5B1972823EAFA6D007C9A28 /* Project object */; +} diff --git a/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..fab8bef75c --- /dev/null +++ b/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}.xcodeproj/project.xcworkspace/xcuserdata/patrickb.xcuserdatad/UserInterfaceState.xcuserstate b/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}.xcodeproj/project.xcworkspace/xcuserdata/patrickb.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..db2ac7d09bb4dd4c51295539c862665e0c404846 GIT binary patch literal 12954 zcmeHtd3aOR_Vyl{CTW`XBu!A#v<>NL8ivj@DAExsv|vjMlp&_+X&Y#hkfaPEup=nW zBMui8Y0FemP*emQ5e3B=P(eg+KoPxMoUg;x-#R%>+5(r)`#sP7@3v2yoU^C3_j>nQ z?^=5|dfJ>me`e-Egb_g$MIaf9L~;~0m7gbgeNMM)8gKVHTAlDTnfJRrQ+apeY{B98 zX%W6|jg5<~t{pEd6q<^AE4_;pNLg3wxBCT#w`i~#sZcb!2wjW@qv2=-N<--=17)Ht zl#NPIDJnzdr~-{e<4`kdL9NJ%W}`W%4YeZ|aw89#i@eB(7NV=rQgj2l5#5AtMz^3_ z(QW8%^blHuI#Cx|k9yFR! z8Td-vggtmJ_F^CQV}=9x8oUfI$1CtHXbZj-uf%`D-FPkDjJM#Y@H6-oya&I7-^EAp zNBAf{fxp0C;$QHu_&5AJ{ullO|49(RL{1bWhQyM=#6aSSk(h~h z%E>g+K&F!!WG0zKY~)H}Cr&b(c*s>`30X##lbgvc%Fut7#3LM(wnbI_MnQMz5jE=yG~3y^gM+ z*V7y5jr1mZ8@-3#OYfs=XeaHW57SNbak`a0L0_Z$=_ctT-#bD6>4Ru(Yr|EjPEQG_5EvyD+V!s4%M}r?{}9u%y(+ z=_*?)D04eH+69+CG_V1g(Xb6D9vM*rN<>M>goZE~i)3=9U`iInR2xt- z;!z5+KqV_mVbN?2OJ|)>X()VV*|^dD`=4xYEOop5UUyrY;I$Rz73WqI=M<(DmS^Us z<>nQZrInPI$@i?)ep7d%IxcEa4Z>S&7{{#qO(j`kakz z!W5^!wbtosX%nEt###FhEOlY4uB{P#ez(^S)q^{>%?#MCT?#WId*n07rH1uJ(c z8V&!)usB$^tJ$^i|3Q{D6M8Oha)Q9cUIFCf;JLQdJ-^yM&)H&!*EVj1ICw~X1B5OU ze9jh^*svB%p1!$UZ>CPs&Y38wmP5S?L|#*SMau>@2ib7 z)e3DeeW9su{e#0)x|)OqlkEOhQ8%J4q;gm@@gXFuw6dl$q1g^HP7?i!&ky^mM(}tA zAAGU}+r&iZa z3_9_Ui1(mX=ZJVe!16(s2C&Nz?PqLoH6`qcE2&5xGvmd6gLk8~v#M*WI{czBDfW8p z3%gM_T8CDGp$ zsBK>v8^cDjQdYz+vvCXUraQn6xLtreo3m;0 zVri+~LGRi)BZwg}I>|S9!IAW?x6|7NcmqXUu#VK%I&`p0#6aBAKc$}o=mQY(5IT%L zL`SAK_4?PknhDeV&UWxc_I3{|W+e>SxRK4yHeqB&Muuped>J0Q-|KYDY0LmaJ_mSc z#s#Nx;nMU4;!=CO?%9%BGghC5IfyTri*aCd-QI;@9vySQJjRCV*rZ}d(9nfuXipsL@|zrdwCI=o&$ z$$);u=4?D`IfVYi2xCl8ET%XD%Wx!?V+B^?DEJnQ)vy6ViXJfQ>L7s`BC1*B9g`P& zgmfP$+0`;|)j(7LhF|Ej6*ey374-SNd*tl;ngONaIQ^jptwZvZF90_r204A(g$Zmj zt78*bGkmLIP5lf3j>Vj42yh&`f>j0g?WEwUVLj|ZJcw0?cOf1u?m{;4|GnwXzcr*y zZ~!O6qVnhnwxGiz;cc}01Tb6aUZD-F&pd%4R?CWmP{c#=u)6A6*h*b^sED$QU~{M2 zJwON~xTlMF2Or*R>Tnv)0ubSJoPjf0J)6R&cH?ZEgLBz5*2o;9%^wjoNja0GhLZL} zMBwbL0*E?a^+{e~p3~jon{4;C2)zI*6&M0AS6}vLXs+dW92(YzEAUu0lg;YFmH2XI zV^^}QdTCPi(tzN6&K>_R*i3u{uCZ|m1Lkw~`gG%3JXzGsg{z(4*qm3e(B!7zsglpK zaRosU(gz4q+u`xJz5aA*rXjHg*7+-QoQY>i;n6u(pbOi?iHV`pIb{+y?HD2u{n_O~ zOVouOV&n4{hNdXs*=X2W+>BdrD|Rv`o6Y91wzYT;Zo}=^#oC#Vb+CCXYpTc^M7lIB zNF2m(4HBOS65Zf1VdfTQg`r_O@Kxe4^YDDU054=N=4Ku?cP$X12D}(qfePg?FK{-X zfbsRfpGDsYOih?Cl3iboeSVO20JIGQOMAI1IhZ^L(l zxGcVt&1YFsk9Pxa#rNQQ*#dS|*vsHmc=g$&5H}+$?u7Lp$`-QW$zv9of($U!{W{!p zPKy?3@d)T^C|eY4!3}8f7~UEd=n1wsB+xdrq#Hktx8ogb31jS{fQ$2K+5b!QTUFQ}9pt zXLdW__73<QU4fIE_&fwPr?lN;BuFU?xx;ty})A!4%;O9 zeBWfR04N>;-9UR_1tF7iZc~xylr4l{#OV<898hLiri z*-2#Vu24K9ZP()9j6zkFLQEX)=iDq|$Nx2}=3^n734h`!j8loj}>>hS6yH9+8 zUw9CIvnI{FU_Xk^9r9i}5fJOE(r!pz{B_q+2H7pZ&$D&2~ zxg$(@xuPnvax+0IMfsT!U69czmRv^0h(x9d2;W3a#LC7{`WX`%A&Zn-{(sZX$GBL9 zGD;P#j)~j;7ZQI6jMZrvI=td7ZetaJYQPF#wAwQ_T`f5D$8ABR0ruH zK&s$!2wg>^`Vt+Nj?TEOxKxDRSxMv0F-+B20@=s71LYMNW5-oOxC6R!fRS@ax~r?J zECVwPHW6k$u&5StX^`WE^d%&ivob5Pi!;lM)3U_;ZGK*1URrTZc3E0hc1dPtc}Zb@ zMMd_EMdF%|fJM*CTntmIss2=OUoM=QugG;AXX zH;z<-faA#oQbnrSTDFd@XB*f?*25lVn>HdVxq{Sy{F6x?sV7r_UOW=OQ30$D;Di89 z3}90L4-u`ft^`QN9FN=S^813hd9cIaoDIDRPKkGmE?n?cxV;bs`esNe{8~q=ARZLx zq{o^L7cfn~(-q8Kf!_`7Rta4qca^@0IXhPiGzAAQrkpEX&2Fe3^3mby7fU2yV00ml zA6kk=B8WstO`2E}d$fx*lNPp_Z4)PzHh}aD>>_=V&|>#>_-fr9UI!$s!z7l>A?@I! za5SswBrdk4&&|yx9pHlpM6_fcnGX@|W9%u2XeG8w7C|UUu4a$-3nj@?!o*OLJ;Ap2 z3m_8{LX)_bT;D%{BsV|+`J@;?o;NZ(@5@kEw~>_)WRTm*9pp}O7kirRV9&7U){?u) zJ>*_;AA5nl$G%`c!?gOMfAU~3(%)|5%A5adS+NW;6zMbC>Av~SW`BPnx`IONv~iXH zJt2ZT=)&2%V3l*F+YprQSsPdO-;*w*76`9U2Jsx6H$k@Q>=k+>D8utMuH?TcL$Swm zmIP9yL$;6|Xjl(ey~n}sJpq>QDYA_`O}4Wa*)H}H+s$5PudqFAFMG9z>=eTTltP{- zFF>X$g}lUG3x){k><#uNgpuiNzX)za-}aNXXL>l<0)fN1g%Af-0Tw|hB>z%zquvC-1=ezbQjIe;3( zKlVDTtN1sXi%5RqjO1EER^bTw7!B$oACaT%E%tU7IYy4Nci6j-$`wTmrrrhLtft7KWJep4nXdx|PU$T?zD|U)~&AwrOXQ$aedg!PyCdLFYQOeGQ zG4Wjx6F>eB6aP6TCW@Gt#J&w-qV|HAXrMDCOw0gGd@m~42AKHaTudlwNXJdIMZ!cg zVB)8L-y~GOoz53A;i7Kpp>wI1`lz3F(0S~i>=*Vc`;Gn1{>A=ae+Dq>p$o#GSR4ce z3t${VMF7i0SOl=*f4KP1adESVi(3Mign)5700Y9AJH@{MrU4uw{ReMDIdDJ0WGKWs z4@dyHpS=~pk)pa+vv&emelA30SB4sO)AbTW*0BQtth_*@P>VZNw)-m zs}C11h={onDU$w6nJ)SyeI5z$Lt(Hf+Wtn3kaLDoSJi|51|xMdf*=0cCsV-S{!?>g!608Z)+x9Df# zno`RzgLy*f`zf?{9gc=b==NYNR0FZl(_%ElMnf)PFx<8q0rx~lqRZe?RXyC4nnl)= zN5~Gc3+^x;fE$b-0$=(BZZMu8U&3w06mV0;v=nYAj)mKa0jaVPCG2-Ef zMSjbIaz+e6kMNeA%6{m9l$e_sLet z9+0h;t&w%f*2>n)Hp(8BJtEsIdr5X8(h!*vIWDq3(iwSk`RLoZR6gMbtSFBTP zQ1mFaDfTIjDvl{WQJhlzr1(uKSL&2{>sC_bt*YC_Z%Q4LWoQNF0fQA?u&QOlxUjCw8V zSk&iHzpAJzU6rGnrD{>lR<)^Iszs`6RLfP@sjgSurn*CQm+EfSy{c~24%O?bgQ^cy zhgC;ZpR4|i#?dsIj~)}<6g@9`W%S1AC!_aAAB=uK`cU+T(H}*B9DO|c)99b0e~JDr z`d`t1sNIt}x>Q}R9;>cY*Q;l#XRF)PF11I!P`y;WT)kSo zNxfD5qpG55wi z5VJaFP0YHO4KY12n_{-c?1*_Q=7X5yu_#s^n;Ls*Y)z~!_R3g$tRwcp*!8hnV)w+p z9{XwR*I;-RoQX^0^0`88BzFlnnj6bia^txwZX$OD*T6M#Zf*s)l6#PQi0kCKxpmxT z?lEpF_awKC+s?hueXkj$$v-lRoWOWr`2lXwPr1^wP>x{VcG)iXzdtniMC8TUR$M|sJ%j4tDT{BYCT%7)~}tX zU94TI4QQ8XuhrhFyM|BFF zL074}QrE8Y=)AfP-F)3b-A%gtboc8X)UDBV>DKBV)$P>n((Ts0qT8!`OZSd$zwV&! zeccDT6S~v7-*kWIk)G;hdbwVwzeqn=Z`3F1P5Lx_raoJrr!UYK=_~YO_2cxH>!;`& z^fUA}yO|xZm)g;UPn(q1&*|u))w{ z*kpLt@Vwzg!*0VK!)t~&3~w9u8x9)2j#tF%<444gj-MXi9)ErO-SMm9ABbNa-yOd$ zenWgu{Nea7;=hXjI{tM0nfUMG|1?G#6~-uIv@y=8H(q2MY>YSZ##G}_<8b38#?i(x z#u8(hvC7zBv>EM2hq2W-+t_Aw8J8LFGp;jkHa=$DYJAeT&G>?GmvOi872{sxYsUAD zhm0Q@KT6;evJ!F<@)8OXmL=StaA(5GgnJT>CY(z6CgF6#nZ&%rlEk{iDT&h(rzg%# zv?baT9f?9>OX5|DS0^q>WQo@#E>FBJ@%qFY6K_tuHE~nozQof>TvC40)TFDE?oZm8 zv^!}}(yK}Pk`5$&l=N}Z@uW|aK2Q20>15KkNq?BANoJCpqD?U-&ZIRZn$k@9rUFxu z=@L_=soFHjRAZWAnr51AnrU*Fyr!k5J54K1_n7W8tuj4eT5Vcm>N2f0J#E@yddBpe z=>^j+({9r%roE=uOs|_x4v`NTKBQ`hZ^+$4b__W*c-mtuF*>8Eza>(+b<)r0X%O9zlR9)&tsfN^qR8wkl zswH)3>WI|KQcF_HQY%s`QzxWOOsz?+OP!KBE!C0gN?nnp|;L>q+Y=>o?ZZ Y)-%@cr2GdV(kK14H`U(zVg2F%0J`RvBLDyZ literal 0 HcmV?d00001 diff --git a/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}.xcodeproj/xcuserdata/patrickb.xcuserdatad/xcschemes/xcschememanagement.plist b/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}.xcodeproj/xcuserdata/patrickb.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000000..abde0216f9 --- /dev/null +++ b/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}.xcodeproj/xcuserdata/patrickb.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + {{cookiecutter.component_id}}.xcscheme_^#shared#^_ + + orderHint + 0 + + + + diff --git a/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}/AppDelegate.swift b/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}/AppDelegate.swift new file mode 100644 index 0000000000..bdcc2c1309 --- /dev/null +++ b/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}/AppDelegate.swift @@ -0,0 +1,36 @@ +// +// AppDelegate.swift +// {{cookiecutter.component_id}} +// +// Copyright © 2020 Organization. All rights reserved. +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + + +} + diff --git a/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}/Assets.xcassets/AppIcon.appiconset/Contents.json b/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..d8db8d65fd --- /dev/null +++ b/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}/Assets.xcassets/Contents.json b/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..da4a164c91 --- /dev/null +++ b/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}/Base.lproj/LaunchScreen.storyboard b/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000000..865e9329f3 --- /dev/null +++ b/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}/Base.lproj/Main.storyboard b/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}/Base.lproj/Main.storyboard new file mode 100644 index 0000000000..25a763858e --- /dev/null +++ b/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}/Base.lproj/Main.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}/Info.plist b/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}/Info.plist new file mode 100644 index 0000000000..2a3483c0d2 --- /dev/null +++ b/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}/Info.plist @@ -0,0 +1,64 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}/SceneDelegate.swift b/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}/SceneDelegate.swift new file mode 100644 index 0000000000..4919f3a006 --- /dev/null +++ b/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}/SceneDelegate.swift @@ -0,0 +1,52 @@ +// +// SceneDelegate.swift +// {{cookiecutter.component_id}} +// +// Copyright © 2020 Organization. All rights reserved. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let _ = (scene as? UIWindowScene) else { return } + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + + +} + diff --git a/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}/ViewController.swift b/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}/ViewController.swift new file mode 100644 index 0000000000..9e2c5fb077 --- /dev/null +++ b/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}/ViewController.swift @@ -0,0 +1,19 @@ +// +// ViewController.swift +// {{cookiecutter.component_id}} +// +// Copyright © 2020 Organization. All rights reserved. +// + +import UIKit + +class ViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view. + } + + +} + diff --git a/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}Tests/Info.plist b/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}Tests/Info.plist new file mode 100644 index 0000000000..64d65ca495 --- /dev/null +++ b/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}Tests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}Tests/{{cookiecutter.component_id}}Tests.swift b/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}Tests/{{cookiecutter.component_id}}Tests.swift new file mode 100644 index 0000000000..3ccb2e1662 --- /dev/null +++ b/backend/scaffolder/templates/ios-swift-app-template/{{cookiecutter.component_id}}/{{cookiecutter.component_id}}Tests/{{cookiecutter.component_id}}Tests.swift @@ -0,0 +1,33 @@ +// +// {{cookiecutter.component_id}}Tests.swift +// {{cookiecutter.component_id}}Tests +// +// Copyright © 2020 Organization. All rights reserved. +// + +import XCTest +@testable import {{cookiecutter.component_id}} + +class {{cookiecutter.component_id}}Tests: XCTestCase { + + override func setUp() { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testPerformanceExample() { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} From 95ea391cc1de40974b6f87f3d8b3687ffda85255 Mon Sep 17 00:00:00 2001 From: Raghunandan Balachandran Date: Wed, 5 Feb 2020 14:47:09 +0100 Subject: [PATCH 08/12] remove createGlobalStyles from theme --- frontend/packages/app/src/App.tsx | 21 +++++++++++++----- frontend/packages/core/src/index.ts | 2 -- .../packages/core/src/theme/BackstageTheme.js | 22 +------------------ .../LoginComponent/LoginComponent.tsx | 2 +- 4 files changed, 18 insertions(+), 29 deletions(-) diff --git a/frontend/packages/app/src/App.tsx b/frontend/packages/app/src/App.tsx index 0f22c76671..a814c94a57 100644 --- a/frontend/packages/app/src/App.tsx +++ b/frontend/packages/app/src/App.tsx @@ -6,7 +6,6 @@ import { InfoCard, Page, theme, - withGlobalStyles, } from '@backstage/core'; import helloWorld, { MyComponent } from '@backstage/plugin-hello-world'; //import PageHeader from './components/PageHeader'; @@ -25,6 +24,20 @@ import SideBar from './components/SideBar'; import entities from './entities'; const useStyles = makeStyles(theme => ({ + '@global': { + html: { + height: '100%', + fontFamily: theme.typography.fontFamily, + }, + body: { + height: '100%', + fontFamily: theme.typography.fontFamily, + }, + a: { + color: 'inherit', + textDecoration: 'none', + }, + }, root: { display: 'grid', // FIXME: Don't used a fixed width here @@ -95,8 +108,6 @@ const AppShell: FC<{}> = ({ children }) => { ); }; -const AppContent = withGlobalStyles(AppShell); - const app = createApp(); app.registerEntityKind(...entities); @@ -107,7 +118,7 @@ const App: FC<{}> = () => { return ( - + @@ -118,7 +129,7 @@ const App: FC<{}> = () => { - + ); diff --git a/frontend/packages/core/src/index.ts b/frontend/packages/core/src/index.ts index 45110734fb..5619dab3e8 100644 --- a/frontend/packages/core/src/index.ts +++ b/frontend/packages/core/src/index.ts @@ -9,5 +9,3 @@ export { default as InfoCard } from '../src/layout/InfoCard'; export { default as ErrorBoundary } from '../src/layout/ErrorBoundary'; export { default as BackstageTheme } from '../src/theme/BackstageTheme'; -export { withGlobalStyles } from '../src/theme/BackstageTheme'; - diff --git a/frontend/packages/core/src/theme/BackstageTheme.js b/frontend/packages/core/src/theme/BackstageTheme.js index fbf521abe0..1c0f4cd2b7 100644 --- a/frontend/packages/core/src/theme/BackstageTheme.js +++ b/frontend/packages/core/src/theme/BackstageTheme.js @@ -1,4 +1,4 @@ -import { createMuiTheme, withStyles } from '@material-ui/core'; +import { createMuiTheme } from '@material-ui/core'; import { darken, lighten } from '@material-ui/core/styles/colorManipulator'; import { blue, yellow } from '@material-ui/core/colors'; @@ -332,24 +332,4 @@ const BackstageTheme = { ...extendedTheme, ...createOverrides(extendedTheme) }; // Temporary workaround for files incorrectly importing the theme directly export const V1 = BackstageTheme; - -// Default styles for the application other than the built in mui components -const styles = theme => ({ - '@global': { - html: { - height: '100%', - fontFamily: theme.typography.fontFamily, - }, - body: { - height: '100%', - fontFamily: theme.typography.fontFamily, - }, - a: { - color: 'inherit', - textDecoration: 'none', - }, - }, -}); - -export const withGlobalStyles = withStyles(styles); export default BackstageTheme; diff --git a/frontend/packages/plugins/login/src/components/LoginComponent/LoginComponent.tsx b/frontend/packages/plugins/login/src/components/LoginComponent/LoginComponent.tsx index 31caa7d4db..8826eceda7 100644 --- a/frontend/packages/plugins/login/src/components/LoginComponent/LoginComponent.tsx +++ b/frontend/packages/plugins/login/src/components/LoginComponent/LoginComponent.tsx @@ -23,7 +23,7 @@ const LoginComponent: FC<{}> = () => { style={{ marginTop: '24px', marginBottom: '24px' }} >