From 3293fe96a43c1778d99bcf2c7e7866e78d7099e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20DOREAU?= Date: Sat, 30 May 2020 03:43:40 +0200 Subject: [PATCH 01/76] feat(core): add Tabs component --- .../core/src/components/Tabs/Tab.test.tsx | 27 ++++ packages/core/src/components/Tabs/Tab.tsx | 65 ++++++++ packages/core/src/components/Tabs/TabBar.tsx | 54 +++++++ packages/core/src/components/Tabs/TabIcon.tsx | 72 +++++++++ .../core/src/components/Tabs/TabPanel.tsx | 42 +++++ .../core/src/components/Tabs/Tabs.stories.tsx | 47 ++++++ packages/core/src/components/Tabs/Tabs.tsx | 145 ++++++++++++++++++ packages/core/src/components/Tabs/index.ts | 17 ++ packages/core/src/components/Tabs/utils.ts | 42 +++++ packages/core/src/index.ts | 1 + packages/theme/src/themes.ts | 8 + packages/theme/src/types.ts | 4 + 12 files changed, 524 insertions(+) create mode 100644 packages/core/src/components/Tabs/Tab.test.tsx create mode 100644 packages/core/src/components/Tabs/Tab.tsx create mode 100644 packages/core/src/components/Tabs/TabBar.tsx create mode 100644 packages/core/src/components/Tabs/TabIcon.tsx create mode 100644 packages/core/src/components/Tabs/TabPanel.tsx create mode 100644 packages/core/src/components/Tabs/Tabs.stories.tsx create mode 100644 packages/core/src/components/Tabs/Tabs.tsx create mode 100644 packages/core/src/components/Tabs/index.ts create mode 100644 packages/core/src/components/Tabs/utils.ts diff --git a/packages/core/src/components/Tabs/Tab.test.tsx b/packages/core/src/components/Tabs/Tab.test.tsx new file mode 100644 index 0000000000..a841af776a --- /dev/null +++ b/packages/core/src/components/Tabs/Tab.test.tsx @@ -0,0 +1,27 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { wrapInTestApp } from '@backstage/test-utils'; +import Tab from './Tab'; + +describe('', () => { + it('renders without exploding', () => { + const rendered = render(wrapInTestApp()); + expect(rendered.getByText('test')).toBeInTheDocument(); + }); +}); diff --git a/packages/core/src/components/Tabs/Tab.tsx b/packages/core/src/components/Tabs/Tab.tsx new file mode 100644 index 0000000000..3726642d94 --- /dev/null +++ b/packages/core/src/components/Tabs/Tab.tsx @@ -0,0 +1,65 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { Tab, withStyles, Theme } from '@material-ui/core'; +import { BackstageTheme } from '@backstage/theme'; + +const withStylesProps = (styles: any) => (Component: any) => (props: any) => { + const Comp = withStyles((theme: Theme) => styles(props, theme))(Component); + return ; +}; + +interface StyledTabProps { + label: string; + isFirstNav?: boolean; + isFirstIndex?: boolean; +} + +const tabMarginLeft = (isFirstNav: boolean, isFirstIndex: boolean) => { + if (isFirstIndex) { + if (isFirstNav) { + return '20px'; + } + return '0'; + } + return '40px'; +}; + +const tabStyles = (props: any, theme: BackstageTheme) => ({ + root: { + textTransform: 'none', + height: '64px', + fontWeight: theme.typography.fontWeightBold, + fontSize: theme.typography.pxToRem(13), + color: theme.palette.textSubtle, + marginLeft: tabMarginLeft(props.isFirstNav, props.isFirstIndex), + width: '130px', + minWidth: '130px', + '&:hover': { + outline: 'none', + backgroundColor: 'transparent', + color: theme.palette.textSubtle, + }, + }, +}); + +const StyledTab = withStylesProps(tabStyles)((props: StyledTabProps) => { + const { isFirstNav, isFirstIndex, ...rest } = props; + return ; +}); + +export default StyledTab; diff --git a/packages/core/src/components/Tabs/TabBar.tsx b/packages/core/src/components/Tabs/TabBar.tsx new file mode 100644 index 0000000000..510111dc37 --- /dev/null +++ b/packages/core/src/components/Tabs/TabBar.tsx @@ -0,0 +1,54 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { FC } from 'react'; +import { Tabs, makeStyles } from '@material-ui/core'; +import { BackstageTheme } from '@backstage/theme'; + +interface StyledTabsProps { + value: number; + onChange: (event: React.ChangeEvent<{}>, newValue: number) => void; +} + +const useStyles = makeStyles(theme => ({ + indicator: { + display: 'flex', + justifyContent: 'center', + backgroundColor: theme.palette.tabbar.indicator, + height: '4px', + }, + flexContainer: { + alignItems: 'center', + }, + root: { + '&:last-child': { + marginLeft: 'auto', + }, + }, +})); + +const StyledTabs: FC = props => { + const classes = useStyles(props); + return ( + }} + /> + ); +}; + +export default StyledTabs; diff --git a/packages/core/src/components/Tabs/TabIcon.tsx b/packages/core/src/components/Tabs/TabIcon.tsx new file mode 100644 index 0000000000..fd58735427 --- /dev/null +++ b/packages/core/src/components/Tabs/TabIcon.tsx @@ -0,0 +1,72 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { IconButton, withStyles, Theme } from '@material-ui/core'; + +export const withStylesProps = (styles: any) => (Component: any) => ( + props: any, +) => { + const Comp = withStyles((theme: Theme) => styles(props, theme))(Component); + return ; +}; + +interface StyledIconProps { + ariaLabel: string; + children: any; + classes: any; + isNext?: boolean; + onClick: any; +} + +const iconStyles = (props: StyledIconProps) => ({ + root: { + color: '#6E6E6E', + overflow: 'visible', + fontSize: '1.5rem', + textAlign: 'center', + borderRadius: '50%', + backgroundColor: '#E6E6E6', + marginLeft: props.isNext ? 'auto' : '0', + marginRight: props.isNext ? '0' : '10px', + '&:hover': { + backgroundColor: '#E6E6E6', + opacity: '1', + }, + }, +}); + +const StyledIcon = withStylesProps(iconStyles)((props: StyledIconProps) => { + const { + classes: { root }, + ariaLabel, + onClick, + } = props; + return ( + + {props.children} + + ); +}); + +export default StyledIcon; diff --git a/packages/core/src/components/Tabs/TabPanel.tsx b/packages/core/src/components/Tabs/TabPanel.tsx new file mode 100644 index 0000000000..32536dc86d --- /dev/null +++ b/packages/core/src/components/Tabs/TabPanel.tsx @@ -0,0 +1,42 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { FC } from 'react'; +import Box from '@material-ui/core/Box'; + +export interface TabPanelProps { + children: any; + value: any; + index: number; +} + +const TabPanel: FC = props => { + const { children, value, index, ...other } = props; + + return ( + + ); +}; + +export default TabPanel; diff --git a/packages/core/src/components/Tabs/Tabs.stories.tsx b/packages/core/src/components/Tabs/Tabs.stories.tsx new file mode 100644 index 0000000000..5258e9a51a --- /dev/null +++ b/packages/core/src/components/Tabs/Tabs.stories.tsx @@ -0,0 +1,47 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import Tabs from './Tabs'; + +export default { + title: 'Tabs', + component: Tabs, +}; + +const containerStyle = {}; + +export const Default = () => ( +
+ ({ + label: `ANOTHER TAB`, + content:
Content {index}
, + }))} + /> +
+); + +export const Expandable = () => ( +
+ ({ + label: `ANOTHER TAB`, + content:
Content {index}
, + }))} + /> +
+); diff --git a/packages/core/src/components/Tabs/Tabs.tsx b/packages/core/src/components/Tabs/Tabs.tsx new file mode 100644 index 0000000000..cb1f5d6138 --- /dev/null +++ b/packages/core/src/components/Tabs/Tabs.tsx @@ -0,0 +1,145 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { FC, useRef, useEffect, MutableRefObject } from 'react'; +import { BackstageTheme } from '@backstage/theme'; +import { AppBar } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; +import NavigateBeforeIcon from '@material-ui/icons/NavigateBefore'; +import NavigateNextIcon from '@material-ui/icons/NavigateNext'; +import { chunkArray, useWindowWidth } from './utils'; + +/* Import Components */ + +import TabPanel from './TabPanel'; +import TabIcon from './TabIcon'; +import Tab from './Tab'; +import TabBar from './TabBar'; + +/* Props Types */ + +interface TabProps { + label: string; + content: any; +} + +export interface TabsProps { + tabs: TabProps[]; +} + +const useStyles = makeStyles((theme: BackstageTheme) => ({ + root: { + flexGrow: 1, + width: '100%', + }, + styledTabs: { + backgroundColor: theme.palette.tabbar.background, + }, + appbar: { + boxShadow: 'none', + backgroundColor: theme.palette.tabbar.background, + paddingLeft: '10px', + paddingRight: '10px', + }, +})); + +const Tabs: FC = ({ tabs }) => { + const classes = useStyles(); + const [value, setValue] = React.useState(0); + const [navIndex, setNavIndex] = React.useState(0); + const [chunkedTabs, setChunkedTabs] = React.useState([[]] as TabProps[][]); + const wrapper = useRef() as MutableRefObject; + + const size = useWindowWidth(); + + const handleChange = (_: React.ChangeEvent<{}>, newValue: number) => { + setValue(newValue); + }; + + const navigateToPrevChunk = () => { + setValue(navIndex - 1 === 0 ? 0 : 1); + setNavIndex(navIndex - 1); + }; + + const navigateToNextChunk = () => { + setValue(1); + setNavIndex(navIndex + 1); + }; + + const hasNextNavIndex = () => navIndex + 1 < chunkedTabs.length; + + useEffect(() => { + // Each time the window is resized we calculate how many tabs wwe can render given the window width + const padding = 20; // The AppBar padding + + const numberOfTabIcons = navIndex === 0 ? 1 : 2; + const wrapperWidth = + wrapper.current.offsetWidth - padding - numberOfTabIcons * 30; + + const numberOfChunkedElement = Math.floor(wrapperWidth / 170); + setChunkedTabs( + chunkArray([...tabs], numberOfChunkedElement) as TabProps[][], + ); + }, [size]); + + return ( +
+ +
+ + {navIndex !== 0 && ( + + + + )} + {chunkedTabs[navIndex].map((tab, index) => ( + + ))} + {hasNextNavIndex() && ( + + + + )} + +
+
+ {chunkedTabs[navIndex].map((tab, index) => ( + + {tab.content} + + ))} +
+ ); +}; + +export default Tabs; diff --git a/packages/core/src/components/Tabs/index.ts b/packages/core/src/components/Tabs/index.ts new file mode 100644 index 0000000000..03995fe2ea --- /dev/null +++ b/packages/core/src/components/Tabs/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { default } from './Tabs'; diff --git a/packages/core/src/components/Tabs/utils.ts b/packages/core/src/components/Tabs/utils.ts new file mode 100644 index 0000000000..d3fa0dbd3e --- /dev/null +++ b/packages/core/src/components/Tabs/utils.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useState, useEffect } from 'react'; + +export const chunkArray = (myArray: any[], chunkSize: number) => { + const results = []; + while (myArray.length) { + results.push(myArray.splice(0, chunkSize)); + } + return results; +}; + +export const useWindowWidth = () => { + const isClient = typeof window === 'object'; + const getWidth = () => (isClient ? window.innerWidth : undefined); + const [windowWidth, setWindowWidth] = useState(getWidth); + + useEffect((): any => { + if (!isClient) { + return false; + } + + const handleResize = () => setWindowWidth(getWidth()); + + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + return windowWidth; +}; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 2ceea470f4..cd53075c28 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -39,3 +39,4 @@ export { default as TrendLine } from './components/TrendLine'; export { FeatureCalloutCircular } from './components/FeatureDiscovery/FeatureCalloutCircular'; export * from './components/Status'; export { default as WarningPanel } from './components/WarningPanel'; +export { default as Tabs } from './components/Tabs'; diff --git a/packages/theme/src/themes.ts b/packages/theme/src/themes.ts index fe3c71a212..630e81c146 100644 --- a/packages/theme/src/themes.ts +++ b/packages/theme/src/themes.ts @@ -60,6 +60,10 @@ export const lightTheme = createTheme({ icon: '#BDBDBD', background: '#404040', }, + tabbar: { + indicator: '#9BF0E1', + background: '#FFFFFF', + }, }, }); @@ -106,5 +110,9 @@ export const darkTheme = createTheme({ icon: '#181818', background: '#BDBDBD', }, + tabbar: { + indicator: '#9BF0E1', + background: '#424242', + }, }, }); diff --git a/packages/theme/src/types.ts b/packages/theme/src/types.ts index c38d67d063..8d18caf4d1 100644 --- a/packages/theme/src/types.ts +++ b/packages/theme/src/types.ts @@ -44,6 +44,10 @@ type PaletteAdditions = { link: string; gold: string; sidebar: string; + tabbar: { + indicator: string; + background: string; + }; bursts: { fontColor: string; slackChannelText: string; From be70935293c8b2b4d24434c2c9d0e40697a35a1a Mon Sep 17 00:00:00 2001 From: Chanwit Kaewkasi Date: Sun, 31 May 2020 20:24:48 +0700 Subject: [PATCH 02/76] add new plugin to create GitOps-managed Kubernetes clusters with ready-to-use profiles --- packages/app/package.json | 3 +- packages/app/src/plugins.ts | 1 + plugins/gitops-profiles/.eslintrc.js | 3 + plugins/gitops-profiles/dev/index.tsx | 20 ++ plugins/gitops-profiles/package.json | 47 +++ plugins/gitops-profiles/src/api.ts | 170 +++++++++ .../components/ClusterPage/ClusterPage.tsx | 159 ++++++++ .../src/components/ClusterPage/index.ts | 17 + .../components/ClusterTable/ClusterTable.tsx | 69 ++++ .../ClusterTemplateCard.tsx | 102 ++++++ .../components/ClusterTemplateCard/index.ts | 17 + .../ClusterTemplateCardList.tsx | 65 ++++ .../ClusterTemplateCardList/index.ts | 17 + .../components/ProfileCard/ProfileCard.tsx | 109 ++++++ .../src/components/ProfileCard/index.ts | 17 + .../ProfileCardList/ProfileCardList.tsx | 78 ++++ .../src/components/ProfileCardList/index.ts | 17 + .../ProfileCatalog/ProfileCatalog.test.tsx | 36 ++ .../ProfileCatalog/ProfileCatalog.tsx | 338 ++++++++++++++++++ .../src/components/ProfileCatalog/index.ts | 17 + plugins/gitops-profiles/src/index.ts | 18 + plugins/gitops-profiles/src/plugin.test.ts | 23 ++ plugins/gitops-profiles/src/plugin.ts | 28 ++ plugins/gitops-profiles/src/setupTests.ts | 18 + 24 files changed, 1388 insertions(+), 1 deletion(-) create mode 100644 plugins/gitops-profiles/.eslintrc.js create mode 100644 plugins/gitops-profiles/dev/index.tsx create mode 100644 plugins/gitops-profiles/package.json create mode 100644 plugins/gitops-profiles/src/api.ts create mode 100644 plugins/gitops-profiles/src/components/ClusterPage/ClusterPage.tsx create mode 100644 plugins/gitops-profiles/src/components/ClusterPage/index.ts create mode 100644 plugins/gitops-profiles/src/components/ClusterTable/ClusterTable.tsx create mode 100644 plugins/gitops-profiles/src/components/ClusterTemplateCard/ClusterTemplateCard.tsx create mode 100644 plugins/gitops-profiles/src/components/ClusterTemplateCard/index.ts create mode 100644 plugins/gitops-profiles/src/components/ClusterTemplateCardList/ClusterTemplateCardList.tsx create mode 100644 plugins/gitops-profiles/src/components/ClusterTemplateCardList/index.ts create mode 100644 plugins/gitops-profiles/src/components/ProfileCard/ProfileCard.tsx create mode 100644 plugins/gitops-profiles/src/components/ProfileCard/index.ts create mode 100644 plugins/gitops-profiles/src/components/ProfileCardList/ProfileCardList.tsx create mode 100644 plugins/gitops-profiles/src/components/ProfileCardList/index.ts create mode 100644 plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.test.tsx create mode 100644 plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.tsx create mode 100644 plugins/gitops-profiles/src/components/ProfileCatalog/index.ts create mode 100644 plugins/gitops-profiles/src/index.ts create mode 100644 plugins/gitops-profiles/src/plugin.test.ts create mode 100644 plugins/gitops-profiles/src/plugin.ts create mode 100644 plugins/gitops-profiles/src/setupTests.ts diff --git a/packages/app/package.json b/packages/app/package.json index ed2f547ad7..6d9c5acc69 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -8,14 +8,15 @@ "@backstage/plugin-catalog": "^0.1.1-alpha.6", "@backstage/plugin-circleci": "^0.1.1-alpha.6", "@backstage/plugin-explore": "^0.1.1-alpha.6", + "@backstage/plugin-gitops-profiles": "^0.1.1-alpha.6", "@backstage/plugin-home-page": "^0.1.1-alpha.6", "@backstage/plugin-lighthouse": "^0.1.1-alpha.6", "@backstage/plugin-register-component": "^0.1.1-alpha.6", "@backstage/plugin-scaffolder": "^0.1.1-alpha.6", + "@backstage/plugin-sentry": "^0.1.1-alpha.6", "@backstage/plugin-tech-radar": "^0.1.1-alpha.6", "@backstage/plugin-welcome": "^0.1.1-alpha.6", "@backstage/theme": "^0.1.1-alpha.6", - "@backstage/plugin-sentry": "^0.1.1-alpha.6", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "prop-types": "^15.7.2", diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts index 25a5bcfc9d..2cf2bde9d5 100644 --- a/packages/app/src/plugins.ts +++ b/packages/app/src/plugins.ts @@ -23,3 +23,4 @@ export { plugin as Explore } from '@backstage/plugin-explore'; export { plugin as Circleci } from '@backstage/plugin-circleci'; export { plugin as RegisterComponent } from '@backstage/plugin-register-component'; export { plugin as Sentry } from '@backstage/plugin-sentry'; +export { plugin as GitopsProfiles } from '@backstage/plugin-gitops-profiles'; diff --git a/plugins/gitops-profiles/.eslintrc.js b/plugins/gitops-profiles/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/gitops-profiles/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/gitops-profiles/dev/index.tsx b/plugins/gitops-profiles/dev/index.tsx new file mode 100644 index 0000000000..812a5585d4 --- /dev/null +++ b/plugins/gitops-profiles/dev/index.tsx @@ -0,0 +1,20 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createDevApp } from '@backstage/dev-utils'; +import { plugin } from '../src/plugin'; + +createDevApp().registerPlugin(plugin).render(); diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json new file mode 100644 index 0000000000..3e6885c0db --- /dev/null +++ b/plugins/gitops-profiles/package.json @@ -0,0 +1,47 @@ +{ + "name": "@backstage/plugin-gitops-profiles", + "version": "0.1.1-alpha.6", + "main": "dist/index.esm.js", + "main:src": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": true, + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "backstage-cli plugin:build", + "start": "backstage-cli plugin:serve", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "diff": "backstage-cli plugin:diff", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/core": "^0.1.1-alpha.6", + "@backstage/theme": "^0.1.1-alpha.6", + "@material-ui/core": "^4.9.1", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.45", + "react": "^16.13.1", + "react-dom": "^16.13.1", + "react-use": "^14.2.0", + "react-router-dom": "^5.2.0" + }, + "devDependencies": { + "@backstage/cli": "^0.1.1-alpha.6", + "@backstage/dev-utils": "^0.1.1-alpha.6", + "@testing-library/jest-dom": "^5.7.0", + "@testing-library/react": "^9.3.2", + "@testing-library/user-event": "^10.2.4", + "@types/jest": "^25.2.2", + "@types/node": "^12.0.0", + "@types/testing-library__jest-dom": "^5.0.4", + "jest-fetch-mock": "^3.0.3" + }, + "files": [ + "dist/**/*.{js,d.ts}" + ] +} diff --git a/plugins/gitops-profiles/src/api.ts b/plugins/gitops-profiles/src/api.ts new file mode 100644 index 0000000000..ad95b1cd52 --- /dev/null +++ b/plugins/gitops-profiles/src/api.ts @@ -0,0 +1,170 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createApiRef } from '@backstage/core-api'; + +export interface CloneFromTemplateRequest { + templateRepository: string; + secrets: { + awsAccessKeyId: string; + awsSecretAccessKey: string; + }; + targetOrg: string; + targetRepo: string; + gitHubUser: string; + gitHubToken: string; +} + +export interface ApplyProfileRequest { + targetOrg: string; + targetRepo: string; + gitHubUser: string; + gitHubToken: string; + profiles: string[]; +} + +export interface ChangeClusterStateRequest { + targetOrg: string; + targetRepo: string; + gitHubUser: string; + gitHubToken: string; + clusterState: 'present' | 'absent'; // /api/cluster/state +} + +export interface PollLogRequest { + targetOrg: string; + targetRepo: string; + gitHubUser: string; + gitHubToken: string; +} + +export interface Status { + status: string; // queued, in_progress, or completed + message: string; + conclusion: string; // success, failure, neutral, cancelled, skipped, timed_out, or action_required +} + +export interface StatusResponse { + result: Status[]; + link: string; + status: string; +} + +export interface ClusterStatus { + name: string; + link: string; + status: string; + conclusion: string; + runStatus: Status[]; +} + +export interface ListClusterStatusesResponse { + result: ClusterStatus[]; +} + +export interface ListClusterRequest { + gitHubUser: string; + gitHubToken: string; +} + +export class FetchError extends Error { + get name(): string { + return this.constructor.name; + } + + static async forResponse(resp: Response): Promise { + return new FetchError( + `Request failed with status code ${ + resp.status + }.\nReason: ${await resp.text()}`, + ); + } +} + +export type GitOpsApi = { + url: string; + fetchLog(req: PollLogRequest): Promise; + changeClusterState(req: ChangeClusterStateRequest): Promise; + cloneClusterFromTemplate(req: CloneFromTemplateRequest): Promise; + applyProfiles(req: ApplyProfileRequest): Promise; + listClusters(req: ListClusterRequest): Promise; +}; + +export const gitOpsApiRef = createApiRef({ + id: 'plugin.gitops.service', + description: 'Used by the GitOps profiles plugin to make requests', +}); + +export class GitOpsRestApi implements GitOpsApi { + constructor(public url: string = '') {} + + private async fetch(path: string, init?: RequestInit): Promise { + const resp = await fetch(`${this.url}${path}`, init); + if (!resp.ok) throw await FetchError.forResponse(resp); + return await resp.json(); + } + + async fetchLog(req: PollLogRequest): Promise { + return await this.fetch(`/api/cluster/run-status`, { + method: 'post', + headers: new Headers({ + 'Content-Type': 'application/json', + }), + body: JSON.stringify(req), + }); + } + + async changeClusterState(req: ChangeClusterStateRequest): Promise { + return await this.fetch('/api/cluster/state', { + method: 'post', + headers: new Headers({ + 'Content-Type': 'application/json', + }), + body: JSON.stringify(req), + }); + } + + async cloneClusterFromTemplate(req: CloneFromTemplateRequest): Promise { + return await this.fetch('/api/cluster/clone-from-template', { + method: 'post', + headers: new Headers({ + 'Content-Type': 'application/json', + }), + body: JSON.stringify(req), + }); + } + + async applyProfiles(req: ApplyProfileRequest): Promise { + return await this.fetch('/api/cluster/profiles', { + method: 'post', + headers: new Headers({ + 'Content-Type': 'application/json', + }), + body: JSON.stringify(req), + }); + } + + async listClusters( + req: ListClusterRequest, + ): Promise { + return await this.fetch('/api/clusters', { + method: 'post', + headers: new Headers({ + 'Content-Type': 'application/json', + }), + body: JSON.stringify(req), + }); + } +} diff --git a/plugins/gitops-profiles/src/components/ClusterPage/ClusterPage.tsx b/plugins/gitops-profiles/src/components/ClusterPage/ClusterPage.tsx new file mode 100644 index 0000000000..680459de52 --- /dev/null +++ b/plugins/gitops-profiles/src/components/ClusterPage/ClusterPage.tsx @@ -0,0 +1,159 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { FC, useEffect, useState } from 'react'; +import { + Content, + ContentHeader, + Header, + SupportButton, + Page, + pageTheme, + Table, + Progress, + HeaderLabel, + useApi, +} from '@backstage/core'; +import ClusterTable from '../ClusterTable/ClusterTable'; +import { Button, Link, Typography } from '@material-ui/core'; +import { useParams } from 'react-router-dom'; +import { useAsync, useLocalStorage } from 'react-use'; +import { gitOpsApiRef, ListClusterStatusesResponse, Status } from '../../api'; +import { transformRunStatus } from '../ProfileCatalog'; + +const ClusterPage: FC<{}> = () => { + const params = useParams<{ owner: string; repo: string }>(); + + const [loginInfo] = useLocalStorage<{ + token: string; + username: string; + name: string; + }>('githubLoginDetails'); + const [pollingLog, setPollingLog] = useState(true); + const [runStatus, setRunStatus] = useState([]); + const [runLink, setRunLink] = useState(''); + const [showProgress, setShowProgress] = useState(true); + + const api = useApi(gitOpsApiRef); + + const columns = [ + { field: 'status', title: 'Status' }, + { field: 'message', title: 'Message' }, + ]; + + useEffect(() => { + if (pollingLog) { + const interval = setInterval(async () => { + const resp = await api.fetchLog({ + gitHubToken: loginInfo.token, + gitHubUser: loginInfo.username, + targetOrg: params.owner, + targetRepo: params.repo, + }); + + setRunStatus(resp.result); + setRunLink(resp.link); + if (resp.status === 'completed') { + setPollingLog(false); + setShowProgress(false); + } + }, 10000); + return () => clearInterval(interval); + } + return () => {}; + }, [pollingLog]); + + if (params.owner === undefined || params.repo === undefined) { + const { loading, error, value } = useAsync( + () => { + setPollingLog(false); + return api.listClusters({ + gitHubToken: loginInfo.token, + gitHubUser: loginInfo.username, + }); + }, + ); + let content: JSX.Element; + if (loading) { + content = ( + + + + ); + } else if (error) { + content = ( + + + Failed to load cluster, {String(error)} + + + ); + } else { + content = ( + + + + All clusters + + + + ); + } + + return ( + +
+ +
+ {content} +
+ ); + } + return ( + +
+ +
+ +