From 5cf3f854d36b82fb4183025b3dfb9dad39af672e Mon Sep 17 00:00:00 2001 From: Marvin9 Date: Sun, 4 Oct 2020 17:55:43 +0530 Subject: [PATCH 001/139] feat: implement customPageTheme which includes core material-ui theming features & backstage pages theming features --- packages/theme/src/baseTheme.ts | 14 +++++++ packages/theme/src/pageTheme.ts | 66 +++++++++++++++++++++++++++++++++ packages/theme/src/themes.ts | 8 +++- packages/theme/src/types.ts | 21 +++++++++++ 4 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 packages/theme/src/pageTheme.ts diff --git a/packages/theme/src/baseTheme.ts b/packages/theme/src/baseTheme.ts index 00a485bb41..a3fded7e04 100644 --- a/packages/theme/src/baseTheme.ts +++ b/packages/theme/src/baseTheme.ts @@ -19,6 +19,8 @@ import { darken, lighten } from '@material-ui/core/styles/colorManipulator'; import { Overrides } from '@material-ui/core/styles/overrides'; import { + BackstagePageTheme, + BackstagePageThemeOptions, BackstageTheme, BackstageThemeOptions, SimpleThemeOptions, @@ -242,3 +244,15 @@ export function createTheme(options: SimpleThemeOptions): BackstageTheme { const theme = { ...baseTheme, overrides }; return theme; } + +export function createPageTheme( + options: BackstagePageThemeOptions, +): BackstagePageTheme { + const baseTheme = createTheme(options); + const pageTheme = options.pageTheme; + + return { + pageTheme, + baseTheme, + }; +} diff --git a/packages/theme/src/pageTheme.ts b/packages/theme/src/pageTheme.ts new file mode 100644 index 0000000000..f86f3a6449 --- /dev/null +++ b/packages/theme/src/pageTheme.ts @@ -0,0 +1,66 @@ +/* + * 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 { PageTheme } from './types'; + +/* + # How to add a shape + 1. Get the svg shape from figma, should be ~1400 wide, ~400 high + and only the white->transparent mask, no colors. + 2. Run it through https://jakearchibald.github.io/svgomg/ + 3. Run that through https://github.com/tigt/mini-svg-data-uri + with something like https://npm.runkit.com/mini-svg-data-uri + 4. Wrap the output in `url("")` + 5. Give it a name and paste it into the `shapes` object below. +*/ +export const shapes: Record = { + wave: `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='1368' height='400' fill='none'%3e%3cmask id='a' width='1368' height='401' x='0' y='0' maskUnits='userSpaceOnUse'%3e%3cpath fill='url(%23paint0_linear)' d='M437 116C223 116 112 0 112 0h1256v400c-82 0-225-21-282-109-112-175-436-175-649-175z'/%3e%3cpath fill='url(%23paint1_linear)' d='M1368 400V282C891-29 788 40 711 161 608 324 121 372 0 361v39h1368z'/%3e%3cpath fill='url(%23paint2_linear)' d='M1368 244v156H0V94c92-24 198-46 375 0l135 41c176 51 195 109 858 109z'/%3e%3cpath fill='url(%23paint3_linear)' d='M1252 400h116c-14-7-35-14-116-16-663-14-837-128-1013-258l-85-61C98 28 46 8 0 0v400h1252z'/%3e%3c/mask%3e%3cg mask='url(%23a)'%3e%3cpath fill='white' d='M-172-98h1671v601H-172z'/%3e%3c/g%3e%3cdefs%3e%3clinearGradient id='paint0_linear' x1='602' x2='1093.5' y1='-960.5' y2='272' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint1_linear' x1='482' x2='480' y1='1058.5' y2='70.5' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint2_linear' x1='424' x2='446.1' y1='-587.5' y2='274.6' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint3_linear' x1='587' x2='349' y1='-1120.5' y2='341' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3c/defs%3e%3c/svg%3e")`, + wave2: `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='1368' height='400' fill='none'%3e%3cmask id='a' width='1764' height='479' x='-229' y='-6' maskUnits='userSpaceOnUse'%3e%3cpath fill='url(%23paint0_linear)' d='M0 400h1350C1321 336 525 33 179-2c-345-34-395 236-408 402H0z'/%3e%3cpath fill='url(%23paint1_linear)' d='M1378 177v223H0V217s219 75 327 52C436 246 717-35 965 45s254 144 413 132z'/%3e%3cpath fill='url(%23paint2_linear)' d='M26 400l-78-16c-170 205-44-6-137-30l-4-1 4 1 137 30c37-45 89-110 159-201 399-514-45 238 1176-50 275-65 354-39 91 267H26z'/%3e%3c/mask%3e%3cg mask='url(%23a)'%3e%3cpath fill='white' d='M0 0h1368v400H0z'/%3e%3c/g%3e%3cdefs%3e%3clinearGradient id='paint0_linear' x1='431' x2='397.3' y1='-599' y2='372.8' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint1_linear' x1='236.5' x2='446.6' y1='-586' y2='381.5' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint2_linear' x1='851.8' x2='640.4' y1='-867.2' y2='363.7' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3c/defs%3e%3c/svg%3e")`, + round: `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='1368' height='400' fill='none'%3e%3cmask id='a' width='2269' height='1408' x='-610' y='-509' maskUnits='userSpaceOnUse'%3e%3ccircle cx='1212.8' cy='74.8' r='317.5' fill='url(%23paint0_linear)' transform='rotate(-52 1213 75)'/%3e%3ccircle cx='737.8' cy='445.8' r='317.5' fill='url(%23paint1_linear)' transform='rotate(-116 738 446)'/%3e%3ccircle cx='601.8' cy='52.8' r='418.6' fill='url(%23paint2_linear)' transform='rotate(-117 602 53)'/%3e%3ccircle cx='999.8' cy='364' r='389.1' fill='url(%23paint3_linear)' transform='rotate(31 1000 364)'/%3e%3cellipse cx='-109.2' cy='263.5' fill='url(%23paint4_linear)' rx='429.2' ry='465.8' transform='rotate(-85 -109 264)'/%3e%3c/mask%3e%3cg mask='url(%23a)'%3e%3cpath fill='white' d='M0 0h1368v400H0z'/%3e%3c/g%3e%3cdefs%3e%3clinearGradient id='paint0_linear' x1='1301.2' x2='161.4' y1='-1879.7' y2='-969.6' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint1_linear' x1='826.2' x2='-313.6' y1='-1508.7' y2='-598.6' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint2_linear' x1='718.4' x2='-784.3' y1='-2524' y2='-1324.2' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint3_linear' x1='1108.2' x2='-288.6' y1='-2031.1' y2='-915.9' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint4_linear' x1='10.4' x2='-1626.5' y1='-2603.8' y2='-1399.5' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3c/defs%3e%3c/svg%3e")`, +}; + +export const colorVariants: Record = { + darkGrey: ['#171717', '#383838'], + marineBlue: ['#006D8F', '#0049A1'], + veryBlue: ['#0027AF', '#270094'], + rubyRed: ['#98002B', '#8D1134'], + toastyOrange: ['#BE2200', '#A41D00'], + purpleSky: ['#8912CA', '#3E00EA'], + eveningSea: ['#00FFF2', '#035355'], + teal: ['#005B4B'], + pinkSea: ['#C8077A', '#C2297D'], +}; + +// As the background shapes and colors are decorative, we place them onto +// the page as a css background-image instead of an html element of its own. +// Utility to not have to write colors and shapes twice. +export function genPageTheme(colors: string[], shape: string): PageTheme { + const gradientColors = colors.length === 1 ? [colors[0], colors[0]] : colors; + const gradient = `linear-gradient(90deg, ${gradientColors.join(', ')})`; + const backgroundImage = `${shape}, ${gradient}`; + + return { colors, shape, backgroundImage }; +} + +export const pageTheme: Record = { + home: genPageTheme(colorVariants.teal, shapes.wave), + documentation: genPageTheme(colorVariants.pinkSea, shapes.wave2), + tool: genPageTheme(colorVariants.purpleSky, shapes.round), + service: genPageTheme(colorVariants.marineBlue, shapes.wave), + website: genPageTheme(colorVariants.veryBlue, shapes.wave), + library: genPageTheme(colorVariants.rubyRed, shapes.wave), + other: genPageTheme(colorVariants.darkGrey, shapes.wave), + app: genPageTheme(colorVariants.toastyOrange, shapes.wave), +}; diff --git a/packages/theme/src/themes.ts b/packages/theme/src/themes.ts index 0ec3304ac5..78a68c8f13 100644 --- a/packages/theme/src/themes.ts +++ b/packages/theme/src/themes.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { createTheme } from './baseTheme'; +import { createTheme, createPageTheme } from './baseTheme'; +import { pageTheme } from './pageTheme'; import { yellow } from '@material-ui/core/colors'; export const lightTheme = createTheme({ @@ -132,3 +133,8 @@ export const darkTheme = createTheme({ }, }, }); + +export const customPageTheme = createPageTheme({ + pageTheme, + palette: lightTheme.palette, +}); diff --git a/packages/theme/src/types.ts b/packages/theme/src/types.ts index 1d244f8cd4..3d3943392a 100644 --- a/packages/theme/src/types.ts +++ b/packages/theme/src/types.ts @@ -87,3 +87,24 @@ export type SimpleThemeOptions = { palette: BackstagePaletteOptions; fontFamily?: string; }; + +export type PageTheme = { + colors: string[]; + shape: string; + backgroundImage: string; +}; + +export type BackstagePage = Record; + +export interface BackstagePageThemeOptions extends SimpleThemeOptions { + pageTheme: BackstagePage; +} + +export interface BackstagePageTheme { + pageTheme: BackstagePage; + baseTheme: BackstageTheme; +} + +export interface BackstagePageThemeConsumer extends BackstageTheme { + pageTheme: PageTheme; +} From 26cfd3e2ca28b200a6f0eddcb567db8a73e4d48a Mon Sep 17 00:00:00 2001 From: Marvin9 Date: Sun, 4 Oct 2020 17:58:09 +0530 Subject: [PATCH 002/139] feat: remove old PageTheme provider & add new implemented functionality to provide theme[using ThemeProvider] --- packages/core/src/layout/Page/Page.stories.tsx | 6 +++--- packages/core/src/layout/Page/Page.tsx | 17 +++++++++-------- packages/core/src/layout/Page/index.ts | 2 -- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/core/src/layout/Page/Page.stories.tsx b/packages/core/src/layout/Page/Page.stories.tsx index 8fd779196d..427dc644d9 100644 --- a/packages/core/src/layout/Page/Page.stories.tsx +++ b/packages/core/src/layout/Page/Page.stories.tsx @@ -15,13 +15,13 @@ */ import React, { useState } from 'react'; +import { customPageTheme } from '@backstage/theme'; import { Header, Page, HeaderLabel, ContentHeader, Content, - pageTheme, InfoCard, HeaderTabs, } from '../'; @@ -196,7 +196,7 @@ export const PluginWithData = () => { const [selectedTab, setSelectedTab] = useState(2); return (
- + { export const PluginWithTable = () => { return (
- + diff --git a/packages/core/src/layout/Page/Page.tsx b/packages/core/src/layout/Page/Page.tsx index a5f5e4fd2b..f4dc4888ed 100644 --- a/packages/core/src/layout/Page/Page.tsx +++ b/packages/core/src/layout/Page/Page.tsx @@ -15,10 +15,8 @@ */ import React, { FC } from 'react'; -import { PageTheme, pageTheme } from './PageThemeProvider'; -import { makeStyles } from '@material-ui/core'; - -export const PageThemeContext = React.createContext(pageTheme.home); +import { customPageTheme, PageTheme } from '@backstage/theme'; +import { makeStyles, ThemeProvider } from '@material-ui/core'; const useStyles = makeStyles(() => ({ root: { @@ -32,14 +30,17 @@ const useStyles = makeStyles(() => ({ })); type Props = { - theme?: PageTheme; + pageTheme?: PageTheme; }; -export const Page: FC = ({ theme = pageTheme.home, children }) => { +export const Page: FC = ({ + pageTheme = customPageTheme.pageTheme.home, + children, +}) => { const classes = useStyles(); return ( - +
{children}
-
+ ); }; diff --git a/packages/core/src/layout/Page/index.ts b/packages/core/src/layout/Page/index.ts index 000a565c83..987aee5cdb 100644 --- a/packages/core/src/layout/Page/index.ts +++ b/packages/core/src/layout/Page/index.ts @@ -15,5 +15,3 @@ */ export { Page } from './Page'; -export { pageTheme } from './PageThemeProvider'; -export type { PageTheme } from './PageThemeProvider'; From 317bb8b6543cbc3b66dc68587d3cb636647d2f88 Mon Sep 17 00:00:00 2001 From: Marvin9 Date: Sun, 4 Oct 2020 17:59:21 +0530 Subject: [PATCH 003/139] chore: remove old Page theme Provider file --- .../core/src/layout/Page/PageThemeProvider.ts | 72 ------------------- 1 file changed, 72 deletions(-) delete mode 100644 packages/core/src/layout/Page/PageThemeProvider.ts diff --git a/packages/core/src/layout/Page/PageThemeProvider.ts b/packages/core/src/layout/Page/PageThemeProvider.ts deleted file mode 100644 index 57411c8786..0000000000 --- a/packages/core/src/layout/Page/PageThemeProvider.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* - * 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 type PageTheme = { - colors: string[]; - shape: string; - backgroundImage: string; -}; - -/* - # How to add a shape - 1. Get the svg shape from figma, should be ~1400 wide, ~400 high - and only the white->transparent mask, no colors. - 2. Run it through https://jakearchibald.github.io/svgomg/ - 3. Run that through https://github.com/tigt/mini-svg-data-uri - with something like https://npm.runkit.com/mini-svg-data-uri - 4. Wrap the output in `url("")` - 5. Give it a name and paste it into the `shapes` object below. - -*/ -export const shapes: Record = { - wave: `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='1368' height='400' fill='none'%3e%3cmask id='a' width='1368' height='401' x='0' y='0' maskUnits='userSpaceOnUse'%3e%3cpath fill='url(%23paint0_linear)' d='M437 116C223 116 112 0 112 0h1256v400c-82 0-225-21-282-109-112-175-436-175-649-175z'/%3e%3cpath fill='url(%23paint1_linear)' d='M1368 400V282C891-29 788 40 711 161 608 324 121 372 0 361v39h1368z'/%3e%3cpath fill='url(%23paint2_linear)' d='M1368 244v156H0V94c92-24 198-46 375 0l135 41c176 51 195 109 858 109z'/%3e%3cpath fill='url(%23paint3_linear)' d='M1252 400h116c-14-7-35-14-116-16-663-14-837-128-1013-258l-85-61C98 28 46 8 0 0v400h1252z'/%3e%3c/mask%3e%3cg mask='url(%23a)'%3e%3cpath fill='white' d='M-172-98h1671v601H-172z'/%3e%3c/g%3e%3cdefs%3e%3clinearGradient id='paint0_linear' x1='602' x2='1093.5' y1='-960.5' y2='272' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint1_linear' x1='482' x2='480' y1='1058.5' y2='70.5' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint2_linear' x1='424' x2='446.1' y1='-587.5' y2='274.6' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint3_linear' x1='587' x2='349' y1='-1120.5' y2='341' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3c/defs%3e%3c/svg%3e")`, - wave2: `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='1368' height='400' fill='none'%3e%3cmask id='a' width='1764' height='479' x='-229' y='-6' maskUnits='userSpaceOnUse'%3e%3cpath fill='url(%23paint0_linear)' d='M0 400h1350C1321 336 525 33 179-2c-345-34-395 236-408 402H0z'/%3e%3cpath fill='url(%23paint1_linear)' d='M1378 177v223H0V217s219 75 327 52C436 246 717-35 965 45s254 144 413 132z'/%3e%3cpath fill='url(%23paint2_linear)' d='M26 400l-78-16c-170 205-44-6-137-30l-4-1 4 1 137 30c37-45 89-110 159-201 399-514-45 238 1176-50 275-65 354-39 91 267H26z'/%3e%3c/mask%3e%3cg mask='url(%23a)'%3e%3cpath fill='white' d='M0 0h1368v400H0z'/%3e%3c/g%3e%3cdefs%3e%3clinearGradient id='paint0_linear' x1='431' x2='397.3' y1='-599' y2='372.8' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint1_linear' x1='236.5' x2='446.6' y1='-586' y2='381.5' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint2_linear' x1='851.8' x2='640.4' y1='-867.2' y2='363.7' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3c/defs%3e%3c/svg%3e")`, - round: `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='1368' height='400' fill='none'%3e%3cmask id='a' width='2269' height='1408' x='-610' y='-509' maskUnits='userSpaceOnUse'%3e%3ccircle cx='1212.8' cy='74.8' r='317.5' fill='url(%23paint0_linear)' transform='rotate(-52 1213 75)'/%3e%3ccircle cx='737.8' cy='445.8' r='317.5' fill='url(%23paint1_linear)' transform='rotate(-116 738 446)'/%3e%3ccircle cx='601.8' cy='52.8' r='418.6' fill='url(%23paint2_linear)' transform='rotate(-117 602 53)'/%3e%3ccircle cx='999.8' cy='364' r='389.1' fill='url(%23paint3_linear)' transform='rotate(31 1000 364)'/%3e%3cellipse cx='-109.2' cy='263.5' fill='url(%23paint4_linear)' rx='429.2' ry='465.8' transform='rotate(-85 -109 264)'/%3e%3c/mask%3e%3cg mask='url(%23a)'%3e%3cpath fill='white' d='M0 0h1368v400H0z'/%3e%3c/g%3e%3cdefs%3e%3clinearGradient id='paint0_linear' x1='1301.2' x2='161.4' y1='-1879.7' y2='-969.6' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint1_linear' x1='826.2' x2='-313.6' y1='-1508.7' y2='-598.6' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint2_linear' x1='718.4' x2='-784.3' y1='-2524' y2='-1324.2' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint3_linear' x1='1108.2' x2='-288.6' y1='-2031.1' y2='-915.9' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint4_linear' x1='10.4' x2='-1626.5' y1='-2603.8' y2='-1399.5' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3c/defs%3e%3c/svg%3e")`, -}; - -export const colorVariants: Record = { - darkGrey: ['#171717', '#383838'], - marineBlue: ['#006D8F', '#0049A1'], - veryBlue: ['#0027AF', '#270094'], - rubyRed: ['#98002B', '#8D1134'], - toastyOrange: ['#BE2200', '#A41D00'], - purpleSky: ['#8912CA', '#3E00EA'], - eveningSea: ['#00FFF2', '#035355'], - teal: ['#005B4B'], - pinkSea: ['#C8077A', '#C2297D'], -}; - -export const pageTheme: Record = { - home: genTheme(colorVariants.teal, shapes.wave), - documentation: genTheme(colorVariants.pinkSea, shapes.wave2), - tool: genTheme(colorVariants.purpleSky, shapes.round), - service: genTheme(colorVariants.marineBlue, shapes.wave), - website: genTheme(colorVariants.veryBlue, shapes.wave), - library: genTheme(colorVariants.rubyRed, shapes.wave), - other: genTheme(colorVariants.darkGrey, shapes.wave), - app: genTheme(colorVariants.toastyOrange, shapes.wave), -}; - -// As the background shapes and colors are decorative, we place them onto -// the page as a css background-image instead of an html element of its own. -// Utility to not have to write colors and shapes twice. -function genTheme(colors: string[], shape: string) { - const gradientColors = colors.length === 1 ? [colors[0], colors[0]] : colors; - const gradient = `linear-gradient(90deg, ${gradientColors.join(', ')})`; - const backgroundImage = `${shape}, ${gradient}`; - - return { colors, shape, backgroundImage }; -} From 3a82aaef7ab693bc02f1bc5911c4d955713f51f9 Mon Sep 17 00:00:00 2001 From: Marvin9 Date: Sun, 4 Oct 2020 18:02:00 +0530 Subject: [PATCH 004/139] feat: Add new theme consumer in Header [use useTheme consumer provided in material-ui] --- .../core/src/layout/Header/Header.stories.tsx | 19 ++++++++++--------- packages/core/src/layout/Header/Header.tsx | 14 +++++++------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/packages/core/src/layout/Header/Header.stories.tsx b/packages/core/src/layout/Header/Header.stories.tsx index 39b9133c0a..cc0276224a 100644 --- a/packages/core/src/layout/Header/Header.stories.tsx +++ b/packages/core/src/layout/Header/Header.stories.tsx @@ -13,10 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { customPageTheme } from '@backstage/theme'; import React from 'react'; import { Header } from '.'; import { HeaderLabel } from '../HeaderLabel'; -import { Page, pageTheme } from '../Page'; +import { Page } from '../Page'; export default { title: 'Header', @@ -32,7 +33,7 @@ const labels = ( ); export const Home = () => ( - +
{labels}
@@ -46,7 +47,7 @@ export const HomeWithSubtitle = () => ( ); export const Tool = () => ( - +
{labels}
@@ -54,7 +55,7 @@ export const Tool = () => ( ); export const Service = () => ( - +
{labels}
@@ -62,7 +63,7 @@ export const Service = () => ( ); export const Website = () => ( - +
{labels}
@@ -70,7 +71,7 @@ export const Website = () => ( ); export const Library = () => ( - +
{labels}
@@ -78,7 +79,7 @@ export const Library = () => ( ); export const App = () => ( - +
{labels}
@@ -86,7 +87,7 @@ export const App = () => ( ); export const Documentation = () => ( - +
{labels}
@@ -94,7 +95,7 @@ export const Documentation = () => ( ); export const Other = () => ( - +
{labels}
diff --git a/packages/core/src/layout/Header/Header.tsx b/packages/core/src/layout/Header/Header.tsx index 6b9ea05245..dc71fe333b 100644 --- a/packages/core/src/layout/Header/Header.tsx +++ b/packages/core/src/layout/Header/Header.tsx @@ -14,12 +14,10 @@ * limitations under the License. */ -import React, { ReactNode, CSSProperties, FC, useContext } from 'react'; +import React, { ReactNode, CSSProperties, FC } from 'react'; import { Helmet } from 'react-helmet'; -import { Typography, Tooltip, makeStyles } from '@material-ui/core'; -import { BackstageTheme } from '@backstage/theme'; - -import { PageThemeContext } from '../Page/Page'; +import { Typography, Tooltip, makeStyles, useTheme } from '@material-ui/core'; +import { BackstageTheme, BackstagePageThemeConsumer } from '@backstage/theme'; const useStyles = makeStyles( theme => ({ @@ -163,8 +161,10 @@ export const Header: FC = ({ type, typeLink, }) => { - const theme = useContext(PageThemeContext); - const classes = useStyles({ backgroundImage: theme.backgroundImage }); + const theme = useTheme(); + const classes = useStyles({ + backgroundImage: theme.pageTheme.backgroundImage, + }); const documentTitle = pageTitleOverride || title; const pageTitle = title || pageTitleOverride; const titleTemplate = `${documentTitle} | %s | Backstage`; From e2564645e5751d684b3b528e90011518eee6c376 Mon Sep 17 00:00:00 2001 From: Marvin9 Date: Sun, 4 Oct 2020 18:03:50 +0530 Subject: [PATCH 005/139] feat: Replace old page theme props with new ones --- .../components/ApiEntityPage/ApiEntityPage.tsx | 8 ++++---- .../ApiExplorerPage/ApiExplorerLayout.tsx | 5 +++-- .../src/components/CatalogPage/CatalogLayout.tsx | 4 ++-- .../EntityPageLayout/EntityPageLayout.tsx | 15 ++++----------- .../AlertInstructionsLayout.tsx | 5 +++-- .../CostInsightsLayout/CostInsightsLayout.tsx | 6 +++--- .../explore/src/components/ExplorePluginPage.tsx | 5 ++--- .../components/NewProjectPage/NewProjectPage.tsx | 4 ++-- .../ProjectDetailsPage/ProjectDetailsPage.tsx | 5 ++--- .../ProjectListPage/ProjectListPage.tsx | 5 ++--- .../src/components/ClusterList/ClusterList.tsx | 4 ++-- .../src/components/ClusterPage/ClusterPage.tsx | 5 ++--- .../components/ProfileCatalog/ProfileCatalog.tsx | 4 ++-- .../src/components/GraphiQLPage/GraphiQLPage.tsx | 5 ++--- .../KubernetesContent/KubernetesContent.tsx | 4 ++-- .../lighthouse/src/components/AuditList/index.tsx | 5 ++--- .../lighthouse/src/components/AuditView/index.tsx | 4 ++-- .../src/components/CreateAudit/index.tsx | 4 ++-- .../NewRelicComponent/NewRelicComponent.tsx | 4 ++-- .../RegisterComponentPage.tsx | 4 ++-- .../src/components/RollbarHome/RollbarHome.tsx | 5 +++-- .../RollbarProjectPage/RollbarProjectPage.tsx | 5 +++-- .../components/ScaffolderPage/ScaffolderPage.tsx | 4 ++-- .../src/components/TemplateCard/TemplateCard.tsx | 6 ++++-- .../src/components/TemplatePage/TemplatePage.tsx | 4 ++-- .../SentryPluginPage/SentryPluginPage.tsx | 4 ++-- plugins/tech-radar/src/components/RadarPage.tsx | 4 ++-- .../src/reader/components/TechDocsHome.tsx | 8 ++++---- .../src/reader/components/TechDocsPage.tsx | 5 +++-- .../src/components/WelcomePage/WelcomePage.tsx | 4 ++-- 30 files changed, 74 insertions(+), 80 deletions(-) diff --git a/plugins/api-docs/src/components/ApiEntityPage/ApiEntityPage.tsx b/plugins/api-docs/src/components/ApiEntityPage/ApiEntityPage.tsx index b7e9996cc0..edfc3ab08b 100644 --- a/plugins/api-docs/src/components/ApiEntityPage/ApiEntityPage.tsx +++ b/plugins/api-docs/src/components/ApiEntityPage/ApiEntityPage.tsx @@ -20,11 +20,10 @@ import { errorApiRef, Header, Page, - pageTheme, - PageTheme, Progress, useApi, } from '@backstage/core'; +import { customPageTheme, PageTheme } from '@backstage/theme'; import { catalogApiRef } from '@backstage/plugin-catalog'; import { Box } from '@material-ui/core'; import { Alert } from '@material-ui/lab'; @@ -54,9 +53,10 @@ function headerProps( }; } +// REUSABLE IN plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx export const getPageTheme = (entity?: Entity): PageTheme => { const themeKey = entity?.spec?.type?.toString() ?? 'home'; - return pageTheme[themeKey] ?? pageTheme.home; + return customPageTheme.pageTheme[themeKey] ?? customPageTheme.pageTheme.home; }; type EntityPageTitleProps = { @@ -107,7 +107,7 @@ export const ApiEntityPage = () => { ); return ( - +
} pageTitleOverride={headerTitle} diff --git a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerLayout.tsx b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerLayout.tsx index a111007278..53b50f1826 100644 --- a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerLayout.tsx +++ b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerLayout.tsx @@ -14,7 +14,8 @@ * limitations under the License. */ -import { Header, Page, pageTheme } from '@backstage/core'; +import { Header, Page } from '@backstage/core'; +import { customPageTheme } from '@backstage/theme'; import React from 'react'; type Props = { @@ -23,7 +24,7 @@ type Props = { export const ApiExplorerLayout = ({ children }: Props) => { return ( - +
{ const userId = useApi(identityApiRef).getUserId(); return ( - +
{ const themeKey = entity?.spec?.type?.toString() ?? 'home'; - return pageTheme[themeKey] ?? pageTheme.home; + return customPageTheme.pageTheme[themeKey] ?? customPageTheme.pageTheme.home; }; const EntityPageTitle = ({ @@ -100,7 +93,7 @@ export const EntityPageLayout = ({ const showRemovalDialog = () => setConfirmationDialogOpen(true); return ( - +
} pageTitleOverride={headerTitle} diff --git a/plugins/cost-insights/src/components/AlertInstructionsLayout/AlertInstructionsLayout.tsx b/plugins/cost-insights/src/components/AlertInstructionsLayout/AlertInstructionsLayout.tsx index 0a422c335a..c0777fc844 100644 --- a/plugins/cost-insights/src/components/AlertInstructionsLayout/AlertInstructionsLayout.tsx +++ b/plugins/cost-insights/src/components/AlertInstructionsLayout/AlertInstructionsLayout.tsx @@ -14,10 +14,11 @@ * limitations under the License. */ +import { customPageTheme } from '@backstage/theme'; import React, { ReactNode } from 'react'; import { Box, Button, Container, makeStyles } from '@material-ui/core'; import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'; -import { Header, Page, pageTheme } from '@backstage/core'; +import { Header, Page } from '@backstage/core'; import { CostInsightsThemeProvider } from '../CostInsightsPage/CostInsightsThemeProvider'; const useStyles = makeStyles(theme => ({ @@ -39,7 +40,7 @@ const AlertInstructionsLayout = ({ const classes = useStyles(); return ( - +
diff --git a/plugins/cost-insights/src/components/CostInsightsLayout/CostInsightsLayout.tsx b/plugins/cost-insights/src/components/CostInsightsLayout/CostInsightsLayout.tsx index 61bf7ac22f..c95f624bbe 100644 --- a/plugins/cost-insights/src/components/CostInsightsLayout/CostInsightsLayout.tsx +++ b/plugins/cost-insights/src/components/CostInsightsLayout/CostInsightsLayout.tsx @@ -13,10 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - +import { customPageTheme } from '@backstage/theme'; import React from 'react'; import { makeStyles } from '@material-ui/core'; -import { Header, Page, pageTheme } from '@backstage/core'; +import { Header, Page } from '@backstage/core'; import { Group } from '../../types'; import CostInsightsTabs from '../CostInsightsTabs'; @@ -41,7 +41,7 @@ type CostInsightsLayoutProps = { const CostInsightsLayout = ({ groups, children }: CostInsightsLayoutProps) => { const classes = useStyles(); return ( - +
(theme => ({ container: { @@ -117,7 +116,7 @@ const toolsCards = [ export const ExplorePluginPage = () => { const classes = useStyles(); return ( - +
{ return ( - +
{labels}
diff --git a/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx b/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx index 3dfb4e886b..e310c27c68 100644 --- a/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx +++ b/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx @@ -13,14 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - +import { customPageTheme } from '@backstage/theme'; import { Content, ContentHeader, Header, HeaderLabel, Page, - pageTheme, SupportButton, useApi, WarningPanel, @@ -149,7 +148,7 @@ const labels = ( export const ProjectDetailsPage = () => { return ( - +
{labels}
diff --git a/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx b/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx index 542d03d5fd..5a404dbb01 100644 --- a/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx +++ b/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx @@ -15,7 +15,7 @@ */ // NEEDS WORK - +import { customPageTheme } from '@backstage/theme'; import { Content, ContentHeader, @@ -23,7 +23,6 @@ import { HeaderLabel, Link, Page, - pageTheme, SupportButton, useApi, WarningPanel, @@ -136,7 +135,7 @@ const PageContents = () => { export const ProjectListPage = () => { return ( - +
{labels}
diff --git a/plugins/gitops-profiles/src/components/ClusterList/ClusterList.tsx b/plugins/gitops-profiles/src/components/ClusterList/ClusterList.tsx index baab018415..8926ed3260 100644 --- a/plugins/gitops-profiles/src/components/ClusterList/ClusterList.tsx +++ b/plugins/gitops-profiles/src/components/ClusterList/ClusterList.tsx @@ -14,6 +14,7 @@ * limitations under the License. */ +import { customPageTheme } from '@backstage/theme'; import React, { FC, useState } from 'react'; import { Content, @@ -21,7 +22,6 @@ import { Header, SupportButton, Page, - pageTheme, Progress, HeaderLabel, useApi, @@ -91,7 +91,7 @@ const ClusterList: FC<{}> = () => { } return ( - +
diff --git a/plugins/gitops-profiles/src/components/ClusterPage/ClusterPage.tsx b/plugins/gitops-profiles/src/components/ClusterPage/ClusterPage.tsx index b2d62e8650..d51fb7656e 100644 --- a/plugins/gitops-profiles/src/components/ClusterPage/ClusterPage.tsx +++ b/plugins/gitops-profiles/src/components/ClusterPage/ClusterPage.tsx @@ -13,13 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - +import { customPageTheme } from '@backstage/theme'; import React, { FC, useEffect, useState } from 'react'; import { Content, Header, Page, - pageTheme, Table, Progress, HeaderLabel, @@ -85,7 +84,7 @@ const ClusterPage: FC<{}> = () => { }, [pollingLog, api, params, githubAuth, githubAccessToken, githubUsername]); return ( - +
diff --git a/plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.tsx b/plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.tsx index a5125fb006..078e1b7601 100644 --- a/plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.tsx +++ b/plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.tsx @@ -14,11 +14,11 @@ * limitations under the License. */ +import { customPageTheme } from '@backstage/theme'; import React, { FC, useEffect, useState } from 'react'; import { Header, Page, - pageTheme, Content, ContentHeader, HeaderLabel, @@ -259,7 +259,7 @@ const ProfileCatalog: FC<{}> = () => { ]; return ( - +
{ } return ( - +
diff --git a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx index 6119884c43..381b942f3d 100644 --- a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx +++ b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx @@ -14,13 +14,13 @@ * limitations under the License. */ +import { customPageTheme } from '@backstage/theme'; import React, { ReactElement, useEffect, useState } from 'react'; import { Grid, TabProps } from '@material-ui/core'; import { CardTab, Content, Page, - pageTheme, Progress, TabbedCard, useApi, @@ -120,7 +120,7 @@ export const KubernetesContent = ({ entity }: KubernetesContentProps) => { kubernetesObjects?.items.filter(r => r.errors.length > 0) ?? []; return ( - + {kubernetesObjects === undefined && error === undefined && ( diff --git a/plugins/lighthouse/src/components/AuditList/index.tsx b/plugins/lighthouse/src/components/AuditList/index.tsx index a75bb25a0d..9c38e27fe3 100644 --- a/plugins/lighthouse/src/components/AuditList/index.tsx +++ b/plugins/lighthouse/src/components/AuditList/index.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - +import { customPageTheme } from '@backstage/theme'; import React, { useState, useMemo, FC, ReactNode } from 'react'; import { useLocalStorage, useAsync } from 'react-use'; import { useNavigate } from 'react-router-dom'; @@ -28,7 +28,6 @@ import { ContentHeader, HeaderLabel, Progress, - pageTheme, useApi, } from '@backstage/core'; @@ -95,7 +94,7 @@ const AuditList: FC<{}> = () => { } return ( - +
= () => { } return ( - +
= () => { ]); return ( - +
= () => ( - +
diff --git a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx index 933b67060a..5e4da3fc35 100644 --- a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx +++ b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx @@ -14,12 +14,12 @@ * limitations under the License. */ +import { customPageTheme } from '@backstage/theme'; import React, { useState } from 'react'; import { Grid, makeStyles } from '@material-ui/core'; import { InfoCard, Page, - pageTheme, Content, useApi, errorApiRef, @@ -113,7 +113,7 @@ export const RegisterComponentPage = ({ }; return ( - +
diff --git a/plugins/rollbar/src/components/RollbarHome/RollbarHome.tsx b/plugins/rollbar/src/components/RollbarHome/RollbarHome.tsx index d09c2054b1..9f0deca967 100644 --- a/plugins/rollbar/src/components/RollbarHome/RollbarHome.tsx +++ b/plugins/rollbar/src/components/RollbarHome/RollbarHome.tsx @@ -14,8 +14,9 @@ * limitations under the License. */ +import { customPageTheme } from '@backstage/theme'; import React from 'react'; -import { Content, Header, Page, pageTheme } from '@backstage/core'; +import { Content, Header, Page } from '@backstage/core'; import { RollbarProjectTable } from '../RollbarProjectTable/RollbarProjectTable'; import { useRollbarEntities } from '../../hooks/useRollbarEntities'; @@ -23,7 +24,7 @@ export const RollbarHome = () => { const { entities, loading, error } = useRollbarEntities(); return ( - +
{ const { entity } = useCatalogEntity(); return ( - +
diff --git a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx index b5bdaaef4a..1bd43ded44 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx @@ -14,6 +14,7 @@ * limitations under the License. */ +import { customPageTheme } from '@backstage/theme'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { Content, @@ -22,7 +23,6 @@ import { Header, Lifecycle, Page, - pageTheme, Progress, SupportButton, useApi, @@ -66,7 +66,7 @@ export const ScaffolderPage = () => { }, [error, errorApi]); return ( - +
{ - const theme = pageTheme[type] ?? pageTheme.other; + const theme = + customPageTheme.pageTheme[type] ?? customPageTheme.pageTheme.other; const classes = useStyles({ backgroundImage: theme.backgroundImage }); const href = generatePath(templateRoute.path, { templateName: name }); diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index e68c795bd6..5067f7963d 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { customPageTheme } from '@backstage/theme'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { Content, @@ -22,7 +23,6 @@ import { Lifecycle, Page, useApi, - pageTheme, } from '@backstage/core'; import { catalogApiRef } from '@backstage/plugin-catalog'; import { LinearProgress } from '@material-ui/core'; @@ -145,7 +145,7 @@ export const TemplatePage = () => { } return ( - +
= () => { const sentryProjectId = 'sample-sentry-project-id'; return ( - +
diff --git a/plugins/tech-radar/src/components/RadarPage.tsx b/plugins/tech-radar/src/components/RadarPage.tsx index 519c9afeee..58dcaec9af 100644 --- a/plugins/tech-radar/src/components/RadarPage.tsx +++ b/plugins/tech-radar/src/components/RadarPage.tsx @@ -14,6 +14,7 @@ * limitations under the License. */ +import { customPageTheme } from '@backstage/theme'; import React from 'react'; import { Grid } from '@material-ui/core'; import { @@ -23,7 +24,6 @@ import { Header, HeaderLabel, SupportButton, - pageTheme, } from '@backstage/core'; import RadarComponent from '../components/RadarComponent'; import { TechRadarComponentProps } from '../api'; @@ -40,7 +40,7 @@ export const RadarPage = ({ pageTitle, ...props }: TechRadarPageProps): JSX.Element => ( - +
diff --git a/plugins/techdocs/src/reader/components/TechDocsHome.tsx b/plugins/techdocs/src/reader/components/TechDocsHome.tsx index c478a52127..11211e713c 100644 --- a/plugins/techdocs/src/reader/components/TechDocsHome.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsHome.tsx @@ -14,6 +14,7 @@ * limitations under the License. */ +import { customPageTheme } from '@backstage/theme'; import React from 'react'; import { useAsync } from 'react-use'; import { useNavigate, generatePath } from 'react-router-dom'; @@ -24,7 +25,6 @@ import { useApi, Content, Page, - pageTheme, Header, } from '@backstage/core'; import { catalogApiRef } from '@backstage/plugin-catalog'; @@ -43,7 +43,7 @@ export const TechDocsHome = () => { if (loading) { return ( - +
{ if (error) { return ( - +
{ } return ( - +
{ }; return ( - + { const profile = { givenName: '' }; return ( - +
Date: Sun, 4 Oct 2020 18:05:16 +0530 Subject: [PATCH 006/139] chore: update new consumer variables in example md file & update in handlebars template --- .../docs/tutorials/quickstart-app-plugin/ExampleComponent.md | 3 ++- .../src/components/ExampleComponent/ExampleComponent.tsx.hbs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/contrib/docs/tutorials/quickstart-app-plugin/ExampleComponent.md b/contrib/docs/tutorials/quickstart-app-plugin/ExampleComponent.md index 855608c8b1..ed4bfc8012 100644 --- a/contrib/docs/tutorials/quickstart-app-plugin/ExampleComponent.md +++ b/contrib/docs/tutorials/quickstart-app-plugin/ExampleComponent.md @@ -3,6 +3,7 @@ ExampleComponent.tsx reference ```tsx +import { customPageTheme } from '@backstage/theme'; import React, { FC } from 'react'; import { Typography, Grid } from '@material-ui/core'; import { @@ -25,7 +26,7 @@ const ExampleComponent: FC<{}> = () => { const profile = identityApi.getProfile(); return ( - +
= () => ( - +
From 773e521301bcbeb1958a15ef9da2f226e461372f Mon Sep 17 00:00:00 2001 From: Marvin9 Date: Sun, 4 Oct 2020 20:53:18 +0530 Subject: [PATCH 007/139] fix: e2e, storybook & unit tests Header component required ThemeProvider as wrapper to work properly --- .../ExampleComponent/ExampleComponent.tsx.hbs | 1 - .../core/src/layout/Header/Header.stories.tsx | 8 +- .../core/src/layout/Header/Header.test.tsx | 86 +++++++++++++------ 3 files changed, 67 insertions(+), 28 deletions(-) diff --git a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs index 5b15ab0c70..a050ccd2ec 100644 --- a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs +++ b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs @@ -5,7 +5,6 @@ import { InfoCard, Header, Page, - pageTheme, Content, ContentHeader, HeaderLabel, diff --git a/packages/core/src/layout/Header/Header.stories.tsx b/packages/core/src/layout/Header/Header.stories.tsx index cc0276224a..b902a709cf 100644 --- a/packages/core/src/layout/Header/Header.stories.tsx +++ b/packages/core/src/layout/Header/Header.stories.tsx @@ -41,9 +41,11 @@ export const Home = () => ( ); export const HomeWithSubtitle = () => ( -
- {labels} -
+ +
+ {labels} +
+
); export const Tool = () => ( diff --git a/packages/core/src/layout/Header/Header.test.tsx b/packages/core/src/layout/Header/Header.test.tsx index 5d28c6633f..00c5bfa2bd 100644 --- a/packages/core/src/layout/Header/Header.test.tsx +++ b/packages/core/src/layout/Header/Header.test.tsx @@ -15,8 +15,10 @@ */ import React from 'react'; -import { render } from '@testing-library/react'; +import { act, render, RenderResult } from '@testing-library/react'; import { wrapInTestApp } from '@backstage/test-utils'; +import { ThemeProvider } from '@material-ui/core'; +import { customPageTheme } from '@backstage/theme'; import { Header } from './Header'; jest.mock('react-helmet', () => { @@ -25,37 +27,73 @@ jest.mock('react-helmet', () => { }; }); +const ThemeProviderWrapper: React.FC = ({ children }) => ( + + {children} + +); + describe('
', () => { - it('should render with title', () => { - const rendered = render(wrapInTestApp(
)); - rendered.getByText('Title'); + it('should render with title', async () => { + let rendered: RenderResult | undefined; + await act(async () => { + rendered = render(wrapInTestApp(
), { + wrapper: ThemeProviderWrapper, + }); + }); + rendered?.getByText('Title'); }); - it('should set document title', () => { - const rendered = render(wrapInTestApp(
)); - rendered.getByText('Title1'); - rendered.getByText('defaultTitle: Title1 | Backstage'); + it('should set document title', async () => { + let rendered: RenderResult | undefined; + await act(async () => { + rendered = render(wrapInTestApp(
), { + wrapper: ThemeProviderWrapper, + }); + }); + rendered?.getByText('Title1'); + rendered?.getByText('defaultTitle: Title1 | Backstage'); }); - it('should override document title', () => { - const rendered = render( - wrapInTestApp(
), - ); - rendered.getByText('Title1'); - rendered.getByText('defaultTitle: Title2 | Backstage'); + it('should override document title', async () => { + let rendered: RenderResult | undefined; + await act(async () => { + rendered = render( + wrapInTestApp(
), + { + wrapper: ThemeProviderWrapper, + }, + ); + }); + rendered?.getByText('Title1'); + rendered?.getByText('defaultTitle: Title2 | Backstage'); }); - it('should have subtitle', () => { - const rendered = render( - wrapInTestApp(
), - ); - rendered.getByText('Subtitle'); + it('should have subtitle', async () => { + let rendered: RenderResult | undefined; + await act(async () => { + rendered = render( + wrapInTestApp(
), + { + wrapper: ThemeProviderWrapper, + }, + ); + }); + rendered?.getByText('Subtitle'); }); - it('should have type rendered', () => { - const rendered = render( - wrapInTestApp(
), - ); - rendered.getByText('tool'); + it('should have type rendered', async () => { + let rendered: RenderResult | undefined; + await act(async () => { + rendered = render(wrapInTestApp(
), { + wrapper: ThemeProviderWrapper, + }); + }); + rendered?.getByText('tool'); }); }); From fd842a09482439e3db3e33bc92fbecc73b77e8da Mon Sep 17 00:00:00 2001 From: Marvin9 Date: Sun, 4 Oct 2020 22:00:05 +0530 Subject: [PATCH 008/139] chore: use home as default pageTheme to reduce complexity --- .../core/src/layout/Header/Header.stories.tsx | 8 +- .../core/src/layout/Header/Header.test.tsx | 76 +++++-------------- packages/core/src/layout/Header/Header.tsx | 10 ++- 3 files changed, 30 insertions(+), 64 deletions(-) diff --git a/packages/core/src/layout/Header/Header.stories.tsx b/packages/core/src/layout/Header/Header.stories.tsx index b902a709cf..cc0276224a 100644 --- a/packages/core/src/layout/Header/Header.stories.tsx +++ b/packages/core/src/layout/Header/Header.stories.tsx @@ -41,11 +41,9 @@ export const Home = () => ( ); export const HomeWithSubtitle = () => ( - -
- {labels} -
-
+
+ {labels} +
); export const Tool = () => ( diff --git a/packages/core/src/layout/Header/Header.test.tsx b/packages/core/src/layout/Header/Header.test.tsx index 00c5bfa2bd..e5dc7fec4d 100644 --- a/packages/core/src/layout/Header/Header.test.tsx +++ b/packages/core/src/layout/Header/Header.test.tsx @@ -15,10 +15,8 @@ */ import React from 'react'; -import { act, render, RenderResult } from '@testing-library/react'; +import { render } from '@testing-library/react'; import { wrapInTestApp } from '@backstage/test-utils'; -import { ThemeProvider } from '@material-ui/core'; -import { customPageTheme } from '@backstage/theme'; import { Header } from './Header'; jest.mock('react-helmet', () => { @@ -27,73 +25,37 @@ jest.mock('react-helmet', () => { }; }); -const ThemeProviderWrapper: React.FC = ({ children }) => ( - - {children} - -); - describe('
', () => { it('should render with title', async () => { - let rendered: RenderResult | undefined; - await act(async () => { - rendered = render(wrapInTestApp(
), { - wrapper: ThemeProviderWrapper, - }); - }); - rendered?.getByText('Title'); + const rendered = render(wrapInTestApp(
)); + rendered.getByText('Title'); }); it('should set document title', async () => { - let rendered: RenderResult | undefined; - await act(async () => { - rendered = render(wrapInTestApp(
), { - wrapper: ThemeProviderWrapper, - }); - }); - rendered?.getByText('Title1'); - rendered?.getByText('defaultTitle: Title1 | Backstage'); + const rendered = render(wrapInTestApp(
)); + rendered.getByText('Title1'); + rendered.getByText('defaultTitle: Title1 | Backstage'); }); it('should override document title', async () => { - let rendered: RenderResult | undefined; - await act(async () => { - rendered = render( - wrapInTestApp(
), - { - wrapper: ThemeProviderWrapper, - }, - ); - }); - rendered?.getByText('Title1'); - rendered?.getByText('defaultTitle: Title2 | Backstage'); + const rendered = render( + wrapInTestApp(
), + ); + rendered.getByText('Title1'); + rendered.getByText('defaultTitle: Title2 | Backstage'); }); it('should have subtitle', async () => { - let rendered: RenderResult | undefined; - await act(async () => { - rendered = render( - wrapInTestApp(
), - { - wrapper: ThemeProviderWrapper, - }, - ); - }); - rendered?.getByText('Subtitle'); + const rendered = render( + wrapInTestApp(
), + ); + rendered.getByText('Subtitle'); }); it('should have type rendered', async () => { - let rendered: RenderResult | undefined; - await act(async () => { - rendered = render(wrapInTestApp(
), { - wrapper: ThemeProviderWrapper, - }); - }); - rendered?.getByText('tool'); + const rendered = render( + wrapInTestApp(
), + ); + rendered.getByText('tool'); }); }); diff --git a/packages/core/src/layout/Header/Header.tsx b/packages/core/src/layout/Header/Header.tsx index dc71fe333b..41840b6ff0 100644 --- a/packages/core/src/layout/Header/Header.tsx +++ b/packages/core/src/layout/Header/Header.tsx @@ -17,7 +17,11 @@ import React, { ReactNode, CSSProperties, FC } from 'react'; import { Helmet } from 'react-helmet'; import { Typography, Tooltip, makeStyles, useTheme } from '@material-ui/core'; -import { BackstageTheme, BackstagePageThemeConsumer } from '@backstage/theme'; +import { + BackstageTheme, + BackstagePageThemeConsumer, + customPageTheme, +} from '@backstage/theme'; const useStyles = makeStyles( theme => ({ @@ -163,7 +167,9 @@ export const Header: FC = ({ }) => { const theme = useTheme(); const classes = useStyles({ - backgroundImage: theme.pageTheme.backgroundImage, + backgroundImage: + theme.pageTheme?.backgroundImage ?? + customPageTheme.pageTheme.home.backgroundImage, }); const documentTitle = pageTitleOverride || title; const pageTitle = title || pageTitleOverride; From 837ae7719729c2efe89ea9cc58a816054ba37d62 Mon Sep 17 00:00:00 2001 From: Marvin9 Date: Sun, 11 Oct 2020 19:10:41 +0530 Subject: [PATCH 009/139] feat: strategy to add page them in top level theme provider --- packages/theme/src/baseTheme.ts | 29 ++++++++++++++--------------- packages/theme/src/index.ts | 1 + packages/theme/src/themes.ts | 10 +++------- packages/theme/src/types.ts | 24 +++++++++--------------- 4 files changed, 27 insertions(+), 37 deletions(-) diff --git a/packages/theme/src/baseTheme.ts b/packages/theme/src/baseTheme.ts index a3fded7e04..1f0e01586e 100644 --- a/packages/theme/src/baseTheme.ts +++ b/packages/theme/src/baseTheme.ts @@ -19,20 +19,28 @@ import { darken, lighten } from '@material-ui/core/styles/colorManipulator'; import { Overrides } from '@material-ui/core/styles/overrides'; import { - BackstagePageTheme, - BackstagePageThemeOptions, BackstageTheme, BackstageThemeOptions, SimpleThemeOptions, } from './types'; +import { pageTheme } from './pageTheme'; + const DEFAULT_FONT_FAMILY = '"Helvetica Neue", Helvetica, Roboto, Arial, sans-serif'; export function createThemeOptions( options: SimpleThemeOptions, ): BackstageThemeOptions { - const { palette, fontFamily = DEFAULT_FONT_FAMILY } = options; + const { + palette, + fontFamily = DEFAULT_FONT_FAMILY, + defaultPageTheme, + } = options; + + if (!pageTheme[defaultPageTheme]) { + throw new Error(`${defaultPageTheme} is not defined in pageTheme.`); + } return { palette, @@ -70,6 +78,9 @@ export function createThemeOptions( marginBottom: 10, }, }, + page: pageTheme[defaultPageTheme], + getPageTheme: ({ themeId }) => + pageTheme[themeId] ?? pageTheme[defaultPageTheme], }; } @@ -244,15 +255,3 @@ export function createTheme(options: SimpleThemeOptions): BackstageTheme { const theme = { ...baseTheme, overrides }; return theme; } - -export function createPageTheme( - options: BackstagePageThemeOptions, -): BackstagePageTheme { - const baseTheme = createTheme(options); - const pageTheme = options.pageTheme; - - return { - pageTheme, - baseTheme, - }; -} diff --git a/packages/theme/src/index.ts b/packages/theme/src/index.ts index 862f9b7755..cafad3fc7c 100644 --- a/packages/theme/src/index.ts +++ b/packages/theme/src/index.ts @@ -17,3 +17,4 @@ export * from './themes'; export * from './baseTheme'; export * from './types'; +export * from './pageTheme'; diff --git a/packages/theme/src/themes.ts b/packages/theme/src/themes.ts index 78a68c8f13..3838ed01cc 100644 --- a/packages/theme/src/themes.ts +++ b/packages/theme/src/themes.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { createTheme, createPageTheme } from './baseTheme'; -import { pageTheme } from './pageTheme'; +import { createTheme } from './baseTheme'; import { yellow } from '@material-ui/core/colors'; export const lightTheme = createTheme({ @@ -74,6 +73,7 @@ export const lightTheme = createTheme({ indicator: '#9BF0E1', }, }, + defaultPageTheme: 'home', }); export const darkTheme = createTheme({ @@ -132,9 +132,5 @@ export const darkTheme = createTheme({ indicator: '#9BF0E1', }, }, -}); - -export const customPageTheme = createPageTheme({ - pageTheme, - palette: lightTheme.palette, + defaultPageTheme: 'home', }); diff --git a/packages/theme/src/types.ts b/packages/theme/src/types.ts index 3d3943392a..b5a07e78c5 100644 --- a/packages/theme/src/types.ts +++ b/packages/theme/src/types.ts @@ -72,12 +72,20 @@ type PaletteAdditions = { export type BackstagePalette = Palette & PaletteAdditions; export type BackstagePaletteOptions = PaletteOptions & PaletteAdditions; +export type pageThemeOptions = { + themeId: string; +}; + export interface BackstageTheme extends Theme { palette: BackstagePalette; + page: PageTheme; + getPageTheme: ({ themeId }: pageThemeOptions) => PageTheme; } export interface BackstageThemeOptions extends ThemeOptions { palette: BackstagePaletteOptions; + page: PageTheme; + getPageTheme: ({ themeId }: pageThemeOptions) => PageTheme; } /** @@ -85,6 +93,7 @@ export interface BackstageThemeOptions extends ThemeOptions { */ export type SimpleThemeOptions = { palette: BackstagePaletteOptions; + defaultPageTheme: string; fontFamily?: string; }; @@ -93,18 +102,3 @@ export type PageTheme = { shape: string; backgroundImage: string; }; - -export type BackstagePage = Record; - -export interface BackstagePageThemeOptions extends SimpleThemeOptions { - pageTheme: BackstagePage; -} - -export interface BackstagePageTheme { - pageTheme: BackstagePage; - baseTheme: BackstageTheme; -} - -export interface BackstagePageThemeConsumer extends BackstageTheme { - pageTheme: PageTheme; -} From 886bff9d44a373778ea4b92fd20887d211d63488 Mon Sep 17 00:00:00 2001 From: Marvin9 Date: Sun, 11 Oct 2020 19:12:48 +0530 Subject: [PATCH 010/139] feat: update theme provider in Page component, to override page theme in children --- packages/core/src/layout/Page/Page.stories.tsx | 6 +++--- packages/core/src/layout/Page/Page.tsx | 11 ++++------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/core/src/layout/Page/Page.stories.tsx b/packages/core/src/layout/Page/Page.stories.tsx index 427dc644d9..998f799b56 100644 --- a/packages/core/src/layout/Page/Page.stories.tsx +++ b/packages/core/src/layout/Page/Page.stories.tsx @@ -15,7 +15,7 @@ */ import React, { useState } from 'react'; -import { customPageTheme } from '@backstage/theme'; +import { pageTheme } from '@backstage/theme'; import { Header, Page, @@ -196,7 +196,7 @@ export const PluginWithData = () => { const [selectedTab, setSelectedTab] = useState(2); return (
- + { export const PluginWithTable = () => { return (
- + diff --git a/packages/core/src/layout/Page/Page.tsx b/packages/core/src/layout/Page/Page.tsx index f4dc4888ed..4bc1917659 100644 --- a/packages/core/src/layout/Page/Page.tsx +++ b/packages/core/src/layout/Page/Page.tsx @@ -15,7 +15,7 @@ */ import React, { FC } from 'react'; -import { customPageTheme, PageTheme } from '@backstage/theme'; +import { PageTheme } from '@backstage/theme'; import { makeStyles, ThemeProvider } from '@material-ui/core'; const useStyles = makeStyles(() => ({ @@ -30,16 +30,13 @@ const useStyles = makeStyles(() => ({ })); type Props = { - pageTheme?: PageTheme; + theme: PageTheme; }; -export const Page: FC = ({ - pageTheme = customPageTheme.pageTheme.home, - children, -}) => { +export const Page: FC = ({ theme, children }) => { const classes = useStyles(); return ( - + ({ ...baseTheme, page: theme })}>
{children}
); From 6d68f6fec17429764cd55d0b84fcd9f1bbde9140 Mon Sep 17 00:00:00 2001 From: Marvin9 Date: Sun, 11 Oct 2020 19:14:23 +0530 Subject: [PATCH 011/139] feat: consume theme provided by Page theme provider --- .../core/src/layout/Header/Header.stories.tsx | 18 +++---- .../core/src/layout/Header/Header.test.tsx | 10 ++-- packages/core/src/layout/Header/Header.tsx | 12 ++--- .../core/src/layout/SignInPage/SignInPage.tsx | 6 ++- .../ApiEntityPage/ApiEntityPage.test.tsx | 33 +----------- .../ApiEntityPage/ApiEntityPage.tsx | 17 +++--- .../ApiExplorerPage/ApiExplorerLayout.tsx | 11 +++- .../components/CatalogPage/CatalogLayout.tsx | 6 ++- .../EntityPageLayout/EntityPageLayout.tsx | 19 ++++--- .../AlertInstructionsLayout.tsx | 17 ++++-- .../CostInsightsLayout/CostInsightsLayout.tsx | 11 ++-- .../src/components/ExplorePluginPage.tsx | 12 +++-- .../NewProjectPage/NewProjectPage.tsx | 12 +++-- .../ProjectDetailsPage/ProjectDetailsPage.tsx | 10 +++- .../ProjectListPage/ProjectListPage.tsx | 11 +++- .../components/ClusterList/ClusterList.tsx | 11 ++-- .../components/ClusterPage/ClusterPage.tsx | 11 ++-- .../ProfileCatalog/ProfileCatalog.tsx | 12 +++-- .../components/GraphiQLPage/GraphiQLPage.tsx | 11 ++-- .../KubernetesContent/KubernetesContent.tsx | 11 ++-- .../src/components/AuditList/index.tsx | 11 ++-- .../src/components/AuditView/index.tsx | 10 +++- .../src/components/CreateAudit/index.tsx | 10 +++- .../NewRelicComponent/NewRelicComponent.tsx | 48 +++++++++-------- .../RegisterComponentPage.tsx | 11 ++-- .../components/RollbarHome/RollbarHome.tsx | 10 +++- .../RollbarProjectPage/RollbarProjectPage.tsx | 10 +++- .../ScaffolderPage/ScaffolderPage.tsx | 7 +-- .../components/TemplateCard/TemplateCard.tsx | 16 ++++-- .../components/TemplatePage/TemplatePage.tsx | 7 +-- .../SentryPluginPage/SentryPluginPage.tsx | 11 ++-- .../tech-radar/src/components/RadarPage.tsx | 54 ++++++++++--------- .../src/reader/components/TechDocsHome.tsx | 23 ++++++-- .../src/reader/components/TechDocsPage.tsx | 10 +++- .../components/WelcomePage/WelcomePage.tsx | 10 +++- 35 files changed, 320 insertions(+), 189 deletions(-) diff --git a/packages/core/src/layout/Header/Header.stories.tsx b/packages/core/src/layout/Header/Header.stories.tsx index cc0276224a..a99e9e235f 100644 --- a/packages/core/src/layout/Header/Header.stories.tsx +++ b/packages/core/src/layout/Header/Header.stories.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { customPageTheme } from '@backstage/theme'; +import { pageTheme } from '@backstage/theme'; import React from 'react'; import { Header } from '.'; import { HeaderLabel } from '../HeaderLabel'; @@ -33,7 +33,7 @@ const labels = ( ); export const Home = () => ( - +
{labels}
@@ -47,7 +47,7 @@ export const HomeWithSubtitle = () => ( ); export const Tool = () => ( - +
{labels}
@@ -55,7 +55,7 @@ export const Tool = () => ( ); export const Service = () => ( - +
{labels}
@@ -63,7 +63,7 @@ export const Service = () => ( ); export const Website = () => ( - +
{labels}
@@ -71,7 +71,7 @@ export const Website = () => ( ); export const Library = () => ( - +
{labels}
@@ -79,7 +79,7 @@ export const Library = () => ( ); export const App = () => ( - +
{labels}
@@ -87,7 +87,7 @@ export const App = () => ( ); export const Documentation = () => ( - +
{labels}
@@ -95,7 +95,7 @@ export const Documentation = () => ( ); export const Other = () => ( - +
{labels}
diff --git a/packages/core/src/layout/Header/Header.test.tsx b/packages/core/src/layout/Header/Header.test.tsx index e5dc7fec4d..5d28c6633f 100644 --- a/packages/core/src/layout/Header/Header.test.tsx +++ b/packages/core/src/layout/Header/Header.test.tsx @@ -26,18 +26,18 @@ jest.mock('react-helmet', () => { }); describe('
', () => { - it('should render with title', async () => { + it('should render with title', () => { const rendered = render(wrapInTestApp(
)); rendered.getByText('Title'); }); - it('should set document title', async () => { + it('should set document title', () => { const rendered = render(wrapInTestApp(
)); rendered.getByText('Title1'); rendered.getByText('defaultTitle: Title1 | Backstage'); }); - it('should override document title', async () => { + it('should override document title', () => { const rendered = render( wrapInTestApp(
), ); @@ -45,14 +45,14 @@ describe('
', () => { rendered.getByText('defaultTitle: Title2 | Backstage'); }); - it('should have subtitle', async () => { + it('should have subtitle', () => { const rendered = render( wrapInTestApp(
), ); rendered.getByText('Subtitle'); }); - it('should have type rendered', async () => { + it('should have type rendered', () => { const rendered = render( wrapInTestApp(
), ); diff --git a/packages/core/src/layout/Header/Header.tsx b/packages/core/src/layout/Header/Header.tsx index 41840b6ff0..6fadd0c834 100644 --- a/packages/core/src/layout/Header/Header.tsx +++ b/packages/core/src/layout/Header/Header.tsx @@ -17,11 +17,7 @@ import React, { ReactNode, CSSProperties, FC } from 'react'; import { Helmet } from 'react-helmet'; import { Typography, Tooltip, makeStyles, useTheme } from '@material-ui/core'; -import { - BackstageTheme, - BackstagePageThemeConsumer, - customPageTheme, -} from '@backstage/theme'; +import { BackstageTheme } from '@backstage/theme'; const useStyles = makeStyles( theme => ({ @@ -165,11 +161,9 @@ export const Header: FC = ({ type, typeLink, }) => { - const theme = useTheme(); + const backstageTheme = useTheme(); const classes = useStyles({ - backgroundImage: - theme.pageTheme?.backgroundImage ?? - customPageTheme.pageTheme.home.backgroundImage, + backgroundImage: backstageTheme.page.backgroundImage, }); const documentTitle = pageTitleOverride || title; const pageTitle = title || pageTitleOverride; diff --git a/packages/core/src/layout/SignInPage/SignInPage.tsx b/packages/core/src/layout/SignInPage/SignInPage.tsx index 8a1ff68250..b2b049d161 100644 --- a/packages/core/src/layout/SignInPage/SignInPage.tsx +++ b/packages/core/src/layout/SignInPage/SignInPage.tsx @@ -19,7 +19,8 @@ import { Page } from '../Page'; import { Header } from '../Header'; import { Content } from '../Content/Content'; import { ContentHeader } from '../ContentHeader/ContentHeader'; -import { Grid } from '@material-ui/core'; +import { BackstageTheme } from '@backstage/theme'; +import { Grid, useTheme } from '@material-ui/core'; import { SignInPageProps, useApi, configApiRef } from '@backstage/core-api'; import { useSignInProviders, getSignInProviders } from './providers'; import { IdentityProviders } from './types'; @@ -38,6 +39,7 @@ export const SignInPage: FC = ({ title, align = 'left', }) => { + const backstageTheme = useTheme(); const configApi = useApi(configApiRef); const classes = useStyles(); @@ -52,7 +54,7 @@ export const SignInPage: FC = ({ } return ( - +
{title && } diff --git a/plugins/api-docs/src/components/ApiEntityPage/ApiEntityPage.test.tsx b/plugins/api-docs/src/components/ApiEntityPage/ApiEntityPage.test.tsx index af5213e3f1..9a2bc0cfb1 100644 --- a/plugins/api-docs/src/components/ApiEntityPage/ApiEntityPage.test.tsx +++ b/plugins/api-docs/src/components/ApiEntityPage/ApiEntityPage.test.tsx @@ -13,14 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import { Entity } from '@backstage/catalog-model'; import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core'; import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog'; import { wrapInTestApp } from '@backstage/test-utils'; import { render, waitFor } from '@testing-library/react'; import * as React from 'react'; -import { ApiEntityPage, getPageTheme } from './ApiEntityPage'; +import { ApiEntityPage } from './ApiEntityPage'; jest.mock('react-router-dom', () => { const actual = jest.requireActual('react-router-dom'); @@ -71,32 +69,3 @@ describe('ApiEntityPage', () => { ); }); }); - -describe('getPageTheme', () => { - const defaultPageTheme = getPageTheme(); - it.each(['service', 'app', 'library', 'tool', 'documentation', 'website'])( - 'should select right theme for predefined type: %p ̰ ', - type => { - const theme = getPageTheme(({ - spec: { - type, - }, - } as any) as Entity); - expect(theme).toBeDefined(); - expect(theme).not.toBe(defaultPageTheme); - }, - ); - - it('should select default theme for unknown/unspecified types', () => { - const theme1 = getPageTheme(({ - spec: { - type: 'unknown-type', - }, - } as any) as Entity); - const theme2 = getPageTheme(({ - spec: {}, - } as any) as Entity); - expect(theme1).toBe(defaultPageTheme); - expect(theme2).toBe(defaultPageTheme); - }); -}); diff --git a/plugins/api-docs/src/components/ApiEntityPage/ApiEntityPage.tsx b/plugins/api-docs/src/components/ApiEntityPage/ApiEntityPage.tsx index edfc3ab08b..85250a8569 100644 --- a/plugins/api-docs/src/components/ApiEntityPage/ApiEntityPage.tsx +++ b/plugins/api-docs/src/components/ApiEntityPage/ApiEntityPage.tsx @@ -23,9 +23,9 @@ import { Progress, useApi, } from '@backstage/core'; -import { customPageTheme, PageTheme } from '@backstage/theme'; +import { BackstageTheme } from '@backstage/theme'; import { catalogApiRef } from '@backstage/plugin-catalog'; -import { Box } from '@material-ui/core'; +import { Box, useTheme } from '@material-ui/core'; import { Alert } from '@material-ui/lab'; import React, { useEffect } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; @@ -53,12 +53,6 @@ function headerProps( }; } -// REUSABLE IN plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx -export const getPageTheme = (entity?: Entity): PageTheme => { - const themeKey = entity?.spec?.type?.toString() ?? 'home'; - return customPageTheme.pageTheme[themeKey] ?? customPageTheme.pageTheme.home; -}; - type EntityPageTitleProps = { title: string; entity: Entity | undefined; @@ -71,6 +65,7 @@ const EntityPageTitle = ({ title }: EntityPageTitleProps) => ( ); export const ApiEntityPage = () => { + const backstageTheme = useTheme(); const { optionalNamespaceAndName } = useParams() as { optionalNamespaceAndName: string; }; @@ -107,7 +102,11 @@ export const ApiEntityPage = () => { ); return ( - +
} pageTitleOverride={headerTitle} diff --git a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerLayout.tsx b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerLayout.tsx index 53b50f1826..410432d715 100644 --- a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerLayout.tsx +++ b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerLayout.tsx @@ -15,7 +15,8 @@ */ import { Header, Page } from '@backstage/core'; -import { customPageTheme } from '@backstage/theme'; +import { BackstageTheme } from '@backstage/theme'; +import { useTheme } from '@material-ui/core'; import React from 'react'; type Props = { @@ -23,8 +24,14 @@ type Props = { }; export const ApiExplorerLayout = ({ children }: Props) => { + const backstageTheme = useTheme(); + return ( - +
{ + const backstageTheme = useTheme(); const greeting = getTimeBasedGreeting(); const profile = useApi(identityApiRef).getProfile(); const userId = useApi(identityApiRef).getUserId(); return ( - +
{ - const themeKey = entity?.spec?.type?.toString() ?? 'home'; - return customPageTheme.pageTheme[themeKey] ?? customPageTheme.pageTheme.home; -}; +// const getPageTheme = (entity?: Entity): PageTheme => { +// const themeKey = entity?.spec?.type?.toString() ?? 'home'; +// return customPageTheme.pageTheme[themeKey] ?? customPageTheme.pageTheme.home; +// }; const EntityPageTitle = ({ entity, @@ -69,6 +69,7 @@ export const EntityPageLayout = ({ }: { children?: React.ReactNode; }) => { + const backstageTheme = useTheme(); const { optionalNamespaceAndName, kind } = useParams() as { optionalNamespaceAndName: string; kind: string; @@ -93,7 +94,11 @@ export const EntityPageLayout = ({ const showRemovalDialog = () => setConfirmationDialogOpen(true); return ( - +
} pageTitleOverride={headerTitle} diff --git a/plugins/cost-insights/src/components/AlertInstructionsLayout/AlertInstructionsLayout.tsx b/plugins/cost-insights/src/components/AlertInstructionsLayout/AlertInstructionsLayout.tsx index c0777fc844..d09b130b70 100644 --- a/plugins/cost-insights/src/components/AlertInstructionsLayout/AlertInstructionsLayout.tsx +++ b/plugins/cost-insights/src/components/AlertInstructionsLayout/AlertInstructionsLayout.tsx @@ -14,9 +14,15 @@ * limitations under the License. */ -import { customPageTheme } from '@backstage/theme'; +import { BackstageTheme } from '@backstage/theme'; import React, { ReactNode } from 'react'; -import { Box, Button, Container, makeStyles } from '@material-ui/core'; +import { + Box, + Button, + Container, + makeStyles, + useTheme, +} from '@material-ui/core'; import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'; import { Header, Page } from '@backstage/core'; import { CostInsightsThemeProvider } from '../CostInsightsPage/CostInsightsThemeProvider'; @@ -37,10 +43,15 @@ const AlertInstructionsLayout = ({ title, children, }: AlertInstructionsLayoutProps) => { + const backstageTheme = useTheme(); const classes = useStyles(); return ( - +
diff --git a/plugins/cost-insights/src/components/CostInsightsLayout/CostInsightsLayout.tsx b/plugins/cost-insights/src/components/CostInsightsLayout/CostInsightsLayout.tsx index c95f624bbe..068174cd68 100644 --- a/plugins/cost-insights/src/components/CostInsightsLayout/CostInsightsLayout.tsx +++ b/plugins/cost-insights/src/components/CostInsightsLayout/CostInsightsLayout.tsx @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { customPageTheme } from '@backstage/theme'; +import { BackstageTheme } from '@backstage/theme'; import React from 'react'; -import { makeStyles } from '@material-ui/core'; +import { makeStyles, useTheme } from '@material-ui/core'; import { Header, Page } from '@backstage/core'; import { Group } from '../../types'; import CostInsightsTabs from '../CostInsightsTabs'; @@ -39,9 +39,14 @@ type CostInsightsLayoutProps = { }; const CostInsightsLayout = ({ groups, children }: CostInsightsLayoutProps) => { + const backstageTheme = useTheme(); const classes = useStyles(); return ( - +
(theme => ({ container: { @@ -115,8 +115,14 @@ const toolsCards = [ export const ExplorePluginPage = () => { const classes = useStyles(); + const backstageTheme = useTheme(); + return ( - +
{ @@ -112,8 +112,14 @@ const labels = ( ); export const NewProjectPage = () => { + const backstageTheme = useTheme(); + return ( - +
{labels}
diff --git a/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx b/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx index e310c27c68..eb86b6fd03 100644 --- a/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx +++ b/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { customPageTheme } from '@backstage/theme'; +import { BackstageTheme } from '@backstage/theme'; import { Content, ContentHeader, @@ -36,6 +36,7 @@ import { TableRow, Theme, Typography, + useTheme, } from '@material-ui/core'; import React from 'react'; import { useAsync } from 'react-use'; @@ -147,8 +148,13 @@ const labels = ( ); export const ProjectDetailsPage = () => { + const backstageTheme = useTheme(); return ( - +
{labels}
diff --git a/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx b/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx index 5a404dbb01..33b3813c2a 100644 --- a/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx +++ b/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx @@ -15,7 +15,7 @@ */ // NEEDS WORK -import { customPageTheme } from '@backstage/theme'; +import { BackstageTheme } from '@backstage/theme'; import { Content, ContentHeader, @@ -38,6 +38,7 @@ import { TableRow, Tooltip, Typography, + useTheme, } from '@material-ui/core'; import React from 'react'; import { useAsync } from 'react-use'; @@ -134,8 +135,14 @@ const PageContents = () => { }; export const ProjectListPage = () => { + const backstageTheme = useTheme(); + return ( - +
{labels}
diff --git a/plugins/gitops-profiles/src/components/ClusterList/ClusterList.tsx b/plugins/gitops-profiles/src/components/ClusterList/ClusterList.tsx index 8926ed3260..3b33320abe 100644 --- a/plugins/gitops-profiles/src/components/ClusterList/ClusterList.tsx +++ b/plugins/gitops-profiles/src/components/ClusterList/ClusterList.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { customPageTheme } from '@backstage/theme'; +import { BackstageTheme } from '@backstage/theme'; import React, { FC, useState } from 'react'; import { Content, @@ -29,12 +29,13 @@ import { } from '@backstage/core'; import ClusterTable from '../ClusterTable/ClusterTable'; -import { Button } from '@material-ui/core'; +import { Button, useTheme } from '@material-ui/core'; import { useAsync } from 'react-use'; import { gitOpsApiRef } from '../../api'; import { Alert } from '@material-ui/lab'; const ClusterList: FC<{}> = () => { + const backstageTheme = useTheme(); const api = useApi(gitOpsApiRef); const githubAuth = useApi(githubAuthApiRef); const [githubUsername, setGithubUsername] = useState(String); @@ -91,7 +92,11 @@ const ClusterList: FC<{}> = () => { } return ( - +
diff --git a/plugins/gitops-profiles/src/components/ClusterPage/ClusterPage.tsx b/plugins/gitops-profiles/src/components/ClusterPage/ClusterPage.tsx index d51fb7656e..f7205c3326 100644 --- a/plugins/gitops-profiles/src/components/ClusterPage/ClusterPage.tsx +++ b/plugins/gitops-profiles/src/components/ClusterPage/ClusterPage.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { customPageTheme } from '@backstage/theme'; +import { BackstageTheme } from '@backstage/theme'; import React, { FC, useEffect, useState } from 'react'; import { Content, @@ -26,12 +26,13 @@ import { githubAuthApiRef, } from '@backstage/core'; -import { Link } from '@material-ui/core'; +import { Link, useTheme } from '@material-ui/core'; import { useParams } from 'react-router-dom'; import { gitOpsApiRef, Status } from '../../api'; import { transformRunStatus } from '../ProfileCatalog'; const ClusterPage: FC<{}> = () => { + const backstageTheme = useTheme(); const params = useParams() as { owner: string; repo: string }; const [pollingLog, setPollingLog] = useState(true); @@ -84,7 +85,11 @@ const ClusterPage: FC<{}> = () => { }, [pollingLog, api, params, githubAuth, githubAccessToken, githubUsername]); return ( - +
diff --git a/plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.tsx b/plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.tsx index 078e1b7601..fe1e97235a 100644 --- a/plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.tsx +++ b/plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { customPageTheme } from '@backstage/theme'; +import { BackstageTheme } from '@backstage/theme'; import React, { FC, useEffect, useState } from 'react'; import { Header, @@ -37,7 +37,7 @@ import { useApi, githubAuthApiRef, } from '@backstage/core'; -import { TextField, List, ListItem, Link } from '@material-ui/core'; +import { TextField, List, ListItem, Link, useTheme } from '@material-ui/core'; import ClusterTemplateCardList from '../ClusterTemplateCardList'; import ProfileCardList from '../ProfileCardList'; @@ -81,6 +81,8 @@ export const transformRunStatus = (x: Status[]) => { }; const ProfileCatalog: FC<{}> = () => { + const backstageTheme = useTheme(); + // TODO: get data from REST API const [clusterTemplates] = React.useState([ { @@ -259,7 +261,11 @@ const ProfileCatalog: FC<{}> = () => { ]; return ( - +
{ + const backstageTheme = useTheme(); const graphQlBrowseApi = useApi(graphQlBrowseApiRef); const endpoints = useAsync(() => graphQlBrowseApi.getEndpoints()); @@ -59,7 +60,11 @@ export const GraphiQLPage = () => { } return ( - +
diff --git a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx index 381b942f3d..70edc167f3 100644 --- a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx +++ b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx @@ -14,9 +14,9 @@ * limitations under the License. */ -import { customPageTheme } from '@backstage/theme'; +import { BackstageTheme } from '@backstage/theme'; import React, { ReactElement, useEffect, useState } from 'react'; -import { Grid, TabProps } from '@material-ui/core'; +import { Grid, TabProps, useTheme } from '@material-ui/core'; import { CardTab, Content, @@ -98,6 +98,7 @@ const groupResponses = (fetchResponse: FetchResponse[]) => { type KubernetesContentProps = { entity: Entity; children?: React.ReactNode }; export const KubernetesContent = ({ entity }: KubernetesContentProps) => { + const backstageTheme = useTheme(); const kubernetesApi = useApi(kubernetesApiRef); const [kubernetesObjects, setKubernetesObjects] = useState< @@ -120,7 +121,11 @@ export const KubernetesContent = ({ entity }: KubernetesContentProps) => { kubernetesObjects?.items.filter(r => r.errors.length > 0) ?? []; return ( - + {kubernetesObjects === undefined && error === undefined && ( diff --git a/plugins/lighthouse/src/components/AuditList/index.tsx b/plugins/lighthouse/src/components/AuditList/index.tsx index 9c38e27fe3..cae1761ecb 100644 --- a/plugins/lighthouse/src/components/AuditList/index.tsx +++ b/plugins/lighthouse/src/components/AuditList/index.tsx @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { customPageTheme } from '@backstage/theme'; +import { BackstageTheme } from '@backstage/theme'; import React, { useState, useMemo, FC, ReactNode } from 'react'; import { useLocalStorage, useAsync } from 'react-use'; import { useNavigate } from 'react-router-dom'; -import { Grid, Button } from '@material-ui/core'; +import { Grid, Button, useTheme } from '@material-ui/core'; import Alert from '@material-ui/lab/Alert'; import Pagination from '@material-ui/lab/Pagination'; import { @@ -41,6 +41,7 @@ import { createAuditRouteRef } from '../../plugin'; export const LIMIT = 10; const AuditList: FC<{}> = () => { + const backstageTheme = useTheme(); const [dismissedStored] = useLocalStorage(LIGHTHOUSE_INTRO_LOCAL_STORAGE); const [dismissed, setDismissed] = useState(dismissedStored); @@ -94,7 +95,11 @@ const AuditList: FC<{}> = () => { } return ( - +
= ({ audit }: { audit?: Audit }) => { }; const ConnectedAuditView: FC<{}> = () => { + const backstageTheme = useTheme(); const lighthouseApi = useApi(lighthouseApiRef); const params = useParams() as { id: string }; const classes = useStyles(); @@ -173,7 +175,11 @@ const ConnectedAuditView: FC<{}> = () => { } return ( - +
({ })); const CreateAudit: FC<{}> = () => { + const backstageTheme = useTheme(); const errorApi = useApi(errorApiRef); const lighthouseApi = useApi(lighthouseApiRef); const classes = useStyles(); @@ -94,7 +96,11 @@ const CreateAudit: FC<{}> = () => { ]); return ( - +
= () => ( - -
- -
- - - - New Relic Application Performance Monitoring - - - - - +const NewRelicComponent: FC<{}> = () => { + const backstageTheme = useTheme(); + return ( + +
+ +
+ + + + New Relic Application Performance Monitoring + + + + + + -
-
-
-); - + + + ); +}; export default NewRelicComponent; diff --git a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx index 5e4da3fc35..722e2e1964 100644 --- a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx +++ b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx @@ -14,9 +14,9 @@ * limitations under the License. */ -import { customPageTheme } from '@backstage/theme'; +import { BackstageTheme } from '@backstage/theme'; import React, { useState } from 'react'; -import { Grid, makeStyles } from '@material-ui/core'; +import { Grid, makeStyles, useTheme } from '@material-ui/core'; import { InfoCard, Page, @@ -61,6 +61,7 @@ export const RegisterComponentPage = ({ }: { catalogRouteRef: RouteRef; }) => { + const backstageTheme = useTheme(); const classes = useStyles(); const catalogApi = useApi(catalogApiRef); const [formState, setFormState] = useState>( @@ -113,7 +114,11 @@ export const RegisterComponentPage = ({ }; return ( - +
diff --git a/plugins/rollbar/src/components/RollbarHome/RollbarHome.tsx b/plugins/rollbar/src/components/RollbarHome/RollbarHome.tsx index 9f0deca967..f219be9615 100644 --- a/plugins/rollbar/src/components/RollbarHome/RollbarHome.tsx +++ b/plugins/rollbar/src/components/RollbarHome/RollbarHome.tsx @@ -14,17 +14,23 @@ * limitations under the License. */ -import { customPageTheme } from '@backstage/theme'; +import { BackstageTheme } from '@backstage/theme'; +import { useTheme } from '@material-ui/core'; import React from 'react'; import { Content, Header, Page } from '@backstage/core'; import { RollbarProjectTable } from '../RollbarProjectTable/RollbarProjectTable'; import { useRollbarEntities } from '../../hooks/useRollbarEntities'; export const RollbarHome = () => { + const backstageTheme = useTheme(); const { entities, loading, error } = useRollbarEntities(); return ( - +
{ + const backstageTheme = useTheme(); const { entity } = useCatalogEntity(); return ( - +
diff --git a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx index 1bd43ded44..bd70fd9565 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { customPageTheme } from '@backstage/theme'; +import { BackstageTheme } from '@backstage/theme'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { Content, @@ -29,7 +29,7 @@ import { WarningPanel, } from '@backstage/core'; import { catalogApiRef } from '@backstage/plugin-catalog'; -import { Button, Grid, Link, Typography } from '@material-ui/core'; +import { Button, Grid, Link, Typography, useTheme } from '@material-ui/core'; import React, { useEffect } from 'react'; import { Link as RouterLink } from 'react-router-dom'; import useStaleWhileRevalidate from 'swr'; @@ -49,6 +49,7 @@ const getTemplateCardProps = ( }; export const ScaffolderPage = () => { + const backstageTheme = useTheme(); const catalogApi = useApi(catalogApiRef); const errorApi = useApi(errorApiRef); @@ -66,7 +67,7 @@ export const ScaffolderPage = () => { }, [error, errorApi]); return ( - +
{ - const theme = - customPageTheme.pageTheme[type] ?? customPageTheme.pageTheme.other; + const backstageTheme = useTheme(); + + const themeId = pageTheme[type] ? type : 'other'; + const theme = backstageTheme.getPageTheme({ themeId }); const classes = useStyles({ backgroundImage: theme.backgroundImage }); const href = generatePath(templateRoute.path, { templateName: name }); diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index 5067f7963d..7bfc1b78a2 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { customPageTheme } from '@backstage/theme'; +import { BackstageTheme } from '@backstage/theme'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { Content, @@ -25,7 +25,7 @@ import { useApi, } from '@backstage/core'; import { catalogApiRef } from '@backstage/plugin-catalog'; -import { LinearProgress } from '@material-ui/core'; +import { LinearProgress, useTheme } from '@material-ui/core'; import { IChangeEvent } from '@rjsf/core'; import React, { useState } from 'react'; import { useParams } from 'react-router-dom'; @@ -80,6 +80,7 @@ const REPO_FORMAT = { }; export const TemplatePage = () => { + const backstageTheme = useTheme(); const errorApi = useApi(errorApiRef); const catalogApi = useApi(catalogApiRef); const scaffolderApi = useApi(scaffolderApiRef); @@ -145,7 +146,7 @@ export const TemplatePage = () => { } return ( - +
= () => { + const backstageTheme = useTheme(); const [statsFor, setStatsFor] = useState<'12h' | '24h'>('12h'); const toggleStatsFor = () => setStatsFor(statsFor === '12h' ? '12h' : '24h'); const sentryProjectId = 'sample-sentry-project-id'; return ( - +
diff --git a/plugins/tech-radar/src/components/RadarPage.tsx b/plugins/tech-radar/src/components/RadarPage.tsx index 58dcaec9af..d23061b094 100644 --- a/plugins/tech-radar/src/components/RadarPage.tsx +++ b/plugins/tech-radar/src/components/RadarPage.tsx @@ -14,9 +14,9 @@ * limitations under the License. */ -import { customPageTheme } from '@backstage/theme'; +import { BackstageTheme } from '@backstage/theme'; import React from 'react'; -import { Grid } from '@material-ui/core'; +import { Grid, useTheme } from '@material-ui/core'; import { Content, ContentHeader, @@ -39,29 +39,35 @@ export const RadarPage = ({ subtitle, pageTitle, ...props -}: TechRadarPageProps): JSX.Element => ( - -
- - -
- - - - This is used for visualizing the official guidelines of different - areas of software development such as languages, frameworks, - infrastructure and processes. - - - - - +}: TechRadarPageProps): JSX.Element => { + const backstageTheme = useTheme(); + return ( + +
+ + +
+ + + + This is used for visualizing the official guidelines of different + areas of software development such as languages, frameworks, + infrastructure and processes. + + + + + + -
-
-
-); - +
+ + ); +}; RadarPage.defaultProps = { title: 'Tech Radar', subtitle: 'Pick the recommended technologies for your projects', diff --git a/plugins/techdocs/src/reader/components/TechDocsHome.tsx b/plugins/techdocs/src/reader/components/TechDocsHome.tsx index 11211e713c..149117ec00 100644 --- a/plugins/techdocs/src/reader/components/TechDocsHome.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsHome.tsx @@ -14,11 +14,11 @@ * limitations under the License. */ -import { customPageTheme } from '@backstage/theme'; +import { BackstageTheme } from '@backstage/theme'; import React from 'react'; import { useAsync } from 'react-use'; import { useNavigate, generatePath } from 'react-router-dom'; -import { Grid } from '@material-ui/core'; +import { Grid, useTheme } from '@material-ui/core'; import { ItemCard, Progress, @@ -31,6 +31,7 @@ import { catalogApiRef } from '@backstage/plugin-catalog'; import { rootDocsRouteRef } from '../../plugin'; export const TechDocsHome = () => { + const backstageTheme = useTheme(); const catalogApi = useApi(catalogApiRef); const navigate = useNavigate(); @@ -43,7 +44,11 @@ export const TechDocsHome = () => { if (loading) { return ( - +
{ if (error) { return ( - +
{ } return ( - +
{ + const backstageTheme = useTheme(); const [documentReady, setDocumentReady] = useState(false); const { entityId } = useParams(); const [kind, namespace, name] = entityId.split(':'); @@ -47,7 +49,11 @@ export const TechDocsPage = () => { }; return ( - + { + const backstageTheme = useTheme(); const appTitle = useApi(configApiRef).getOptionalString('app.title') ?? 'Backstage'; const profile = { givenName: '' }; return ( - +
Date: Sun, 11 Oct 2020 19:27:59 +0530 Subject: [PATCH 012/139] fix: Implement new strategy consumer --- plugins/user-settings/src/components/SettingsPage.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/plugins/user-settings/src/components/SettingsPage.tsx b/plugins/user-settings/src/components/SettingsPage.tsx index 86096e9a1f..e8e5cbdbfd 100644 --- a/plugins/user-settings/src/components/SettingsPage.tsx +++ b/plugins/user-settings/src/components/SettingsPage.tsx @@ -15,7 +15,9 @@ */ import React, { useState } from 'react'; -import { Content, Header, HeaderTabs, Page, pageTheme } from '@backstage/core'; +import { Content, Header, HeaderTabs, Page } from '@backstage/core'; +import { BackstageTheme } from '@backstage/theme'; +import { useTheme } from '@material-ui/core'; import { General } from './General'; import { AuthProviders } from './AuthProviders'; import { FeatureFlags } from './FeatureFlags'; @@ -25,6 +27,7 @@ type Props = { }; export const SettingsPage = ({ providerSettings }: Props) => { + const backstageTheme = useTheme(); const [activeTab, setActiveTab] = useState(0); const onTabChange = (index: number) => { setActiveTab(index); @@ -43,7 +46,11 @@ export const SettingsPage = ({ providerSettings }: Props) => { ]; return ( - +
{content[activeTab]} From cd66aed3d1b37c1a1cba2a56ed61cb3069f08c03 Mon Sep 17 00:00:00 2001 From: Marvin9 Date: Sun, 11 Oct 2020 19:34:16 +0530 Subject: [PATCH 013/139] chore: remove comments --- .../src/components/EntityPageLayout/EntityPageLayout.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx b/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx index 68e871009a..4958a6f850 100644 --- a/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx +++ b/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx @@ -27,11 +27,6 @@ import { UnregisterEntityDialog } from '../UnregisterEntityDialog/UnregisterEnti import { Alert } from '@material-ui/lab'; import { Tabbed } from './Tabbed'; -// const getPageTheme = (entity?: Entity): PageTheme => { -// const themeKey = entity?.spec?.type?.toString() ?? 'home'; -// return customPageTheme.pageTheme[themeKey] ?? customPageTheme.pageTheme.home; -// }; - const EntityPageTitle = ({ entity, title, From a4aa277e9f4447e253bf9c58b0aabb0eeb73bea5 Mon Sep 17 00:00:00 2001 From: Marvin9 Date: Sun, 11 Oct 2020 19:35:01 +0530 Subject: [PATCH 014/139] fix: Replace new theme strategy consumer in handlebar template & Example md file --- .../tutorials/quickstart-app-plugin/ExampleComponent.md | 7 ++++--- .../components/ExampleComponent/ExampleComponent.tsx.hbs | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/contrib/docs/tutorials/quickstart-app-plugin/ExampleComponent.md b/contrib/docs/tutorials/quickstart-app-plugin/ExampleComponent.md index ed4bfc8012..83b18bf4c2 100644 --- a/contrib/docs/tutorials/quickstart-app-plugin/ExampleComponent.md +++ b/contrib/docs/tutorials/quickstart-app-plugin/ExampleComponent.md @@ -3,9 +3,9 @@ ExampleComponent.tsx reference ```tsx -import { customPageTheme } from '@backstage/theme'; +import { BackstageTheme } from '@backstage/theme'; import React, { FC } from 'react'; -import { Typography, Grid } from '@material-ui/core'; +import { Typography, Grid, useTheme } from '@material-ui/core'; import { InfoCard, Header, @@ -21,12 +21,13 @@ import { useApi } from '@backstage/core-api'; import ExampleFetchComponent from '../ExampleFetchComponent'; const ExampleComponent: FC<{}> = () => { + const backstageTheme = useTheme(); const identityApi = useApi(identityApiRef); const userId = identityApi.getUserId(); const profile = identityApi.getProfile(); return ( - +
= () => ( - +
From fae67f2be1aa19f2caad112df92e5b93e68de009 Mon Sep 17 00:00:00 2001 From: Marvin9 Date: Sun, 11 Oct 2020 20:08:08 +0530 Subject: [PATCH 015/139] chore: update example component --- .../ExampleComponent/ExampleComponent.tsx.hbs | 55 ++++++++++--------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs index 9cdd22c0ca..d4be6d87ac 100644 --- a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs +++ b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs @@ -1,6 +1,6 @@ -import { BackstageTheme } from '@backstage/theme'; +import { pageTheme } from '@backstage/theme'; import React, { FC } from 'react'; -import { Typography, Grid, useTheme } from '@material-ui/core'; +import { Typography, Grid } from '@material-ui/core'; import { InfoCard, Header, @@ -12,30 +12,31 @@ import { } from '@backstage/core'; import ExampleFetchComponent from '../ExampleFetchComponent'; -const ExampleComponent: FC<{}> = () => ( - -
- - -
- - - A description of your plugin goes here. - - - - - - All content should be wrapped in a card like this. - - +const ExampleComponent: FC<{}> = () => { + return ( + +
+ + +
+ + + A description of your plugin goes here. + + + + + + All content should be wrapped in a card like this. + + + + + + - - - -
-
-
-); - + + + ); +} export default ExampleComponent; From b79017fd331112754e127cc48c6150cf219bec4e Mon Sep 17 00:00:00 2001 From: Jesko Steinberg Date: Thu, 1 Oct 2020 14:56:57 +0200 Subject: [PATCH 016/139] feat(githubAuth): make default scopes configurable --- .changeset/2800.md | 15 +++++++++++++++ .../implementations/auth/github/GithubAuth.ts | 4 +++- packages/core/src/api-wrappers/defaultApis.ts | 6 +++++- 3 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 .changeset/2800.md diff --git a/.changeset/2800.md b/.changeset/2800.md new file mode 100644 index 0000000000..0e051b0557 --- /dev/null +++ b/.changeset/2800.md @@ -0,0 +1,15 @@ +--- +'@backstage/core': minor +'@backstage/core-api': minor +--- + +Updated the `GithubAuth.create` method to configure the default scope of the Github Auth Api. As a result the +default scope is configurable when overwriting the Core Api in the app. + +``` +GithubAuth.create({ + discoveryApi, + oauthRequestApi, + defaultScopes: ['read:user', 'repo'], +}), +``` diff --git a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts index 1d87ff3720..eb2f375ab0 100644 --- a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts @@ -41,6 +41,7 @@ type CreateOptions = { discoveryApi: DiscoveryApi; oauthRequestApi: OAuthRequestApi; + defaultScopes?: string[]; environment?: string; provider?: AuthProvider & { id: string }; }; @@ -67,6 +68,7 @@ class GithubAuth implements OAuthApi, SessionApi { environment = 'development', provider = DEFAULT_PROVIDER, oauthRequestApi, + defaultScopes = ['read:user'], }: CreateOptions) { const connector = new DefaultAuthConnector({ discoveryApi, @@ -89,7 +91,7 @@ class GithubAuth implements OAuthApi, SessionApi { const sessionManager = new StaticAuthSessionManager({ connector, - defaultScopes: new Set(['read:user']), + defaultScopes: new Set(defaultScopes), sessionScopes: (session: GithubSession) => session.providerInfo.scopes, }); diff --git a/packages/core/src/api-wrappers/defaultApis.ts b/packages/core/src/api-wrappers/defaultApis.ts index dedadf0456..e735e4d371 100644 --- a/packages/core/src/api-wrappers/defaultApis.ts +++ b/packages/core/src/api-wrappers/defaultApis.ts @@ -96,7 +96,11 @@ export const defaultApis = [ oauthRequestApi: oauthRequestApiRef, }, factory: ({ discoveryApi, oauthRequestApi }) => - GithubAuth.create({ discoveryApi, oauthRequestApi }), + GithubAuth.create({ + discoveryApi, + oauthRequestApi, + defaultScopes: ['read:user'], + }), }), createApiFactory({ api: oktaAuthApiRef, From 6109e354951a30bcabddf1c550ee70dec61249dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 9 Oct 2020 13:33:20 +0200 Subject: [PATCH 017/139] feat(catalog-backend): remove byUid and byName as special cases --- .../src/catalog/DatabaseEntitiesCatalog.ts | 17 +------ plugins/catalog-backend/src/catalog/types.ts | 4 +- .../ingestion/HigherOrderOperations.test.ts | 2 - .../src/service/router.test.ts | 46 ++++++++++--------- plugins/catalog-backend/src/service/router.ts | 22 +++++---- 5 files changed, 38 insertions(+), 53 deletions(-) diff --git a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts index 2575411e7c..c43e1b2f05 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts @@ -15,13 +15,12 @@ */ import { NotFoundError } from '@backstage/backend-common'; +import type { Entity } from '@backstage/catalog-model'; import { - EntityName, generateUpdatedEntity, getEntityName, LOCATION_ANNOTATION, } from '@backstage/catalog-model'; -import type { Entity } from '@backstage/catalog-model'; import type { Database, DbEntityResponse, EntityFilters } from '../database'; import type { EntitiesCatalog } from './types'; @@ -35,20 +34,6 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog { return items.map(i => i.entity); } - async entityByUid(uid: string): Promise { - const response = await this.database.transaction(tx => - this.database.entityByUid(tx, uid), - ); - return response?.entity; - } - - async entityByName(name: EntityName): Promise { - const response = await this.database.transaction(tx => - this.database.entityByName(tx, name), - ); - return response?.entity; - } - async addOrUpdateEntity( entity: Entity, locationId?: string, diff --git a/plugins/catalog-backend/src/catalog/types.ts b/plugins/catalog-backend/src/catalog/types.ts index 5d84cbcebc..810d51a6c1 100644 --- a/plugins/catalog-backend/src/catalog/types.ts +++ b/plugins/catalog-backend/src/catalog/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Entity, EntityName, Location } from '@backstage/catalog-model'; +import { Entity, Location } from '@backstage/catalog-model'; import type { EntityFilters } from '../database'; // @@ -23,8 +23,6 @@ import type { EntityFilters } from '../database'; export type EntitiesCatalog = { entities(filters?: EntityFilters): Promise; - entityByUid(uid: string): Promise; - entityByName(name: EntityName): Promise; addOrUpdateEntity(entity: Entity, locationId?: string): Promise; addEntities(entities: Entity[], locationId?: string): Promise; removeEntityByUid(uid: string): Promise; diff --git a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.test.ts b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.test.ts index 15d9168732..81753fe6d1 100644 --- a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.test.ts +++ b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.test.ts @@ -36,8 +36,6 @@ describe('HigherOrderOperations', () => { beforeAll(() => { entitiesCatalog = { entities: jest.fn(), - entityByUid: jest.fn(), - entityByName: jest.fn(), addOrUpdateEntity: jest.fn(), addEntities: jest.fn(), removeEntityByUid: jest.fn(), diff --git a/plugins/catalog-backend/src/service/router.test.ts b/plugins/catalog-backend/src/service/router.test.ts index ecf4b0e614..ec5c11be33 100644 --- a/plugins/catalog-backend/src/service/router.test.ts +++ b/plugins/catalog-backend/src/service/router.test.ts @@ -32,8 +32,6 @@ describe('createRouter', () => { beforeAll(async () => { entitiesCatalog = { entities: jest.fn(), - entityByUid: jest.fn(), - entityByName: jest.fn(), addOrUpdateEntity: jest.fn(), addEntities: jest.fn(), removeEntityByUid: jest.fn(), @@ -100,23 +98,27 @@ describe('createRouter', () => { name: 'c', }, }; - entitiesCatalog.entityByUid.mockResolvedValue(entity); + entitiesCatalog.entities.mockResolvedValue([entity]); const response = await request(app).get('/entities/by-uid/zzz'); - expect(entitiesCatalog.entityByUid).toHaveBeenCalledTimes(1); - expect(entitiesCatalog.entityByUid).toHaveBeenCalledWith('zzz'); + expect(entitiesCatalog.entities).toHaveBeenCalledTimes(1); + expect(entitiesCatalog.entities).toHaveBeenCalledWith([ + { key: 'metadata.uid', values: ['zzz'] }, + ]); expect(response.status).toEqual(200); expect(response.body).toEqual(expect.objectContaining(entity)); }); it('responds with a 404 for missing entities', async () => { - entitiesCatalog.entityByUid.mockResolvedValue(undefined); + entitiesCatalog.entities.mockResolvedValue([]); const response = await request(app).get('/entities/by-uid/zzz'); - expect(entitiesCatalog.entityByUid).toHaveBeenCalledTimes(1); - expect(entitiesCatalog.entityByUid).toHaveBeenCalledWith('zzz'); + expect(entitiesCatalog.entities).toHaveBeenCalledTimes(1); + expect(entitiesCatalog.entities).toHaveBeenCalledWith([ + { key: 'metadata.uid', values: ['zzz'] }, + ]); expect(response.status).toEqual(404); expect(response.text).toMatch(/uid/); }); @@ -132,31 +134,31 @@ describe('createRouter', () => { namespace: 'ns', }, }; - entitiesCatalog.entityByName.mockResolvedValue(entity); + entitiesCatalog.entities.mockResolvedValue([entity]); const response = await request(app).get('/entities/by-name/k/ns/n'); - expect(entitiesCatalog.entityByName).toHaveBeenCalledTimes(1); - expect(entitiesCatalog.entityByName).toHaveBeenCalledWith({ - kind: 'k', - namespace: 'ns', - name: 'n', - }); + expect(entitiesCatalog.entities).toHaveBeenCalledTimes(1); + expect(entitiesCatalog.entities).toHaveBeenCalledWith([ + { key: 'kind', values: ['k'] }, + { key: 'metadata.namespace', values: ['ns'] }, + { key: 'metadata.name', values: ['n'] }, + ]); expect(response.status).toEqual(200); expect(response.body).toEqual(expect.objectContaining(entity)); }); it('responds with a 404 for missing entities', async () => { - entitiesCatalog.entityByName.mockResolvedValue(undefined); + entitiesCatalog.entities.mockResolvedValue([]); const response = await request(app).get('/entities/by-name/b/d/c'); - expect(entitiesCatalog.entityByName).toHaveBeenCalledTimes(1); - expect(entitiesCatalog.entityByName).toHaveBeenCalledWith({ - kind: 'b', - namespace: 'd', - name: 'c', - }); + expect(entitiesCatalog.entities).toHaveBeenCalledTimes(1); + expect(entitiesCatalog.entities).toHaveBeenCalledWith([ + { key: 'kind', values: ['b'] }, + { key: 'metadata.namespace', values: ['d'] }, + { key: 'metadata.name', values: ['c'] }, + ]); expect(response.status).toEqual(404); expect(response.text).toMatch(/name/); }); diff --git a/plugins/catalog-backend/src/service/router.ts b/plugins/catalog-backend/src/service/router.ts index 8da589c721..06a7ea43d3 100644 --- a/plugins/catalog-backend/src/service/router.ts +++ b/plugins/catalog-backend/src/service/router.ts @@ -54,11 +54,13 @@ export async function createRouter( }) .get('/entities/by-uid/:uid', async (req, res) => { const { uid } = req.params; - const entity = await entitiesCatalog.entityByUid(uid); - if (!entity) { + const entities = await entitiesCatalog.entities([ + { key: 'metadata.uid', values: [uid] }, + ]); + if (!entities.length) { res.status(404).send(`No entity with uid ${uid}`); } - res.status(200).send(entity); + res.status(200).send(entities[0]); }) .delete('/entities/by-uid/:uid', async (req, res) => { const { uid } = req.params; @@ -67,19 +69,19 @@ export async function createRouter( }) .get('/entities/by-name/:kind/:namespace/:name', async (req, res) => { const { kind, namespace, name } = req.params; - const entity = await entitiesCatalog.entityByName({ - kind, - namespace, - name, - }); - if (!entity) { + const entities = await entitiesCatalog.entities([ + { key: 'kind', values: [kind] }, + { key: 'metadata.namespace', values: [namespace] }, + { key: 'metadata.name', values: [name] }, + ]); + if (!entities.length) { res .status(404) .send( `No entity with kind ${kind} namespace ${namespace} name ${name}`, ); } - res.status(200).send(entity); + res.status(200).send(entities[0]); }); } From 67e7d476a2e653916c6e2a0cccf2d25a648914c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 9 Oct 2020 16:24:33 +0200 Subject: [PATCH 018/139] feat(catalog-backend): simplify the search filters structure --- .../src/catalog/DatabaseEntitiesCatalog.ts | 9 +- .../src/database/CommonDatabase.test.ts | 17 +--- .../src/database/CommonDatabase.ts | 99 ++++++++++--------- .../src/database/search.test.ts | 3 + .../catalog-backend/src/database/search.ts | 35 +++++-- plugins/catalog-backend/src/database/types.ts | 26 ++++- .../ingestion/HigherOrderOperations.test.ts | 13 +-- .../src/ingestion/HigherOrderOperations.ts | 11 +-- .../src/service/router.test.ts | 42 ++++---- plugins/catalog-backend/src/service/router.ts | 29 +++--- 10 files changed, 155 insertions(+), 129 deletions(-) diff --git a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts index c43e1b2f05..8619355c91 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts @@ -85,12 +85,9 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog { const location = entityResponse.entity.metadata.annotations?.[LOCATION_ANNOTATION]; const colocatedEntities = location - ? await this.database.entities(tx, [ - { - key: `metadata.annotations.${LOCATION_ANNOTATION}`, - values: [location], - }, - ]) + ? await this.database.entities(tx, { + [`metadata.annotations.${LOCATION_ANNOTATION}`]: location, + }) : [entityResponse]; for (const dbResponse of colocatedEntities) { await this.database.removeEntityByUid( diff --git a/plugins/catalog-backend/src/database/CommonDatabase.test.ts b/plugins/catalog-backend/src/database/CommonDatabase.test.ts index 840af74825..f3ca212df0 100644 --- a/plugins/catalog-backend/src/database/CommonDatabase.test.ts +++ b/plugins/catalog-backend/src/database/CommonDatabase.test.ts @@ -347,7 +347,7 @@ describe('CommonDatabase', () => { await db.transaction(async tx => { await db.addEntities(tx, [{ entity: e1 }, { entity: e2 }]); }); - const result = await db.transaction(async tx => db.entities(tx, [])); + const result = await db.transaction(async tx => db.entities(tx, {})); expect(result.length).toEqual(2); expect(result).toEqual( expect.arrayContaining([ @@ -389,10 +389,7 @@ describe('CommonDatabase', () => { await expect( db.transaction(async tx => - db.entities(tx, [ - { key: 'kind', values: ['k2'] }, - { key: 'spec.c', values: ['some'] }, - ]), + db.entities(tx, { kind: 'k2', 'spec.c': 'some' }), ), ).resolves.toEqual([ { @@ -427,10 +424,7 @@ describe('CommonDatabase', () => { }); const rows = await db.transaction(async tx => - db.entities(tx, [ - { key: 'apiVersion', values: ['a'] }, - { key: 'spec.c', values: [null, 'some'] }, - ]), + db.entities(tx, { apiVersion: 'a', 'spec.c': [null, 'some'] }), ); expect(rows.length).toEqual(3); @@ -477,10 +471,7 @@ describe('CommonDatabase', () => { }); const rows = await db.transaction(async tx => - db.entities(tx, [ - { key: 'ApiVersioN', values: ['A'] }, - { key: 'spEc.C', values: [null, 'some'] }, - ]), + db.entities(tx, { ApiVersioN: 'A', 'spEc.C': [null, 'some'] }), ); expect(rows.length).toEqual(3); diff --git a/plugins/catalog-backend/src/database/CommonDatabase.ts b/plugins/catalog-backend/src/database/CommonDatabase.ts index 49b36e0933..563fd24c80 100644 --- a/plugins/catalog-backend/src/database/CommonDatabase.ts +++ b/plugins/catalog-backend/src/database/CommonDatabase.ts @@ -219,61 +219,64 @@ export class CommonDatabase implements Database { let entitiesQuery = tx('entities'); - for (const filter of filters || []) { - const key = filter.key.toLowerCase().replace(/[*]/g, '%'); - const keyOp = filter.key.includes('*') ? 'like' : '='; + if (filters && Object.keys(filters).length) { + for (const [matchKey, matchVal] of Object.entries(filters)) { + const key = matchKey.toLowerCase().replace(/[*]/g, '%'); + const keyOp = key.includes('*') ? 'like' : '='; + const values = Array.isArray(matchVal) ? matchVal : [matchVal]; - let matchNulls = false; - const matchIn: string[] = []; - const matchLike: string[] = []; + let matchNulls = false; + const matchIn: string[] = []; + const matchLike: string[] = []; - for (const value of filter.values) { - if (!value) { - matchNulls = true; - } else if (value.includes('*')) { - matchLike.push(value.toLowerCase().replace(/[*]/g, '%')); - } else { - matchIn.push(value.toLowerCase()); + for (const value of values) { + if (!value) { + matchNulls = true; + } else if (value.includes('*')) { + matchLike.push(value.toLowerCase().replace(/[*]/g, '%')); + } else { + matchIn.push(value.toLowerCase()); + } } - } - // NOTE(freben): This used to be a set of OUTER JOIN, which may seem to - // make a lot of sense. However, it had abysmal performance on sqlite - // when datasets grew large, so we're using IN instead. - const matchQuery = tx('entities_search') - .select('entity_id') - .where(function keyFilter() { - this.andWhere('key', keyOp, key); - this.andWhere(function valueFilter() { - if (matchIn.length === 1) { - this.orWhere({ value: matchIn[0] }); - } else if (matchIn.length > 1) { - this.orWhereIn('value', matchIn); - } - if (matchLike.length) { - for (const x of matchLike) { - this.orWhere('value', 'like', tx.raw('?', [x])); + // NOTE(freben): This used to be a set of OUTER JOIN, which may seem to + // make a lot of sense. However, it had abysmal performance on sqlite + // when datasets grew large, so we're using IN instead. + const matchQuery = tx('entities_search') + .select('entity_id') + .where(function keyFilter() { + this.andWhere('key', keyOp, key); + this.andWhere(function valueFilter() { + if (matchIn.length === 1) { + this.orWhere({ value: matchIn[0] }); + } else if (matchIn.length > 1) { + this.orWhereIn('value', matchIn); } - } - if (matchNulls) { - // Match explicit nulls, and then handle absence separately below - this.orWhereNull('value'); - } + if (matchLike.length) { + for (const x of matchLike) { + this.orWhere('value', 'like', tx.raw('?', [x])); + } + } + if (matchNulls) { + // Match explicit nulls, and then handle absence separately below + this.orWhereNull('value'); + } + }); }); - }); - // Handle absence as nulls as well - entitiesQuery = entitiesQuery.andWhere(function match() { - this.whereIn('id', matchQuery); - if (matchNulls) { - this.orWhereNotIn( - 'id', - tx('entities_search') - .select('entity_id') - .where('key', keyOp, key), - ); - } - }); + // Handle absence as nulls as well + entitiesQuery = entitiesQuery.andWhere(function match() { + this.whereIn('id', matchQuery); + if (matchNulls) { + this.orWhereNotIn( + 'id', + tx('entities_search') + .select('entity_id') + .where('key', keyOp, key), + ); + } + }); + } } const rows = await entitiesQuery diff --git a/plugins/catalog-backend/src/database/search.test.ts b/plugins/catalog-backend/src/database/search.test.ts index d108820ca9..c6793cbdf4 100644 --- a/plugins/catalog-backend/src/database/search.test.ts +++ b/plugins/catalog-backend/src/database/search.test.ts @@ -24,8 +24,11 @@ describe('search', () => { const output = traverse(input); expect(output).toEqual([ { key: 'a', value: 'b' }, + { key: 'a.b', value: true }, { key: 'a', value: 'c' }, + { key: 'a.c', value: true }, { key: 'a', value: 'd' }, + { key: 'a.d', value: true }, ]); }); diff --git a/plugins/catalog-backend/src/database/search.ts b/plugins/catalog-backend/src/database/search.ts index 863b86ee9a..9e58d2468b 100644 --- a/plugins/catalog-backend/src/database/search.ts +++ b/plugins/catalog-backend/src/database/search.ts @@ -35,7 +35,7 @@ const MAX_VALUE_LENGTH = 200; type Kv = { key: string; - value: any; + value: unknown; }; // Helper for traversing through a nested structure and outputting a list of @@ -58,15 +58,17 @@ type Kv = { // "a", 1 // "b.c", null // "b.e": "f" +// "b.e.f": true // "b.e": "g" +// "b.e.g": true // "h.i": 1 // "h.j": "k" // "h.i": 2 // "h.j": "l" -export function traverse(root: any): Kv[] { +export function traverse(root: unknown): Kv[] { const output: Kv[] = []; - function visit(path: string, current: any) { + function visit(path: string, current: unknown) { if (SPECIAL_KEYS.includes(path)) { return; } @@ -89,14 +91,29 @@ export function traverse(root: any): Kv[] { // array if (Array.isArray(current)) { for (const item of current) { - // keep the same path as currently + // NOTE(freben): The reason that these are output in two different ways, + // is to support use cases where you want to express that MORE than one + // tag is present in a list. Since the EntityFilters structure is a + // record, you can't have several entries of the same key. Therefore + // you will have to match on + // + // { "a.b": ["true"], "a.c": ["true"] } + // + // rather than + // + // { "a": ["b", "c"] } + // + // because the latter means EITHER b or c has to be present. visit(path, item); + if (typeof item === 'string') { + output.push({ key: `${path}.${item}`, value: true }); + } } return; } // object - for (const [key, value] of Object.entries(current)) { + for (const [key, value] of Object.entries(current!)) { visit(path ? `${path}.${key}` : key, value); } } @@ -113,12 +130,12 @@ export function mapToRows( ): DbEntitiesSearchRow[] { const result: DbEntitiesSearchRow[] = []; - for (let { key, value } of input) { - key = key.toLowerCase(); - if (value === undefined || value === null) { + for (const { key: rawKey, value: rawValue } of input) { + const key = rawKey.toLowerCase(); + if (rawValue === undefined || rawValue === null) { result.push({ entity_id: entityId, key, value: null }); } else { - value = String(value).toLowerCase(); + const value = String(rawValue).toLowerCase(); if (value.length <= MAX_VALUE_LENGTH) { result.push({ entity_id: entityId, key, value }); } diff --git a/plugins/catalog-backend/src/database/types.ts b/plugins/catalog-backend/src/database/types.ts index 20cc123c0a..14e8d5be64 100644 --- a/plugins/catalog-backend/src/database/types.ts +++ b/plugins/catalog-backend/src/database/types.ts @@ -67,11 +67,27 @@ export type DatabaseLocationUpdateLogEvent = { message?: string; }; -export type EntityFilter = { - key: string; - values: (string | null)[]; -}; -export type EntityFilters = EntityFilter[]; +/** + * Filter matcher for a single entity field. + * + * Can be either null or a string, or an array of those. Null and the empty + * string are treated equally, and match both a present field with a null or + * empty value, as well as an absent field. + * + * A filter may contain asterisks (*) that are treated as wildcards for zero + * or more arbitrary characters. + */ +export type EntityFilter = null | string | (null | string)[]; + +/** + * A set of filter matchers used for filtering entities. + * + * The keys are full dot-separated paths into the structure of an entity, for + * example "metadata.name". You can also address any item in an array the same + * way, e.g. "a.b.c": "x" works if b is an array of objects that have a c field + * and any of those have the value x. + */ +export type EntityFilters = Record; /** * An abstraction on top of the underlying database, wrapping the basic CRUD diff --git a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.test.ts b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.test.ts index 81753fe6d1..909139d678 100644 --- a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.test.ts +++ b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.test.ts @@ -204,14 +204,11 @@ describe('HigherOrderOperations', () => { target: 'thing', }); expect(entitiesCatalog.entities).toHaveBeenCalledTimes(1); - expect(entitiesCatalog.entities).toHaveBeenNthCalledWith( - 1, - expect.arrayContaining([ - { key: 'kind', values: ['Component'] }, - { key: 'metadata.namespace', values: [ENTITY_DEFAULT_NAMESPACE] }, - { key: 'metadata.name', values: ['c1'] }, - ]), - ); + expect(entitiesCatalog.entities).toHaveBeenNthCalledWith(1, { + kind: 'Component', + 'metadata.namespace': ENTITY_DEFAULT_NAMESPACE, + 'metadata.name': ['c1'], + }); expect(entitiesCatalog.addEntities).toHaveBeenCalledTimes(1); expect(entitiesCatalog.addEntities).toHaveBeenNthCalledWith( 1, diff --git a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts index 737166436c..fa73c58348 100644 --- a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts +++ b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts @@ -277,12 +277,11 @@ export class HigherOrderOperations implements HigherOrderOperation { }> { const markTimestamp = process.hrtime(); - const names = newEntities.map(e => e.metadata.name); - const oldEntities = await this.entitiesCatalog.entities([ - { key: 'kind', values: [kind] }, - { key: 'metadata.namespace', values: [namespace] }, - { key: 'metadata.name', values: names }, - ]); + const oldEntities = await this.entitiesCatalog.entities({ + kind: kind, + 'metadata.namespace': namespace, + 'metadata.name': newEntities.map(e => e.metadata.name), + }); const oldEntitiesByName = new Map( oldEntities.map(e => [e.metadata.name, e]), diff --git a/plugins/catalog-backend/src/service/router.test.ts b/plugins/catalog-backend/src/service/router.test.ts index ec5c11be33..f7ab4878f8 100644 --- a/plugins/catalog-backend/src/service/router.test.ts +++ b/plugins/catalog-backend/src/service/router.test.ts @@ -81,11 +81,11 @@ describe('createRouter', () => { expect(response.status).toEqual(200); expect(entitiesCatalog.entities).toHaveBeenCalledTimes(1); - expect(entitiesCatalog.entities).toHaveBeenCalledWith([ - { key: 'a', values: ['1', null, '3'] }, - { key: 'b', values: ['4'] }, - { key: 'c', values: [null] }, - ]); + expect(entitiesCatalog.entities).toHaveBeenCalledWith({ + a: ['1', null, '3'], + b: ['4'], + c: [null], + }); }); }); @@ -103,9 +103,9 @@ describe('createRouter', () => { const response = await request(app).get('/entities/by-uid/zzz'); expect(entitiesCatalog.entities).toHaveBeenCalledTimes(1); - expect(entitiesCatalog.entities).toHaveBeenCalledWith([ - { key: 'metadata.uid', values: ['zzz'] }, - ]); + expect(entitiesCatalog.entities).toHaveBeenCalledWith({ + 'metadata.uid': 'zzz', + }); expect(response.status).toEqual(200); expect(response.body).toEqual(expect.objectContaining(entity)); }); @@ -116,9 +116,9 @@ describe('createRouter', () => { const response = await request(app).get('/entities/by-uid/zzz'); expect(entitiesCatalog.entities).toHaveBeenCalledTimes(1); - expect(entitiesCatalog.entities).toHaveBeenCalledWith([ - { key: 'metadata.uid', values: ['zzz'] }, - ]); + expect(entitiesCatalog.entities).toHaveBeenCalledWith({ + 'metadata.uid': 'zzz', + }); expect(response.status).toEqual(404); expect(response.text).toMatch(/uid/); }); @@ -139,11 +139,11 @@ describe('createRouter', () => { const response = await request(app).get('/entities/by-name/k/ns/n'); expect(entitiesCatalog.entities).toHaveBeenCalledTimes(1); - expect(entitiesCatalog.entities).toHaveBeenCalledWith([ - { key: 'kind', values: ['k'] }, - { key: 'metadata.namespace', values: ['ns'] }, - { key: 'metadata.name', values: ['n'] }, - ]); + expect(entitiesCatalog.entities).toHaveBeenCalledWith({ + kind: 'k', + 'metadata.namespace': 'ns', + 'metadata.name': 'n', + }); expect(response.status).toEqual(200); expect(response.body).toEqual(expect.objectContaining(entity)); }); @@ -154,11 +154,11 @@ describe('createRouter', () => { const response = await request(app).get('/entities/by-name/b/d/c'); expect(entitiesCatalog.entities).toHaveBeenCalledTimes(1); - expect(entitiesCatalog.entities).toHaveBeenCalledWith([ - { key: 'kind', values: ['b'] }, - { key: 'metadata.namespace', values: ['d'] }, - { key: 'metadata.name', values: ['c'] }, - ]); + expect(entitiesCatalog.entities).toHaveBeenCalledWith({ + kind: 'b', + 'metadata.namespace': 'd', + 'metadata.name': 'c', + }); expect(response.status).toEqual(404); expect(response.text).toMatch(/name/); }); diff --git a/plugins/catalog-backend/src/service/router.ts b/plugins/catalog-backend/src/service/router.ts index 06a7ea43d3..94dc76ab9d 100644 --- a/plugins/catalog-backend/src/service/router.ts +++ b/plugins/catalog-backend/src/service/router.ts @@ -54,9 +54,9 @@ export async function createRouter( }) .get('/entities/by-uid/:uid', async (req, res) => { const { uid } = req.params; - const entities = await entitiesCatalog.entities([ - { key: 'metadata.uid', values: [uid] }, - ]); + const entities = await entitiesCatalog.entities({ + 'metadata.uid': uid, + }); if (!entities.length) { res.status(404).send(`No entity with uid ${uid}`); } @@ -69,11 +69,11 @@ export async function createRouter( }) .get('/entities/by-name/:kind/:namespace/:name', async (req, res) => { const { kind, namespace, name } = req.params; - const entities = await entitiesCatalog.entities([ - { key: 'kind', values: [kind] }, - { key: 'metadata.namespace', values: [namespace] }, - { key: 'metadata.name', values: [name] }, - ]); + const entities = await entitiesCatalog.entities({ + kind: kind, + 'metadata.namespace': namespace, + 'metadata.name': name, + }); if (!entities.length) { res .status(404) @@ -123,7 +123,7 @@ export async function createRouter( function translateQueryToEntityFilters( request: express.Request, ): EntityFilters { - const filters: EntityFilters = []; + const filters: EntityFilters = {}; for (const [key, valueOrValues] of Object.entries(request.query)) { const values = Array.isArray(valueOrValues) @@ -134,10 +134,13 @@ function translateQueryToEntityFilters( throw new InputError('Complex query parameters are not supported'); } - filters.push({ - key, - values: values.map(v => v || null) as string[], - }); + // This one always emits arrays + let matchers = filters[key] as (string | null)[]; + if (!matchers) { + matchers = []; + filters[key] = matchers; + } + matchers.push(...(values.map(v => v || null) as (string | null)[])); } return filters; From 90461a7eac7a5a321ec59dcdf8e48bc77a358e42 Mon Sep 17 00:00:00 2001 From: lewtakm Date: Mon, 12 Oct 2020 12:50:37 +0200 Subject: [PATCH 019/139] feat: Add Pull Request plugin to sample app. --- packages/app/package.json | 2 +- .../app/src/components/catalog/EntityPage.tsx | 11 +++++++++++ yarn.lock | 16 ++++++++-------- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/packages/app/package.json b/packages/app/package.json index a3b150cef6..bc0b50eaf5 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -34,7 +34,7 @@ "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@octokit/rest": "^18.0.0", - "@roadiehq/backstage-plugin-github-pull-requests": "^0.4.3", + "@roadiehq/backstage-plugin-github-pull-requests": "^0.5.1", "@roadiehq/backstage-plugin-travis-ci": "^0.2.3", "dayjs": "^1.9.1", "history": "^5.0.0", diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 393ffe2631..e8b9a95fda 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -54,6 +54,7 @@ import { LastLighthouseAuditCard, isPluginApplicableToEntity as isLighthouseAvailable, } from '@backstage/plugin-lighthouse/'; +import { Router as GithubPullRequestsRouter } from '@roadiehq/backstage-plugin-github-pull-requests'; const CICDSwitcher = ({ entity }: { entity: Entity }) => { // This component is just an example of how you can implement your company's logic in entity page. @@ -160,6 +161,11 @@ const ServiceEntityPage = ({ entity }: { entity: Entity }) => ( title="Kubernetes" element={} /> + } + /> ); @@ -195,6 +201,11 @@ const WebsiteEntityPage = ({ entity }: { entity: Entity }) => ( title="Kubernetes" element={} /> + } + /> ); const DefaultEntityPage = ({ entity }: { entity: Entity }) => ( diff --git a/yarn.lock b/yarn.lock index 34d0287c42..4f76b0a66b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3639,15 +3639,15 @@ resolved "https://registry.npmjs.org/@rjsf/material-ui/-/material-ui-2.3.0.tgz#a051eb4db2ad778e39933a31ba806f415dd3ba90" integrity sha512-v/xZ4Xk18ZgBARcCe99IDqdO97GU0UC784gG8PhGGFDdy5Rbdy7ixTjHkGYttkr43PB7SX6ttohNhkKSW4nATQ== -"@roadiehq/backstage-plugin-github-pull-requests@^0.4.3": - version "0.4.3" - resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-github-pull-requests/-/backstage-plugin-github-pull-requests-0.4.3.tgz#720fd3c53ae8e61d8cfc727cda47e236af442b02" - integrity sha512-IJGMys5FJ512sitaMwWgACll3ZTuE3nZUX05MNXly/WXty6VBcfboiPbSB6IzLAEWMmvYlV1YUwwL7hAl/62GQ== +"@roadiehq/backstage-plugin-github-pull-requests@^0.5.1": + version "0.5.1" + resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-github-pull-requests/-/backstage-plugin-github-pull-requests-0.5.1.tgz#a0e3ef97de20fe87e55a26f0198d4ff80b6ecd8c" + integrity sha512-0Btg4jn7SWAp/0A7+xCuHxD4LKPoE3LSEkjMvLmWk/AIIvNA6uSWXeAv1nJZSe4JBV0Qzt+r+TS+hCcFR+vcMg== dependencies: - "@backstage/catalog-model" "^0.1.1-alpha.23" - "@backstage/core" "^0.1.1-alpha.23" - "@backstage/plugin-catalog" "^0.1.1-alpha.23" - "@backstage/theme" "^0.1.1-alpha.23" + "@backstage/catalog-model" "^0.1.1-alpha.24" + "@backstage/core" "^0.1.1-alpha.24" + "@backstage/plugin-catalog" "^0.1.1-alpha.24" + "@backstage/theme" "^0.1.1-alpha.24" "@material-ui/core" "^4.11.0" "@material-ui/icons" "^4.9.1" "@material-ui/lab" "^4.0.0-alpha.56" From 65d7224554ab789951e327849f58d326ae3acce9 Mon Sep 17 00:00:00 2001 From: lewtakm Date: Mon, 12 Oct 2020 13:00:19 +0200 Subject: [PATCH 020/139] feat: add changeset --- .changeset/beige-apes-serve.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/beige-apes-serve.md diff --git a/.changeset/beige-apes-serve.md b/.changeset/beige-apes-serve.md new file mode 100644 index 0000000000..54ee39a85e --- /dev/null +++ b/.changeset/beige-apes-serve.md @@ -0,0 +1,5 @@ +--- +'example-app': patch +--- + +Add Pull Request tab to components view. From d9bfacffadf0fe35f5a83128ebc4bbb4450e9754 Mon Sep 17 00:00:00 2001 From: lewtakm Date: Mon, 12 Oct 2020 13:07:48 +0200 Subject: [PATCH 021/139] fix: update component name --- packages/app/src/components/catalog/EntityPage.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index e8b9a95fda..ed63e26e79 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -54,7 +54,7 @@ import { LastLighthouseAuditCard, isPluginApplicableToEntity as isLighthouseAvailable, } from '@backstage/plugin-lighthouse/'; -import { Router as GithubPullRequestsRouter } from '@roadiehq/backstage-plugin-github-pull-requests'; +import { Router as PullRequestsRouter } from '@roadiehq/backstage-plugin-github-pull-requests'; const CICDSwitcher = ({ entity }: { entity: Entity }) => { // This component is just an example of how you can implement your company's logic in entity page. @@ -164,7 +164,7 @@ const ServiceEntityPage = ({ entity }: { entity: Entity }) => ( } + element={} /> ); @@ -204,7 +204,7 @@ const WebsiteEntityPage = ({ entity }: { entity: Entity }) => ( } + element={} /> ); From 69d50967e6a6f63003b6caed8d0f999a2a2d9bbf Mon Sep 17 00:00:00 2001 From: lewtakm Date: Mon, 12 Oct 2020 13:44:09 +0200 Subject: [PATCH 022/139] feat: add pull requests widget to overview page of the component --- packages/app/src/components/catalog/EntityPage.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 393ffe2631..d3e25238c7 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -54,6 +54,10 @@ import { LastLighthouseAuditCard, isPluginApplicableToEntity as isLighthouseAvailable, } from '@backstage/plugin-lighthouse/'; +import { + isPluginApplicableToEntity as isPullRequestsAvailable, + PullRequestsStatsCard, +} from '@roadiehq/backstage-plugin-github-pull-requests'; const CICDSwitcher = ({ entity }: { entity: Entity }) => { // This component is just an example of how you can implement your company's logic in entity page. @@ -125,6 +129,11 @@ const OverviewContent = ({ entity }: { entity: Entity }) => ( )} + {isPullRequestsAvailable(entity) && ( + + + + )} ); From 74a29db272aa5e9cb18ef2fcfd2c298bf1477324 Mon Sep 17 00:00:00 2001 From: lewtakm Date: Mon, 12 Oct 2020 14:04:40 +0200 Subject: [PATCH 023/139] fix: bumb package version --- packages/app/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/package.json b/packages/app/package.json index a3b150cef6..bc0b50eaf5 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -34,7 +34,7 @@ "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@octokit/rest": "^18.0.0", - "@roadiehq/backstage-plugin-github-pull-requests": "^0.4.3", + "@roadiehq/backstage-plugin-github-pull-requests": "^0.5.1", "@roadiehq/backstage-plugin-travis-ci": "^0.2.3", "dayjs": "^1.9.1", "history": "^5.0.0", From 689b2f50e528d45c9d2e9b31da374b8d0b580f01 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Mon, 12 Oct 2020 14:27:59 +0200 Subject: [PATCH 024/139] Run vale linter only if vale is installed --- package.json | 3 +- scripts/check-docs-quality.js | 86 ++++++++++++++++++++--------------- yarn.lock | 5 ++ 3 files changed, 57 insertions(+), 37 deletions(-) diff --git a/package.json b/package.json index 371646074c..96e02e33f9 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@changesets/cli": "^2.11.0", "@spotify/eslint-config-oss": "^1.0.1", "@spotify/prettier-config": "^8.0.0", + "command-exists": "^1.2.9", "concurrently": "^5.2.0", "fs-extra": "^9.0.0", "husky": "^4.2.3", @@ -65,7 +66,7 @@ "prettier --write" ], "*.md": [ - "vale" + "node ./scripts/check-docs-quality" ] }, "jest": { diff --git a/scripts/check-docs-quality.js b/scripts/check-docs-quality.js index 61968a2ac0..4f5f689105 100755 --- a/scripts/check-docs-quality.js +++ b/scripts/check-docs-quality.js @@ -16,6 +16,8 @@ */ const { execSync, spawnSync } = require('child_process'); const path = require('path'); +// eslint-disable-next-line import/no-extraneous-dependencies +const commandExists = require('command-exists'); const listFilesTrackedByGit = 'git ls-files'; @@ -23,46 +25,58 @@ const inheritStdIo = { stdio: 'inherit', }; +const LINT_SKIPPED_MESSAGE = + 'Skipping documentation quality check (vale not found). Install vale linter (https://docs.errata.ai/vale/install) to enable.\n'; const ERROR_MESSAGE = - 'Please install vale linter(https://docs.errata.ai/vale/install). Ignore this message if already installed.\n'; + 'Language linter (vale) generated errors. Please check the errors and review any markdown files that you changed.\n'; -// xargs is not supported by shx. -if (process.platform === 'win32') { - const validMDFilesCommand = `${listFilesTrackedByGit} | .\\node_modules\\.bin\\shx grep ".md"`; - try { - // get list of all md files except in directories of gitignore. - let filesToLint = execSync(validMDFilesCommand, { - stdio: ['ignore', 'pipe', 'inherit'], - }); +// Proceed with the script only if Vale linter is installed. Limit the friction and surprises caused by the script. +commandExists('vale') + .catch(() => { + console.log(LINT_SKIPPED_MESSAGE); + process.exit(0); + }) + .then(() => { + // xargs is not supported by shx. + if (process.platform === 'win32') { + const validMDFilesCommand = `${listFilesTrackedByGit} | .\\node_modules\\.bin\\shx grep ".md"`; + try { + // get list of all md files except in directories of gitignore. + let filesToLint = execSync(validMDFilesCommand, { + stdio: ['ignore', 'pipe', 'inherit'], + }); - // set all file(s) path as absolute path - filesToLint = filesToLint - .toString() - .split('\n') - .map(filepath => (filepath ? path.join(process.cwd(), filepath) : null)) - .filter(Boolean); + // set all file(s) path as absolute path + filesToLint = filesToLint + .toString() + .split('\n') + .map(filepath => + filepath ? path.join(process.cwd(), filepath) : null, + ) + .filter(Boolean); - const output = spawnSync('vale', filesToLint, inheritStdIo); + const output = spawnSync('vale', filesToLint, inheritStdIo); - // if the command does not succeed - if (output.status !== 0) { - // if it contains system level error. [in this case vale does not exist] - if (output.error) { - console.error(ERROR_MESSAGE); + // if the command does not succeed + if (output.status !== 0) { + // if it contains system level error. [in this case vale does not exist] + if (output.error) { + console.error(ERROR_MESSAGE); + } + process.exit(1); + } + } catch (e) { + console.error(e.message); + process.exit(1); + } + } else { + const validMDFilesCommand = `${listFilesTrackedByGit} | ./node_modules/.bin/shx grep ".md"`; + // use xargs + try { + execSync(`${validMDFilesCommand} | xargs vale`, inheritStdIo); + } catch (e) { + console.error(ERROR_MESSAGE); + process.exit(1); } - process.exit(1); } - } catch (e) { - console.error(e.message); - process.exit(1); - } -} else { - const validMDFilesCommand = `${listFilesTrackedByGit} | ./node_modules/.bin/shx grep ".md"`; - // use xargs - try { - execSync(`${validMDFilesCommand} | xargs vale`, inheritStdIo); - } catch (e) { - console.error(ERROR_MESSAGE); - process.exit(1); - } -} + }); diff --git a/yarn.lock b/yarn.lock index 7dbc7f964a..859c26c6c4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8537,6 +8537,11 @@ command-exists-promise@^2.0.2: resolved "https://registry.npmjs.org/command-exists-promise/-/command-exists-promise-2.0.2.tgz#7beecc4b218299f3c61fa69a4047aa0b36a64a99" integrity sha512-T6PB6vdFrwnHXg/I0kivM3DqaCGZLjjYSOe0a5WgFKcz1sOnmOeIjnhQPXVXX3QjVbLyTJ85lJkX6lUpukTzaA== +command-exists@^1.2.9: + version "1.2.9" + resolved "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz#c50725af3808c8ab0260fd60b01fbfa25b954f69" + integrity sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w== + commander@^2.11.0, commander@^2.19.0, commander@^2.20.0, commander@^2.20.3, commander@~2.20.3: version "2.20.3" resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" From 4a867ed4a4d4b68db4b02fb9f8f8d7a2145e3515 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 12 Oct 2020 15:45:19 +0200 Subject: [PATCH 025/139] chore: fix comments --- .../src/database/CommonDatabase.ts | 106 +++++++++--------- plugins/catalog-backend/src/service/router.ts | 9 +- 2 files changed, 54 insertions(+), 61 deletions(-) diff --git a/plugins/catalog-backend/src/database/CommonDatabase.ts b/plugins/catalog-backend/src/database/CommonDatabase.ts index 563fd24c80..dca5072b30 100644 --- a/plugins/catalog-backend/src/database/CommonDatabase.ts +++ b/plugins/catalog-backend/src/database/CommonDatabase.ts @@ -219,64 +219,62 @@ export class CommonDatabase implements Database { let entitiesQuery = tx('entities'); - if (filters && Object.keys(filters).length) { - for (const [matchKey, matchVal] of Object.entries(filters)) { - const key = matchKey.toLowerCase().replace(/[*]/g, '%'); - const keyOp = key.includes('*') ? 'like' : '='; - const values = Array.isArray(matchVal) ? matchVal : [matchVal]; + for (const [matchKey, matchVal] of Object.entries(filters ?? {})) { + const key = matchKey.toLowerCase().replace(/[*]/g, '%'); + const keyOp = key.includes('%') ? 'like' : '='; + const values = Array.isArray(matchVal) ? matchVal : [matchVal]; - let matchNulls = false; - const matchIn: string[] = []; - const matchLike: string[] = []; + let matchNulls = false; + const matchIn: string[] = []; + const matchLike: string[] = []; - for (const value of values) { - if (!value) { - matchNulls = true; - } else if (value.includes('*')) { - matchLike.push(value.toLowerCase().replace(/[*]/g, '%')); - } else { - matchIn.push(value.toLowerCase()); - } + for (const value of values) { + if (!value) { + matchNulls = true; + } else if (value.includes('*')) { + matchLike.push(value.toLowerCase().replace(/[*]/g, '%')); + } else { + matchIn.push(value.toLowerCase()); } - - // NOTE(freben): This used to be a set of OUTER JOIN, which may seem to - // make a lot of sense. However, it had abysmal performance on sqlite - // when datasets grew large, so we're using IN instead. - const matchQuery = tx('entities_search') - .select('entity_id') - .where(function keyFilter() { - this.andWhere('key', keyOp, key); - this.andWhere(function valueFilter() { - if (matchIn.length === 1) { - this.orWhere({ value: matchIn[0] }); - } else if (matchIn.length > 1) { - this.orWhereIn('value', matchIn); - } - if (matchLike.length) { - for (const x of matchLike) { - this.orWhere('value', 'like', tx.raw('?', [x])); - } - } - if (matchNulls) { - // Match explicit nulls, and then handle absence separately below - this.orWhereNull('value'); - } - }); - }); - - // Handle absence as nulls as well - entitiesQuery = entitiesQuery.andWhere(function match() { - this.whereIn('id', matchQuery); - if (matchNulls) { - this.orWhereNotIn( - 'id', - tx('entities_search') - .select('entity_id') - .where('key', keyOp, key), - ); - } - }); } + + // NOTE(freben): This used to be a set of OUTER JOIN, which may seem to + // make a lot of sense. However, it had abysmal performance on sqlite + // when datasets grew large, so we're using IN instead. + const matchQuery = tx('entities_search') + .select('entity_id') + .where(function keyFilter() { + this.andWhere('key', keyOp, key); + this.andWhere(function valueFilter() { + if (matchIn.length === 1) { + this.orWhere({ value: matchIn[0] }); + } else if (matchIn.length > 1) { + this.orWhereIn('value', matchIn); + } + if (matchLike.length) { + for (const x of matchLike) { + this.orWhere('value', 'like', tx.raw('?', [x])); + } + } + if (matchNulls) { + // Match explicit nulls, and then handle absence separately below + this.orWhereNull('value'); + } + }); + }); + + // Handle absence as nulls as well + entitiesQuery = entitiesQuery.andWhere(function match() { + this.whereIn('id', matchQuery); + if (matchNulls) { + this.orWhereNotIn( + 'id', + tx('entities_search') + .select('entity_id') + .where('key', keyOp, key), + ); + } + }); } const rows = await entitiesQuery diff --git a/plugins/catalog-backend/src/service/router.ts b/plugins/catalog-backend/src/service/router.ts index 94dc76ab9d..a262e9b5f4 100644 --- a/plugins/catalog-backend/src/service/router.ts +++ b/plugins/catalog-backend/src/service/router.ts @@ -123,7 +123,7 @@ export async function createRouter( function translateQueryToEntityFilters( request: express.Request, ): EntityFilters { - const filters: EntityFilters = {}; + const filters: Record = {}; for (const [key, valueOrValues] of Object.entries(request.query)) { const values = Array.isArray(valueOrValues) @@ -134,12 +134,7 @@ function translateQueryToEntityFilters( throw new InputError('Complex query parameters are not supported'); } - // This one always emits arrays - let matchers = filters[key] as (string | null)[]; - if (!matchers) { - matchers = []; - filters[key] = matchers; - } + const matchers = key in filters ? filters[key] : (filters[key] = []); matchers.push(...(values.map(v => v || null) as (string | null)[])); } From 0b2a13b5c9ef12b51f91b03253e9f9288546ce67 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 12 Oct 2020 22:19:50 +0200 Subject: [PATCH 026/139] chore(cli): backend plugins should have the backend suffix --- packages/cli/src/commands/create-plugin/createPlugin.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/commands/create-plugin/createPlugin.ts b/packages/cli/src/commands/create-plugin/createPlugin.ts index cbaf956bdb..491e399b1f 100644 --- a/packages/cli/src/commands/create-plugin/createPlugin.ts +++ b/packages/cli/src/commands/create-plugin/createPlugin.ts @@ -224,9 +224,12 @@ export default async (cmd: Command) => { } const answers: Answers = await inquirer.prompt(questions); - const name = cmd.scope + const pluginName = cmd.scope ? `@${cmd.scope.replace(/^@/, '')}/plugin-${answers.id}` : `plugin-${answers.id}`; + + const name = cmd.backend ? `${pluginName}-backend` : pluginName; + const npmRegistry = cmd.npmRegistry && cmd.scope ? cmd.npmRegistry : ''; const privatePackage = cmd.private === false ? false : true; const isMonoRepo = await fs.pathExists(paths.resolveTargetRoot('lerna.json')); From a8a4f4138035d4e141b9adeee9ca30ac6772a6fe Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 12 Oct 2020 22:27:21 +0200 Subject: [PATCH 027/139] chore(cli): folder name etc should have the suffix too --- .../commands/create-plugin/createPlugin.ts | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/cli/src/commands/create-plugin/createPlugin.ts b/packages/cli/src/commands/create-plugin/createPlugin.ts index 491e399b1f..057e36ceb9 100644 --- a/packages/cli/src/commands/create-plugin/createPlugin.ts +++ b/packages/cli/src/commands/create-plugin/createPlugin.ts @@ -224,12 +224,11 @@ export default async (cmd: Command) => { } const answers: Answers = await inquirer.prompt(questions); - const pluginName = cmd.scope - ? `@${cmd.scope.replace(/^@/, '')}/plugin-${answers.id}` - : `plugin-${answers.id}`; - - const name = cmd.backend ? `${pluginName}-backend` : pluginName; + const pluginId = cmd.backend ? `${answers.id}-backend` : answers.id; + const name = cmd.scope + ? `@${cmd.scope.replace(/^@/, '')}/plugin-${pluginId}` + : `plugin-${pluginId}`; const npmRegistry = cmd.npmRegistry && cmd.scope ? cmd.npmRegistry : ''; const privatePackage = cmd.private === false ? false : true; const isMonoRepo = await fs.pathExists(paths.resolveTargetRoot('lerna.json')); @@ -239,10 +238,10 @@ export default async (cmd: Command) => { ? 'templates/default-backend-plugin' : 'templates/default-plugin', ); - const tempDir = resolvePath(os.tmpdir(), answers.id); + const tempDir = resolvePath(os.tmpdir(), pluginId); const pluginDir = isMonoRepo - ? paths.resolveTargetRoot('plugins', answers.id) - : paths.resolveTargetRoot(answers.id); + ? paths.resolveTargetRoot('plugins', pluginId) + : paths.resolveTargetRoot(pluginId); const ownerIds = parseOwnerIds(answers.owner); const { version } = isMonoRepo ? await fs.readJson(paths.resolveTargetRoot('lerna.json')) @@ -270,7 +269,7 @@ export default async (cmd: Command) => { }); Task.section('Moving to final location'); - await movePlugin(tempDir, pluginDir, answers.id); + await movePlugin(tempDir, pluginDir, pluginId); Task.section('Building the plugin'); await buildPlugin(pluginDir); @@ -280,13 +279,13 @@ export default async (cmd: Command) => { await addPluginDependencyToApp(paths.targetRoot, name, version); Task.section('Import plugin in app'); - await addPluginToApp(paths.targetRoot, answers.id, name); + await addPluginToApp(paths.targetRoot, pluginId, name); } if (ownerIds && ownerIds.length) { await addCodeownersEntry( codeownersPath!, - `/plugins/${answers.id}`, + `/plugins/${pluginId}`, ownerIds, ); } From bd3864385b3085c02d47094d40fdd0c986068bd1 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 12 Oct 2020 22:51:17 +0200 Subject: [PATCH 028/139] chore(e2e): fix the e2e test --- packages/e2e-test/src/commands/run.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/e2e-test/src/commands/run.ts b/packages/e2e-test/src/commands/run.ts index 2a0f641014..7111ff1867 100644 --- a/packages/e2e-test/src/commands/run.ts +++ b/packages/e2e-test/src/commands/run.ts @@ -56,7 +56,7 @@ export async function run() { const pluginName = await createPlugin('test-plugin', appDir); print('Creating a Backstage Backend Plugin'); - await createPlugin('test-backend-plugin', appDir, ['--backend']); + await createPlugin('test-plugin', appDir, ['--backend']); print('Starting the app'); await testAppServe(pluginName, appDir); @@ -265,13 +265,16 @@ async function createPlugin( print('Waiting for plugin create script to be done'); await waitForExit(child); - const pluginDir = resolvePath(appDir, 'plugins', pluginName); + const canonicalName = options.includes('--backend') + ? `${pluginName}-backend}` + : pluginName; + const pluginDir = resolvePath(appDir, 'plugins', canonicalName); for (const cmd of [['tsc'], ['lint'], ['test', '--no-watch']]) { print(`Running 'yarn ${cmd.join(' ')}' in newly created plugin`); await runPlain(['yarn', ...cmd], { cwd: pluginDir }); } - return pluginName; + return canonicalName; } finally { child.kill(); } From b7d15b4f1ac20a7c0abab11de272a7846b20fc03 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 13 Oct 2020 03:02:58 +0200 Subject: [PATCH 029/139] chore(cli): remove superfluous bracket that was causing all the things --- packages/e2e-test/src/commands/run.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/e2e-test/src/commands/run.ts b/packages/e2e-test/src/commands/run.ts index 7111ff1867..cfe0ad9685 100644 --- a/packages/e2e-test/src/commands/run.ts +++ b/packages/e2e-test/src/commands/run.ts @@ -266,9 +266,11 @@ async function createPlugin( await waitForExit(child); const canonicalName = options.includes('--backend') - ? `${pluginName}-backend}` + ? `${pluginName}-backend` : pluginName; + const pluginDir = resolvePath(appDir, 'plugins', canonicalName); + for (const cmd of [['tsc'], ['lint'], ['test', '--no-watch']]) { print(`Running 'yarn ${cmd.join(' ')}' in newly created plugin`); await runPlain(['yarn', ...cmd], { cwd: pluginDir }); From ef8304af83488013e6a9b45c8b2134209032b74f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Oct 2020 04:37:50 +0000 Subject: [PATCH 030/139] chore(deps-dev): bump @types/passport from 1.0.3 to 1.0.4 Bumps [@types/passport](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/passport) from 1.0.3 to 1.0.4. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/passport) Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 4832a549b8..810dc62ea4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5354,9 +5354,9 @@ "@types/passport" "*" "@types/passport@*", "@types/passport@^1.0.3": - version "1.0.3" - resolved "https://registry.npmjs.org/@types/passport/-/passport-1.0.3.tgz#e459ed6c262bf0686684d1b05901be0d0b192a9c" - integrity sha512-nyztuxtDPQv9utCzU0qW7Gl8BY2Dn8BKlYAFFyxKipFxjaVd96celbkLCV/tRqqBUZ+JB8If3UfgV8347DTo3Q== + version "1.0.4" + resolved "https://registry.npmjs.org/@types/passport/-/passport-1.0.4.tgz#1b35c4e197560d3974fa5f71711b6e9cce0711f0" + integrity sha512-h5OfAbfBBYSzjeU0GTuuqYEk9McTgWeGQql9g3gUw2/NNCfD7VgExVRYJVVeU13Twn202Mvk9BT0bUrl30sEgA== dependencies: "@types/express" "*" From a0a3e6d6cacdeb2218065c6bacf6be1485bff175 Mon Sep 17 00:00:00 2001 From: J Shamsul Bahri Date: Tue, 13 Oct 2020 12:49:16 +0800 Subject: [PATCH 031/139] Fix: SAML provider postMessageResponse localhost (#2870) * make saml provider path from globalConfig * fix:(auth-backend/saml) change hardcoded localhost Co-authored-by: JShamsul --- plugins/auth-backend/src/providers/saml/provider.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index 59e46a86d7..a8edc9a714 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -41,8 +41,10 @@ type SamlInfo = { export class SamlAuthProvider implements AuthProviderRouteHandlers { private readonly strategy: SamlStrategy; private readonly tokenIssuer: TokenIssuer; + private readonly appUrl: string; constructor(options: SAMLProviderOptions) { + this.appUrl = options.appUrl; this.tokenIssuer = options.tokenIssuer; this.strategy = new SamlStrategy({ ...options }, (( profile: SamlProfile, @@ -82,7 +84,7 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { claims: { sub: id }, }); - return postMessageResponse(res, 'http://localhost:3000', { + return postMessageResponse(res, this.appUrl, { type: 'authorization_response', response: { providerInfo: {}, @@ -91,7 +93,7 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { }, }); } catch (error) { - return postMessageResponse(res, 'http://localhost:3000', { + return postMessageResponse(res, this.appUrl, { type: 'authorization_response', error: { name: error.name, @@ -115,6 +117,7 @@ type SAMLProviderOptions = { issuer: string; path: string; tokenIssuer: TokenIssuer; + appUrl: string; }; export const createSamlProvider: AuthProviderFactory = ({ @@ -131,6 +134,7 @@ export const createSamlProvider: AuthProviderFactory = ({ issuer, path: `${url.pathname}/${providerId}/handler/frame`, tokenIssuer, + appUrl: globalConfig.appUrl, }; return new SamlAuthProvider(opts); From 321da5aff61051e080b90d90d9751006948afe0c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Oct 2020 06:52:44 +0200 Subject: [PATCH 032/139] chore(deps): bump @testing-library/jest-dom from 5.10.1 to 5.11.4 (#2792) Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 5.10.1 to 5.11.4. - [Release notes](https://github.com/testing-library/jest-dom/releases) - [Changelog](https://github.com/testing-library/jest-dom/blob/master/CHANGELOG.md) - [Commits](https://github.com/testing-library/jest-dom/compare/v5.10.1...v5.11.4) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 50 +++++++++++++++++++++++--------------------------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/yarn.lock b/yarn.lock index 4832a549b8..c0630e742b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4545,17 +4545,16 @@ pretty-format "^26.4.2" "@testing-library/jest-dom@^5.10.1": - version "5.10.1" - resolved "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.10.1.tgz#6508a9f007bd74e5d3c0b3135b668027ab663989" - integrity sha512-uv9lLAnEFRzwUTN/y9lVVXVXlEzazDkelJtM5u92PsGkEasmdI+sfzhZHxSDzlhZVTrlLfuMh2safMr8YmzXLg== + version "5.11.4" + resolved "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.11.4.tgz#f325c600db352afb92995c2576022b35621ddc99" + integrity sha512-6RRn3epuweBODDIv3dAlWjOEHQLpGJHB2i912VS3JQtsD22+ENInhdDNl4ZZQiViLlIfFinkSET/J736ytV9sw== dependencies: "@babel/runtime" "^7.9.2" "@types/testing-library__jest-dom" "^5.9.1" + aria-query "^4.2.2" chalk "^3.0.0" - css "^2.2.4" + css "^3.0.0" css.escape "^1.5.1" - jest-diff "^25.1.0" - jest-matcher-utils "^25.1.0" lodash "^4.17.15" redent "^3.0.0" @@ -9227,15 +9226,14 @@ css.escape@1.5.1, css.escape@^1.5.1: resolved "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" integrity sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s= -css@^2.2.4: - version "2.2.4" - resolved "https://registry.npmjs.org/css/-/css-2.2.4.tgz#c646755c73971f2bba6a601e2cf2fd71b1298929" - integrity sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw== +css@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/css/-/css-3.0.0.tgz#4447a4d58fdd03367c516ca9f64ae365cee4aa5d" + integrity sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ== dependencies: - inherits "^2.0.3" + inherits "^2.0.4" source-map "^0.6.1" - source-map-resolve "^0.5.2" - urix "^0.1.0" + source-map-resolve "^0.6.0" cssesc@^3.0.0: version "3.0.0" @@ -14121,7 +14119,7 @@ jest-css-modules@^2.1.0: dependencies: identity-obj-proxy "3.0.0" -jest-diff@^25.1.0, jest-diff@^25.2.1: +jest-diff@^25.2.1: version "25.5.0" resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-25.5.0.tgz#1dd26ed64f96667c068cef026b677dfa01afcfa9" integrity sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A== @@ -14200,7 +14198,7 @@ jest-fetch-mock@^3.0.3: cross-fetch "^3.0.4" promise-polyfill "^8.1.3" -jest-get-type@^25.1.0, jest-get-type@^25.2.6: +jest-get-type@^25.2.6: version "25.2.6" resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.2.6.tgz#0b0a32fab8908b44d508be81681487dbabb8d877" integrity sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig== @@ -14263,16 +14261,6 @@ jest-leak-detector@^26.5.2: jest-get-type "^26.3.0" pretty-format "^26.5.2" -jest-matcher-utils@^25.1.0: - version "25.1.0" - resolved "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-25.1.0.tgz#fa5996c45c7193a3c24e73066fc14acdee020220" - integrity sha512-KGOAFcSFbclXIFE7bS4C53iYobKI20ZWleAdAFun4W1Wz1Kkej8Ng6RRbhL8leaEvIOjGXhGf/a1JjO8bkxIWQ== - dependencies: - chalk "^3.0.0" - jest-diff "^25.1.0" - jest-get-type "^25.1.0" - pretty-format "^25.1.0" - jest-matcher-utils@^26.5.2: version "26.5.2" resolved "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-26.5.2.tgz#6aa2c76ce8b9c33e66f8856ff3a52bab59e6c85a" @@ -18557,7 +18545,7 @@ pretty-error@^2.1.1: renderkid "^2.0.1" utila "~0.4" -pretty-format@^25.1.0, pretty-format@^25.2.1, pretty-format@^25.5.0: +pretty-format@^25.2.1, pretty-format@^25.5.0: version "25.5.0" resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz#7873c1d774f682c34b8d48b6743a2bf2ac55791a" integrity sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ== @@ -21036,7 +21024,7 @@ source-list-map@^2.0.0: resolved "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== -source-map-resolve@^0.5.0, source-map-resolve@^0.5.2: +source-map-resolve@^0.5.0: version "0.5.3" resolved "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== @@ -21047,6 +21035,14 @@ source-map-resolve@^0.5.0, source-map-resolve@^0.5.2: source-map-url "^0.4.0" urix "^0.1.0" +source-map-resolve@^0.6.0: + version "0.6.0" + resolved "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz#3d9df87e236b53f16d01e58150fc7711138e5ed2" + integrity sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w== + dependencies: + atob "^2.1.2" + decode-uri-component "^0.2.0" + source-map-support@^0.5.16, source-map-support@^0.5.17, source-map-support@^0.5.6, source-map-support@~0.5.12: version "0.5.19" resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" From b041aab8adea2e199564011cdb0f445a0a614479 Mon Sep 17 00:00:00 2001 From: Sebastian Qvarfordt Date: Tue, 13 Oct 2020 09:07:14 +0200 Subject: [PATCH 033/139] [TechDocs] Remove all git preparers and use a commonGit preparer instead (#2856) * Remove all git preparers and use a commonGit preparer instead * Added more tests * Updated default-app template --- packages/backend/src/plugins/techdocs.ts | 15 ++--- .../packages/backend/src/plugins/techdocs.ts | 7 +- .../src/techdocs/stages/prepare/azure.test.ts | 63 ------------------ .../src/techdocs/stages/prepare/azure.ts | 55 ---------------- .../{github.test.ts => commonGit.test.ts} | 36 ++++++++++- .../prepare/{gitlab.ts => commonGit.ts} | 9 +-- .../src/techdocs/stages/prepare/github.ts | 55 ---------------- .../techdocs/stages/prepare/gitlab.test.ts | 64 ------------------- .../src/techdocs/stages/prepare/index.ts | 4 +- 9 files changed, 46 insertions(+), 262 deletions(-) delete mode 100644 plugins/techdocs-backend/src/techdocs/stages/prepare/azure.test.ts delete mode 100644 plugins/techdocs-backend/src/techdocs/stages/prepare/azure.ts rename plugins/techdocs-backend/src/techdocs/stages/prepare/{github.test.ts => commonGit.test.ts} (59%) rename plugins/techdocs-backend/src/techdocs/stages/prepare/{gitlab.ts => commonGit.ts} (83%) delete mode 100644 plugins/techdocs-backend/src/techdocs/stages/prepare/github.ts delete mode 100644 plugins/techdocs-backend/src/techdocs/stages/prepare/gitlab.test.ts diff --git a/packages/backend/src/plugins/techdocs.ts b/packages/backend/src/plugins/techdocs.ts index 6e36c26f39..dea31c763b 100644 --- a/packages/backend/src/plugins/techdocs.ts +++ b/packages/backend/src/plugins/techdocs.ts @@ -21,9 +21,7 @@ import { Generators, LocalPublish, TechdocsGenerator, - GithubPreparer, - GitlabPreparer, - AzurePreparer, + CommonGitPreparer, } from '@backstage/plugin-techdocs-backend'; import { PluginEnvironment } from '../types'; import Docker from 'dockerode'; @@ -38,14 +36,13 @@ export default async function createPlugin({ generators.register('techdocs', techdocsGenerator); const preparers = new Preparers(); - const githubPreparer = new GithubPreparer(logger); - const gitlabPreparer = new GitlabPreparer(logger); - const azurePreparer = new AzurePreparer(logger); + const commonGitPreparer = new CommonGitPreparer(logger); + const directoryPreparer = new DirectoryPreparer(logger); preparers.register('dir', directoryPreparer); - preparers.register('github', githubPreparer); - preparers.register('gitlab', gitlabPreparer); - preparers.register('azure/api', azurePreparer); + preparers.register('github', commonGitPreparer); + preparers.register('gitlab', commonGitPreparer); + preparers.register('azure/api', commonGitPreparer); const publisher = new LocalPublish(logger); diff --git a/packages/create-app/templates/default-app/packages/backend/src/plugins/techdocs.ts b/packages/create-app/templates/default-app/packages/backend/src/plugins/techdocs.ts index b522992a75..5506228962 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/plugins/techdocs.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/plugins/techdocs.ts @@ -1,7 +1,7 @@ import { createRouter, DirectoryPreparer, - GithubPreparer, + CommonGitPreparer, Preparers, Generators, LocalPublish, @@ -22,10 +22,11 @@ export default async function createPlugin({ const preparers = new Preparers(); const directoryPreparer = new DirectoryPreparer(logger); - const githubPreparer = new GithubPreparer(logger); + const commonGitPreparer = new CommonGitPreparer(logger); preparers.register('dir', directoryPreparer); - preparers.register('github', githubPreparer); + preparers.register('github', commonGitPreparer); + preparers.register('gitlab', commonGitPreparer); const publisher = new LocalPublish(logger); diff --git a/plugins/techdocs-backend/src/techdocs/stages/prepare/azure.test.ts b/plugins/techdocs-backend/src/techdocs/stages/prepare/azure.test.ts deleted file mode 100644 index 39a3d5c7fa..0000000000 --- a/plugins/techdocs-backend/src/techdocs/stages/prepare/azure.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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 { getVoidLogger } from '@backstage/backend-common'; -import { AzurePreparer } from './azure'; -import { checkoutGitRepository } from '../../../helpers'; - -function normalizePath(path: string) { - return path - .replace(/^[a-z]:/i, '') - .split('\\') - .join('/'); -} - -jest.mock('../../../helpers', () => ({ - ...jest.requireActual<{}>('../../../helpers'), - checkoutGitRepository: jest.fn(() => '/tmp/backstage-repo/org/name/branch'), -})); - -const createMockEntity = (annotations = {}) => { - return { - apiVersion: 'version', - kind: 'TestKind', - metadata: { - name: 'test-component-name', - annotations: { - ...annotations, - }, - }, - }; -}; - -const logger = getVoidLogger(); - -describe('Azure DevOps preparer', () => { - it('should prepare temp docs path from Azure DevOps repo', async () => { - const preparer = new AzurePreparer(logger); - - const mockEntity = createMockEntity({ - 'backstage.io/techdocs-ref': - 'azure/api:https://dev.azure.com/backstage-org/backstage-project/_git/template-repo?path=%2Ftemplate.yaml', - }); - - const tempDocsPath = await preparer.prepare(mockEntity); - expect(checkoutGitRepository).toHaveBeenCalledTimes(1); - expect(normalizePath(tempDocsPath)).toEqual( - '/tmp/backstage-repo/org/name/branch/template.yaml', - ); - }); -}); diff --git a/plugins/techdocs-backend/src/techdocs/stages/prepare/azure.ts b/plugins/techdocs-backend/src/techdocs/stages/prepare/azure.ts deleted file mode 100644 index 18e6c3c755..0000000000 --- a/plugins/techdocs-backend/src/techdocs/stages/prepare/azure.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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 path from 'path'; -import { Entity } from '@backstage/catalog-model'; -import { InputError } from '@backstage/backend-common'; -import { PreparerBase } from './types'; -import parseGitUrl from 'git-url-parse'; -import { - parseReferenceAnnotation, - checkoutGitRepository, -} from '../../../helpers'; - -import { Logger } from 'winston'; - -export class AzurePreparer implements PreparerBase { - private readonly logger: Logger; - - constructor(logger: Logger) { - this.logger = logger; - } - - async prepare(entity: Entity): Promise { - const { type, target } = parseReferenceAnnotation( - 'backstage.io/techdocs-ref', - entity, - ); - - if (type !== 'azure/api') { - throw new InputError(`Wrong target type: ${type}, should be 'azure/api'`); - } - - try { - const repoPath = await checkoutGitRepository(target, this.logger); - const parsedGitLocation = parseGitUrl(target); - - return path.join(repoPath, parsedGitLocation.filepath); - } catch (error) { - this.logger.debug(`Repo checkout failed with error ${error.message}`); - throw error; - } - } -} diff --git a/plugins/techdocs-backend/src/techdocs/stages/prepare/github.test.ts b/plugins/techdocs-backend/src/techdocs/stages/prepare/commonGit.test.ts similarity index 59% rename from plugins/techdocs-backend/src/techdocs/stages/prepare/github.test.ts rename to plugins/techdocs-backend/src/techdocs/stages/prepare/commonGit.test.ts index a657cd1697..cec1110f97 100644 --- a/plugins/techdocs-backend/src/techdocs/stages/prepare/github.test.ts +++ b/plugins/techdocs-backend/src/techdocs/stages/prepare/commonGit.test.ts @@ -15,7 +15,7 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import { GithubPreparer } from './github'; +import { CommonGitPreparer } from './commonGit'; import { checkoutGitRepository } from '../../../helpers'; function normalizePath(path: string) { @@ -45,9 +45,9 @@ const createMockEntity = (annotations = {}) => { const logger = getVoidLogger(); -describe('github preparer', () => { +describe('commonGit preparer', () => { it('should prepare temp docs path from github repo', async () => { - const preparer = new GithubPreparer(logger); + const preparer = new CommonGitPreparer(logger); const mockEntity = createMockEntity({ 'backstage.io/techdocs-ref': @@ -60,4 +60,34 @@ describe('github preparer', () => { '/tmp/backstage-repo/org/name/branch/plugins/techdocs-backend/examples/documented-component', ); }); + + it('should prepare temp docs path from gitlab repo', async () => { + const preparer = new CommonGitPreparer(logger); + + const mockEntity = createMockEntity({ + 'backstage.io/techdocs-ref': + 'gitlab:https://gitlab.com/xesjkeee/go-logger/blob/master/catalog-info.yaml', + }); + + const tempDocsPath = await preparer.prepare(mockEntity); + expect(checkoutGitRepository).toHaveBeenCalledTimes(2); + expect(normalizePath(tempDocsPath)).toEqual( + '/tmp/backstage-repo/org/name/branch/catalog-info.yaml', + ); + }); + + it('should prepare temp docs path from azure repo', async () => { + const preparer = new CommonGitPreparer(logger); + + const mockEntity = createMockEntity({ + 'backstage.io/techdocs-ref': + 'azure/api:https://dev.azure.com/backstage-org/backstage-project/_git/template-repo?path=%2Ftemplate.yaml', + }); + + const tempDocsPath = await preparer.prepare(mockEntity); + expect(checkoutGitRepository).toHaveBeenCalledTimes(3); + expect(normalizePath(tempDocsPath)).toEqual( + '/tmp/backstage-repo/org/name/branch/template.yaml', + ); + }); }); diff --git a/plugins/techdocs-backend/src/techdocs/stages/prepare/gitlab.ts b/plugins/techdocs-backend/src/techdocs/stages/prepare/commonGit.ts similarity index 83% rename from plugins/techdocs-backend/src/techdocs/stages/prepare/gitlab.ts rename to plugins/techdocs-backend/src/techdocs/stages/prepare/commonGit.ts index 56f3793fbb..d9ba96a031 100644 --- a/plugins/techdocs-backend/src/techdocs/stages/prepare/gitlab.ts +++ b/plugins/techdocs-backend/src/techdocs/stages/prepare/commonGit.ts @@ -15,7 +15,6 @@ */ import path from 'path'; import { Entity } from '@backstage/catalog-model'; -import { InputError } from '@backstage/backend-common'; import { PreparerBase } from './types'; import parseGitUrl from 'git-url-parse'; import { @@ -25,7 +24,7 @@ import { import { Logger } from 'winston'; -export class GitlabPreparer implements PreparerBase { +export class CommonGitPreparer implements PreparerBase { private readonly logger: Logger; constructor(logger: Logger) { @@ -33,15 +32,11 @@ export class GitlabPreparer implements PreparerBase { } async prepare(entity: Entity): Promise { - const { type, target } = parseReferenceAnnotation( + const { target } = parseReferenceAnnotation( 'backstage.io/techdocs-ref', entity, ); - if (type !== 'gitlab') { - throw new InputError(`Wrong target type: ${type}, should be 'gitlab'`); - } - try { const repoPath = await checkoutGitRepository(target, this.logger); const parsedGitLocation = parseGitUrl(target); diff --git a/plugins/techdocs-backend/src/techdocs/stages/prepare/github.ts b/plugins/techdocs-backend/src/techdocs/stages/prepare/github.ts deleted file mode 100644 index a0f91a375a..0000000000 --- a/plugins/techdocs-backend/src/techdocs/stages/prepare/github.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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 path from 'path'; -import { Entity } from '@backstage/catalog-model'; -import { InputError } from '@backstage/backend-common'; -import { PreparerBase } from './types'; -import parseGitUrl from 'git-url-parse'; -import { - parseReferenceAnnotation, - checkoutGitRepository, -} from '../../../helpers'; - -import { Logger } from 'winston'; - -export class GithubPreparer implements PreparerBase { - private readonly logger: Logger; - - constructor(logger: Logger) { - this.logger = logger; - } - - async prepare(entity: Entity): Promise { - const { type, target } = parseReferenceAnnotation( - 'backstage.io/techdocs-ref', - entity, - ); - - if (type !== 'github') { - throw new InputError(`Wrong target type: ${type}, should be 'github'`); - } - - try { - const repoPath = await checkoutGitRepository(target, this.logger); - - const parsedGitLocation = parseGitUrl(target); - return path.join(repoPath, parsedGitLocation.filepath); - } catch (error) { - this.logger.debug(`Repo checkout failed with error ${error.message}`); - throw error; - } - } -} diff --git a/plugins/techdocs-backend/src/techdocs/stages/prepare/gitlab.test.ts b/plugins/techdocs-backend/src/techdocs/stages/prepare/gitlab.test.ts deleted file mode 100644 index 75cad50224..0000000000 --- a/plugins/techdocs-backend/src/techdocs/stages/prepare/gitlab.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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 { getVoidLogger } from '@backstage/backend-common'; -import { GitlabPreparer } from './gitlab'; -import { checkoutGitRepository } from '../../../helpers'; - -function normalizePath(path: string) { - return path - .replace(/^[a-z]:/i, '') - .split('\\') - .join('/'); -} - -jest.mock('../../../helpers', () => ({ - ...jest.requireActual<{}>('../../../helpers'), - checkoutGitRepository: jest.fn(() => '/tmp/backstage-repo/org/name/branch'), -})); - -const createMockEntity = (annotations = {}) => { - return { - apiVersion: 'version', - kind: 'TestKind', - metadata: { - name: 'test-component-name', - annotations: { - ...annotations, - }, - }, - }; -}; - -const logger = getVoidLogger(); - -describe('gitlab preparer', () => { - it('should prepare temp docs path from gitlab repo', async () => { - const preparer = new GitlabPreparer(logger); - - // TODO: fix url repo - const mockEntity = createMockEntity({ - 'backstage.io/techdocs-ref': - 'gitlab:https://gitlab.com/xesjkeee/go-logger/blob/master/catalog-info.yaml', - }); - - const tempDocsPath = await preparer.prepare(mockEntity); - expect(checkoutGitRepository).toHaveBeenCalledTimes(1); - expect(normalizePath(tempDocsPath)).toEqual( - '/tmp/backstage-repo/org/name/branch/catalog-info.yaml', - ); - }); -}); diff --git a/plugins/techdocs-backend/src/techdocs/stages/prepare/index.ts b/plugins/techdocs-backend/src/techdocs/stages/prepare/index.ts index 249854f7ee..46e1d84c69 100644 --- a/plugins/techdocs-backend/src/techdocs/stages/prepare/index.ts +++ b/plugins/techdocs-backend/src/techdocs/stages/prepare/index.ts @@ -14,8 +14,6 @@ * limitations under the License. */ export { DirectoryPreparer } from './dir'; -export { GithubPreparer } from './github'; -export { GitlabPreparer } from './gitlab'; -export { AzurePreparer } from './azure'; +export { CommonGitPreparer } from './commonGit'; export { Preparers } from './preparers'; export type { PreparerBuilder, PreparerBase } from './types'; From 46b9ae82ba111a7f9ecfed0bcb419357ab413cc3 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Tue, 13 Oct 2020 09:45:25 +0200 Subject: [PATCH 034/139] Add filters to API Explorer (#2830) * fix: support nested fields in table filter Add support for nested fields like metadata.name to the table filters. Previously only top-level fields were allowed. In addition, support for filtering on fields that are arrays is introduced (like tags). Filters are now distinct. * feat: introduce table filters to the api explorer * fix: allow to close select in multiple mode * fix: export TableFilter --- .../core/src/components/Select/Select.tsx | 48 ++++---- .../core/src/components/Table/Filters.tsx | 2 +- packages/core/src/components/Table/Table.tsx | 107 ++++++++++++++---- packages/core/src/components/Table/index.ts | 4 +- .../ApiExplorerPage/ApiExplorerPage.test.tsx | 2 +- .../ApiExplorerPage/ApiExplorerPage.tsx | 1 - .../ApiExplorerTable.test.tsx | 8 +- .../ApiExplorerTable/ApiExplorerTable.tsx | 27 ++++- 8 files changed, 133 insertions(+), 66 deletions(-) diff --git a/packages/core/src/components/Select/Select.tsx b/packages/core/src/components/Select/Select.tsx index a2efcc7725..1c691340b2 100644 --- a/packages/core/src/components/Select/Select.tsx +++ b/packages/core/src/components/Select/Select.tsx @@ -14,25 +14,23 @@ * limitations under the License. */ -import React, { useEffect, useState } from 'react'; +import { + Checkbox, + Chip, + ClickAwayListener, + FormControl, + InputBase, + MenuItem, + Select, + Typography, +} from '@material-ui/core'; import { createStyles, makeStyles, - withStyles, Theme, + withStyles, } from '@material-ui/core/styles'; - -import { - FormControl, - Select, - MenuItem, - InputBase, - Chip, - Typography, - Checkbox, - ClickAwayListener, -} from '@material-ui/core'; - +import React, { useEffect, useState } from 'react'; import ClosedDropdown from './static/ClosedDropdown'; import OpenedDropdown from './static/OpenedDropdown'; @@ -111,7 +109,7 @@ export const SelectComponent = (props: SelectProps) => { const [value, setValue] = useState( multiple ? [] : '', ); - const [canOpen, setCanOpen] = React.useState(false); + const [isOpen, setOpen] = useState(false); useEffect(() => { setValue(multiple ? [] : ''); @@ -122,19 +120,17 @@ export const SelectComponent = (props: SelectProps) => { onChange(event.target.value); }; - const selectHandleOnOpen = () => { - setCanOpen(previous => { - if (multiple) { + const handleClick = (event: React.ChangeEvent) => { + setOpen(previous => { + if (multiple && !(event.target instanceof HTMLElement)) { return true; } return !previous; }); }; - const handleClickAway = (event: React.ChangeEvent) => { - if (event.target.id !== 'menu-item') { - setCanOpen(false); - } + const handleClickAway = () => { + setOpen(false); }; const handleDelete = (selectedValue: string | number) => () => { @@ -154,8 +150,8 @@ export const SelectComponent = (props: SelectProps) => { displayEmpty multiple={multiple} onChange={handleChange} - onClick={selectHandleOnOpen} - open={canOpen} + onClick={handleClick} + open={isOpen} input={} renderValue={selected => multiple && (value as any[]).length !== 0 ? ( @@ -181,7 +177,7 @@ export const SelectComponent = (props: SelectProps) => { ) } IconComponent={() => - !canOpen ? : + !isOpen ? : } MenuProps={{ anchorOrigin: { @@ -200,7 +196,7 @@ export const SelectComponent = (props: SelectProps) => { )} {items && items.map(item => ( - + {multiple && ( (theme => ({ }, })); -type Without = Pick>; +export type Without = Pick>; export type Filter = { type: 'select' | 'checkbox-tree' | 'multiple-select'; diff --git a/packages/core/src/components/Table/Table.tsx b/packages/core/src/components/Table/Table.tsx index 5d0b7d8344..87ad50e58f 100644 --- a/packages/core/src/components/Table/Table.tsx +++ b/packages/core/src/components/Table/Table.tsx @@ -45,7 +45,9 @@ import MTable, { Options, } from 'material-table'; import React, { forwardRef, useCallback, useEffect, useState } from 'react'; -import { Filters, SelectedFilters } from './Filters'; +import { CheckboxTreeProps } from '../CheckboxTree/CheckboxTree'; +import { SelectProps } from '../Select/Select'; +import { Filter, Filters, SelectedFilters, Without } from './Filters'; const tableIcons = { Add: forwardRef((props, ref: React.Ref) => ( @@ -101,6 +103,23 @@ const tableIcons = { )), }; +// TODO: Material table might already have such a function internally that we can use? +function extractValueByField(data: any, field: string): any | undefined { + const path = field.split('.'); + let value = data[path[0]]; + + for (let i = 1; i < path.length; ++i) { + if (value === undefined) { + return value; + } + + const f = path[i]; + value = value[f]; + } + + return value; +} + const useHeaderStyles = makeStyles(theme => ({ header: { padding: theme.spacing(1, 2, 1, 2.5), @@ -234,11 +253,21 @@ export function Table({ el => !!Object.entries(selectedFilters) .filter(([, value]) => !!value.length) - .every(([key, value]) => { - if (Array.isArray(value)) { - return value.includes(el[getFieldByTitle(key)]); + .every(([key, filterValue]) => { + const fieldValue = extractValueByField( + el, + getFieldByTitle(key) as string, + ); + + if (Array.isArray(fieldValue) && Array.isArray(filterValue)) { + return fieldValue.some(v => filterValue.includes(v)); + } else if (Array.isArray(fieldValue)) { + return fieldValue.includes(filterValue); + } else if (Array.isArray(filterValue)) { + return filterValue.includes(fieldValue); } - return el[getFieldByTitle(key)] === value; + + return fieldValue === filterValue; }), ); setTableData(newData); @@ -248,29 +277,61 @@ export function Table({ setSelectedFiltersLength(selectedFiltersArray.flat().length); }, [data, selectedFilters, getFieldByTitle]); - const constructFilters = (filterConfig: TableFilter[], dataValue: any[]) => { - const extractColumnData = (column: string | keyof T) => - dataValue.map(el => ({ label: el[column], options: [] })); + const constructFilters = ( + filterConfig: TableFilter[], + dataValue: any[], + ): Filter[] => { + const extractDistinctValues = (field: string | keyof T): Set => { + const distinctValues = new Set(); + const addValue = (value: any) => { + if (value !== undefined && value !== null) { + distinctValues.add(value); + } + }; + + dataValue.forEach(el => { + const value = extractValueByField(el, getFieldByTitle(field) as string); + + if (Array.isArray(value)) { + (value as []).forEach(addValue); + } else { + addValue(value); + } + }); + + return distinctValues; + }; + + const constructCheckboxTree = ( + filter: TableFilter, + ): Without => ({ + label: filter.column, + subCategories: [...extractDistinctValues(filter.column)].map(v => ({ + label: v, + options: [], + })), + }); + + const constructSelect = ( + filter: TableFilter, + ): Without => { + return { + placeholder: 'All results', + label: filter.column, + multiple: filter.type === 'multiple-select', + items: [...extractDistinctValues(filter.column)].map(value => ({ + label: value, + value, + })), + }; + }; return filterConfig.map(filter => ({ type: filter.type, element: filter.type === 'checkbox-tree' - ? { - label: filter.column, - subCategories: extractColumnData( - getFieldByTitle(filter.column) || '', - ), - } - : { - placeholder: 'All results', - label: filter.column, - multiple: filter.type === 'multiple-select', - items: dataValue.map(el => ({ - label: el[getFieldByTitle(filter.column) || ''], - value: el[getFieldByTitle(filter.column) || ''], - })), - }, + ? constructCheckboxTree(filter) + : constructSelect(filter), })); }; diff --git a/packages/core/src/components/Table/index.ts b/packages/core/src/components/Table/index.ts index c10399dc59..9cb8478688 100644 --- a/packages/core/src/components/Table/index.ts +++ b/packages/core/src/components/Table/index.ts @@ -14,6 +14,6 @@ * limitations under the License. */ -export { Table } from './Table'; -export type { TableColumn, TableProps } from './Table'; export { SubvalueCell } from './SubvalueCell'; +export { Table } from './Table'; +export type { TableColumn, TableFilter, TableProps } from './Table'; diff --git a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.test.tsx b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.test.tsx index e386f281e8..46cf014134 100644 --- a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.test.tsx +++ b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.test.tsx @@ -72,6 +72,6 @@ describe('ApiCatalogPage', () => { // https://github.com/mbrn/material-table/issues/1293 it('should render', async () => { const { findByText } = renderWrapped(); - expect(await findByText(/APIs \(2\)/)).toBeInTheDocument(); + expect(await findByText(/Backstage API Explorer/)).toBeInTheDocument(); }); }); diff --git a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx index b6440edc3a..c6a8d62b58 100644 --- a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx +++ b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx @@ -44,7 +44,6 @@ export const ApiExplorerPage = () => { All your APIs { wrapInTestApp( { const rendered = render( wrapInTestApp( - + , ), ); - expect(rendered.getByText(/APIs \(3\)/)).toBeInTheDocument(); expect(rendered.getByText(/api1/)).toBeInTheDocument(); expect(rendered.getByText(/api2/)).toBeInTheDocument(); expect(rendered.getByText(/api3/)).toBeInTheDocument(); diff --git a/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx index 485fcc216d..f3be9f664b 100644 --- a/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx +++ b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx @@ -15,7 +15,7 @@ */ import { ApiEntityV1alpha1, Entity } from '@backstage/catalog-model'; -import { Table, TableColumn, useApi } from '@backstage/core'; +import { Table, TableFilter, TableColumn, useApi } from '@backstage/core'; import { Chip, Link } from '@material-ui/core'; import { Alert } from '@material-ui/lab'; import React from 'react'; @@ -90,9 +90,27 @@ const columns: TableColumn[] = [ }, ]; +const filters: TableFilter[] = [ + { + column: 'Owner', + type: 'select', + }, + { + column: 'Type', + type: 'multiple-select', + }, + { + column: 'Lifecycle', + type: 'multiple-select', + }, + { + column: 'Tags', + type: 'checkbox-tree', + }, +]; + type ExplorerTableProps = { entities: Entity[]; - titlePreamble: string; loading: boolean; error?: any; }; @@ -101,7 +119,6 @@ export const ApiExplorerTable = ({ entities, loading, error, - titlePreamble, }: ExplorerTableProps) => { if (error) { return ( @@ -114,7 +131,7 @@ export const ApiExplorerTable = ({ } return ( - + ); }; From 9cc95aa90f3ff2957ef7a7c782ebca091cbd0f91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 13 Oct 2020 17:12:19 +0200 Subject: [PATCH 035/139] fix(templates): add some jsonify to fix escaping issues --- .../{{cookiecutter.component_id}}/component-info.yaml | 8 ++++---- .../{{cookiecutter.component_id}}/mkdocs.yml | 6 +++--- .../{{cookiecutter.component_id}}/component-info.yaml | 8 ++++---- .../{{cookiecutter.component_id}}/mkdocs.yml | 4 ++-- .../{{cookiecutter.component_id}}/component-info.yaml | 8 ++++---- .../{{cookiecutter.component_id}}/mkdocs.yml | 4 ++-- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/plugins/scaffolder-backend/sample-templates/docs-template/{{cookiecutter.component_id}}/component-info.yaml b/plugins/scaffolder-backend/sample-templates/docs-template/{{cookiecutter.component_id}}/component-info.yaml index 854b692fcf..e39350e928 100644 --- a/plugins/scaffolder-backend/sample-templates/docs-template/{{cookiecutter.component_id}}/component-info.yaml +++ b/plugins/scaffolder-backend/sample-templates/docs-template/{{cookiecutter.component_id}}/component-info.yaml @@ -1,12 +1,12 @@ apiVersion: backstage.io/v1alpha1 kind: Component metadata: - name: {{cookiecutter.component_id}} - description: {{cookiecutter.description}} + name: {{cookiecutter.component_id | jsonify}} + description: {{cookiecutter.description | jsonify}} annotations: - github.com/project-slug: {{cookiecutter.storePath}} + github.com/project-slug: {{cookiecutter.storePath | jsonify}} backstage.io/techdocs-ref: github:https://github.com/{{cookiecutter.storePath}} spec: type: documentation lifecycle: experimental - owner: {{cookiecutter.owner}} + owner: {{cookiecutter.owner | jsonify}} diff --git a/plugins/scaffolder-backend/sample-templates/docs-template/{{cookiecutter.component_id}}/mkdocs.yml b/plugins/scaffolder-backend/sample-templates/docs-template/{{cookiecutter.component_id}}/mkdocs.yml index 2126971502..64f1e9ee8e 100644 --- a/plugins/scaffolder-backend/sample-templates/docs-template/{{cookiecutter.component_id}}/mkdocs.yml +++ b/plugins/scaffolder-backend/sample-templates/docs-template/{{cookiecutter.component_id}}/mkdocs.yml @@ -1,7 +1,7 @@ -site_name: {{cookiecutter.component_id}} -site_description: {{cookiecutter.description}} +site_name: {{cookiecutter.component_id | jsonify}} +site_description: {{cookiecutter.description | jsonify}} -nav: +nav: - Introduction: index.md plugins: diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.component_id}}/component-info.yaml b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.component_id}}/component-info.yaml index f6e1d2c8d9..423c8683a1 100644 --- a/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.component_id}}/component-info.yaml +++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.component_id}}/component-info.yaml @@ -1,12 +1,12 @@ apiVersion: backstage.io/v1alpha1 kind: Component metadata: - name: {{cookiecutter.component_id}} - description: {{cookiecutter.description}} + name: {{cookiecutter.component_id | jsonify}} + description: {{cookiecutter.description | jsonify}} annotations: - github.com/project-slug: {{cookiecutter.storePath}} + github.com/project-slug: {{cookiecutter.storePath | jsonify}} backstage.io/techdocs-ref: github:https://github.com/{{cookiecutter.storePath}} spec: type: website lifecycle: experimental - owner: {{cookiecutter.owner}} + owner: {{cookiecutter.owner | jsonify}} diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.component_id}}/mkdocs.yml b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.component_id}}/mkdocs.yml index 0d10d11063..64f1e9ee8e 100644 --- a/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.component_id}}/mkdocs.yml +++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/{{cookiecutter.component_id}}/mkdocs.yml @@ -1,5 +1,5 @@ -site_name: {{cookiecutter.component_id}} -site_description: {{cookiecutter.description}} +site_name: {{cookiecutter.component_id | jsonify}} +site_description: {{cookiecutter.description | jsonify}} nav: - Introduction: index.md diff --git a/plugins/scaffolder-backend/sample-templates/springboot-grpc-template/{{cookiecutter.component_id}}/component-info.yaml b/plugins/scaffolder-backend/sample-templates/springboot-grpc-template/{{cookiecutter.component_id}}/component-info.yaml index 05b87c9c0c..f4ff350d10 100644 --- a/plugins/scaffolder-backend/sample-templates/springboot-grpc-template/{{cookiecutter.component_id}}/component-info.yaml +++ b/plugins/scaffolder-backend/sample-templates/springboot-grpc-template/{{cookiecutter.component_id}}/component-info.yaml @@ -1,12 +1,12 @@ apiVersion: backstage.io/v1alpha1 kind: Component metadata: - name: {{cookiecutter.component_id}} - description: {{cookiecutter.description}} + name: {{cookiecutter.component_id | jsonify}} + description: {{cookiecutter.description | jsonify}} annotations: - github.com/project-slug: {{cookiecutter.storePath}} + github.com/project-slug: {{cookiecutter.storePath | jsonify}} backstage.io/techdocs-ref: github:https://github.com/{{cookiecutter.storePath}} spec: type: service lifecycle: experimental - owner: {{cookiecutter.owner}} + owner: {{cookiecutter.owner | jsonify}} diff --git a/plugins/scaffolder-backend/sample-templates/springboot-grpc-template/{{cookiecutter.component_id}}/mkdocs.yml b/plugins/scaffolder-backend/sample-templates/springboot-grpc-template/{{cookiecutter.component_id}}/mkdocs.yml index 0d10d11063..64f1e9ee8e 100644 --- a/plugins/scaffolder-backend/sample-templates/springboot-grpc-template/{{cookiecutter.component_id}}/mkdocs.yml +++ b/plugins/scaffolder-backend/sample-templates/springboot-grpc-template/{{cookiecutter.component_id}}/mkdocs.yml @@ -1,5 +1,5 @@ -site_name: {{cookiecutter.component_id}} -site_description: {{cookiecutter.description}} +site_name: {{cookiecutter.component_id | jsonify}} +site_description: {{cookiecutter.description | jsonify}} nav: - Introduction: index.md From 1268eadd193f7e93ba41f9781d63bdcb29582baa Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Tue, 13 Oct 2020 16:01:50 -0400 Subject: [PATCH 036/139] add metric comparison to top panel --- app-config.yaml | 7 +- .../ExampleCostInsightsClient.ts | 44 ++-- .../cost-insights/src/api/CostInsightsApi.ts | 29 +-- .../CostInsightsPage/CostInsightsPage.tsx | 41 ++-- .../CostOverviewCard/CostOverviewCard.tsx | 94 ++++++--- .../CostOverviewCard/CostOverviewChart.tsx | 188 ++++++++++++++++++ .../CostOverviewTooltip.tsx | 21 +- .../CostOverviewChart.test.tsx | 50 ----- .../CostOverviewChart/CostOverviewChart.tsx | 120 ----------- .../src/components/CostOverviewChart/index.ts | 17 -- .../CostOverviewChartLegend.test.tsx | 40 ---- .../CostOverviewChartLegend.tsx | 48 ----- plugins/cost-insights/src/hooks/useConfig.tsx | 29 ++- .../cost-insights/src/hooks/useFilters.tsx | 5 +- .../src/types/ChangeStatistic.ts | 13 ++ .../index.ts => types/ChartData.tsx} | 7 +- plugins/cost-insights/src/types/Cost.ts | 4 + plugins/cost-insights/src/types/Filters.ts | 2 +- plugins/cost-insights/src/types/Metric.ts | 3 +- plugins/cost-insights/src/types/index.ts | 1 + plugins/cost-insights/src/utils/charts.ts | 39 ++++ plugins/cost-insights/src/utils/graphs.tsx | 7 +- 22 files changed, 417 insertions(+), 392 deletions(-) create mode 100644 plugins/cost-insights/src/components/CostOverviewCard/CostOverviewChart.tsx rename plugins/cost-insights/src/components/{CostOverviewChart => CostOverviewCard}/CostOverviewTooltip.tsx (72%) delete mode 100644 plugins/cost-insights/src/components/CostOverviewChart/CostOverviewChart.test.tsx delete mode 100644 plugins/cost-insights/src/components/CostOverviewChart/CostOverviewChart.tsx delete mode 100644 plugins/cost-insights/src/components/CostOverviewChart/index.ts delete mode 100644 plugins/cost-insights/src/components/CostOverviewChartLegend/CostOverviewChartLegend.test.tsx delete mode 100644 plugins/cost-insights/src/components/CostOverviewChartLegend/CostOverviewChartLegend.tsx rename plugins/cost-insights/src/{components/CostOverviewChartLegend/index.ts => types/ChartData.tsx} (84%) create mode 100644 plugins/cost-insights/src/utils/charts.ts diff --git a/app-config.yaml b/app-config.yaml index 228eafc588..aad474ea33 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -237,9 +237,12 @@ costInsights: icon: search metrics: dailyCost: - name: Your Company's Daily Cost + name: Pied Piper's Daily Cost + compare: DAU DAU: - name: Cost Per DAU + name: Daily Active Users + MSC: + name: Monthly Active Subscribers homepage: clocks: - label: UTC diff --git a/packages/app/src/plugins/cost-insights/ExampleCostInsightsClient.ts b/packages/app/src/plugins/cost-insights/ExampleCostInsightsClient.ts index aa795e9eee..fc2249fadc 100644 --- a/packages/app/src/plugins/cost-insights/ExampleCostInsightsClient.ts +++ b/packages/app/src/plugins/cost-insights/ExampleCostInsightsClient.ts @@ -28,6 +28,7 @@ import { Group, inclusiveStartDateOf, Maybe, + MetricData, ProductCost, Project, ProjectGrowthAlert, @@ -116,19 +117,33 @@ export class ExampleCostInsightsClient implements CostInsightsApi { return projects; } - async getGroupDailyCost( - group: string, + async getMetricData( metric: string | null, intervals: string, - ): Promise { + ): Promise { const aggregation = aggregationFor( durationOf(intervals), - metric ? 0.3 : 8_000, - ); - const groupDailyCost: Cost = await this.request( - { group, metric, intervals }, + 100_000, + ).map(entry => ({ ...entry, amount: Math.round(entry.amount) })); + + const cost: MetricData = await this.request( + { metric, intervals }, + { + format: 'number', + aggregation: aggregation, + change: changeOf(aggregation), + trendline: trendlineOf(aggregation), + }, + ); + + return cost; + } + + async getGroupDailyCost(group: string, intervals: string): Promise { + const aggregation = aggregationFor(durationOf(intervals), 8_000); + const groupDailyCost: Cost = await this.request( + { group, intervals }, { - id: metric, // costs with null ids will appear as "All Projects" in Cost Overview panel aggregation: aggregation, change: changeOf(aggregation), trendline: trendlineOf(aggregation), @@ -138,17 +153,10 @@ export class ExampleCostInsightsClient implements CostInsightsApi { return groupDailyCost; } - async getProjectDailyCost( - project: string, - metric: string | null, - intervals: string, - ): Promise { - const aggregation = aggregationFor( - durationOf(intervals), - metric ? 0.1 : 1_500, - ); + async getProjectDailyCost(project: string, intervals: string): Promise { + const aggregation = aggregationFor(durationOf(intervals), 1_500); const projectDailyCost: Cost = await this.request( - { project, metric, intervals }, + { project, intervals }, { id: 'project-a', aggregation: aggregation, diff --git a/plugins/cost-insights/src/api/CostInsightsApi.ts b/plugins/cost-insights/src/api/CostInsightsApi.ts index bdf673b7d1..52b3f335d9 100644 --- a/plugins/cost-insights/src/api/CostInsightsApi.ts +++ b/plugins/cost-insights/src/api/CostInsightsApi.ts @@ -23,6 +23,7 @@ import { Project, ProductCost, Maybe, + MetricData, } from '../types'; export type CostInsightsApi = { @@ -54,16 +55,10 @@ export type CostInsightsApi = { * reduction) and compare it to metrics important to the business. * * @param group The group id from getUserGroups or query parameters - * @param metric A metric from the cost-insights configuration in app-config.yaml. The backend - * should divide the actual daily cost by the corresponding metric for the same date. * @param intervals An ISO 8601 repeating interval string, such as R2/P1M/2020-09-01 * https://en.wikipedia.org/wiki/ISO_8601#Repeating_intervals */ - getGroupDailyCost( - group: string, - metric: string | null, - intervals: string, - ): Promise; + getGroupDailyCost(group: string, intervals: string): Promise; /** * Get daily cost aggregations for a given billing entity (project in GCP, AWS has a similar @@ -78,16 +73,21 @@ export type CostInsightsApi = { * (or reduction) and compare it to metrics important to the business. * * @param project The project id from getGroupProjects or query parameters - * @param metric A metric from the cost-insights configuration in app-config.yaml. The backend - * should divide the actual daily cost by the corresponding metric for the same date. * @param intervals An ISO 8601 repeating interval string, such as R2/P1M/2020-09-01 * https://en.wikipedia.org/wiki/ISO_8601#Repeating_intervals */ - getProjectDailyCost( - project: string, - metric: string | null, - intervals: string, - ): Promise; + getProjectDailyCost(project: string, intervals: string): Promise; + + /** + * Get aggregations for a particular metric and interval timeframe. Teams + * can see metrics important to their business in comparison to the growth + * (or reduction) of a project or group's daily costs. + * + * @param metric A metric from the cost-insights configuration in app-config.yaml. + * @param intervals An ISO 8601 repeating interval string, such as R2/P1M/2020-09-01 + * https://en.wikipedia.org/wiki/ISO_8601#Repeating_intervals + */ + getMetricData(metric: string | null, intervals: string): Promise; /** * Get cost aggregations for a particular cloud product and interval timeframe. This includes @@ -107,6 +107,7 @@ export type CostInsightsApi = { * of how the durations are interpreted in Cost Insights. * @param project (optional) The project id from getGroupProjects or query parameters */ + getProductInsights( product: string, group: string, diff --git a/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPage.tsx b/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPage.tsx index 8922ac31e0..9bc396537b 100644 --- a/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPage.tsx +++ b/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPage.tsx @@ -39,7 +39,14 @@ import { useCurrency, useConfig, } from '../../hooks'; -import { Alert, Cost, intervalsOf, Maybe, Project } from '../../types'; +import { + Alert, + Cost, + intervalsOf, + Maybe, + MetricData, + Project, +} from '../../types'; import { mapLoadingToProps } from './selector'; import ProjectSelect from '../ProjectSelect'; @@ -48,15 +55,17 @@ const CostInsightsPage = () => { // There is not currently a UI to set feature flags // flags.set('cost-insights-currencies', FeatureFlagState.On); const client = useApi(costInsightsApiRef); - const { currencies } = useConfig(); + const config = useConfig(); const groups = useGroups(); const [currency, setCurrency] = useCurrency(); const [projects, setProjects] = useState>(null); const [dailyCost, setDailyCost] = useState>(null); + const [metricData, setMetricData] = useState>(null); const [alerts, setAlerts] = useState>(null); const [error, setError] = useState>(null); const { pageFilters, setPageFilters } = useFilters(p => p); + const { loadingActions, loadingGroups, @@ -94,26 +103,32 @@ const CostInsightsPage = () => { dispatchLoadingInsights(true); const [ fetchedProjects, - fetchedCosts, fetchedAlerts, + fetchedMetricData, + fetchedCosts, ] = await Promise.all([ client.getGroupProjects(pageFilters.group), + client.getAlerts(pageFilters.group), + pageFilters.metric + ? client.getMetricData( + pageFilters.metric, + intervalsOf(pageFilters.duration), + ) + : null, pageFilters.project ? client.getProjectDailyCost( pageFilters.project, - pageFilters.metric, intervalsOf(pageFilters.duration), ) : client.getGroupDailyCost( pageFilters.group, - pageFilters.metric, intervalsOf(pageFilters.duration), ), - client.getAlerts(pageFilters.group), ]); setProjects(fetchedProjects); - setDailyCost(fetchedCosts); setAlerts(fetchedAlerts); + setMetricData(fetchedMetricData); + setDailyCost(fetchedCosts); } else { dispatchLoadingNone(loadingActions); } @@ -133,11 +148,11 @@ const CostInsightsPage = () => { }, [ client, pageFilters, + loadingActions, loadingGroups, dispatchLoadingInsights, dispatchLoadingInitial, dispatchLoadingNone, - loadingActions, ]); if (loadingInitial) { @@ -166,7 +181,6 @@ const CostInsightsPage = () => { ); } - // These should be defined, alerts can be an empty array but that's truthy if (!dailyCost || !alerts) { return ( @@ -195,7 +209,7 @@ const CostInsightsPage = () => { @@ -253,12 +267,7 @@ const CostInsightsPage = () => { {!!dailyCost.aggregation.length && ( - + )} diff --git a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx index b8d1be5d01..7f270e2374 100644 --- a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx +++ b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx @@ -15,42 +15,41 @@ */ import React from 'react'; -import { Box, Card, CardContent, Divider } from '@material-ui/core'; -import CostOverviewChart from '../CostOverviewChart'; -import CostOverviewChartLegend from '../CostOverviewChartLegend'; +import { Box, Card, CardContent, Divider, useTheme } from '@material-ui/core'; +import CostGrowth from '../CostGrowth'; +import CostOverviewChart from './CostOverviewChart'; import CostOverviewHeader from './CostOverviewHeader'; +import LegendItem from '../LegendItem'; import MetricSelect from '../MetricSelect'; import PeriodSelect from '../PeriodSelect'; import { useScroll, useFilters, useConfig } from '../../hooks'; import { mapFiltersToProps } from './selector'; import { DefaultNavigation } from '../../utils/navigation'; +import { formatPercent } from '../../utils/formatters'; import { - ChangeStatistic, - DateAggregation, - Project, - Trendline, + Cost, + CostInsightsTheme, + MetricData, findAlways, + getComparedChange, } from '../../types'; -type CostOverviewCardProps = { - change: ChangeStatistic; - aggregation: Array; - trendline: Trendline; - projects: Array; +export type CostOverviewCardProps = { + data: [Cost, MetricData | null]; }; -const CostOverviewCard = ({ - change, - aggregation, - trendline, -}: CostOverviewCardProps) => { - const { metrics } = useConfig(); +const CostOverviewCard = ({ data }: CostOverviewCardProps) => { + const theme = useTheme(); + const config = useConfig(); const { ScrollAnchor } = useScroll(DefaultNavigation.CostOverviewCard); - const { setDuration, setProject, metric, setMetric, ...filters } = useFilters( + const { setDuration, setProject, setMetric, ...filters } = useFilters( mapFiltersToProps, ); - const { name } = findAlways(metrics, m => m.kind === metric); + // There should always be a daily cost metric but a comparison metric is optional. + const dailyCost = findAlways(config.metrics, m => m.kind === null); + const metric = config.metrics.find(m => m.kind === filters.metric); + const comparedChange = data[1] ? getComparedChange(data[0], data[1]) : null; return ( @@ -60,22 +59,53 @@ const CostOverviewCard = ({ - - + + + + + {formatPercent(data[0].change.ratio)} + + + {metric && metric.kind && data[1] && comparedChange && ( + <> + + + {formatPercent(data[1].change.ratio)} + + + + + + + )} + - + {config.metrics.length > 1 && ( + + )} diff --git a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewChart.tsx b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewChart.tsx new file mode 100644 index 0000000000..40cefbb589 --- /dev/null +++ b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewChart.tsx @@ -0,0 +1,188 @@ +/* + * 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 { useTheme } from '@material-ui/core'; +import { + AxisDomain, + ComposedChart, + XAxis, + YAxis, + Tooltip, + CartesianGrid, + Area, + Line, + ResponsiveContainer, + TooltipPayload, +} from 'recharts'; +import { + ChartData, + Cost, + Metric, + MetricData, + CostInsightsTheme, +} from '../../types'; +import { + overviewGraphTickFormatter, + formatGraphValue, +} from '../../utils/graphs'; +import CostOverviewTooltip from './CostOverviewTooltip'; +import { TooltipItemProps } from '../Tooltip'; +import { NULL_METRIC } from '../../hooks/useConfig'; +import { useCostOverviewStyles as useStyles } from '../../utils/styles'; +import { groupByDate, toDataMax, trendFrom } from '../../utils/charts'; +import { aggregationSort } from '../../utils/sort'; + +type CostOverviewChartProps = { + data: [Cost, MetricData | null]; + name: string; + compare?: Metric; + responsive?: boolean; +}; + +const CostOverviewChart = ({ + data, + name, + compare, + responsive = true, +}: CostOverviewChartProps) => { + const theme = useTheme(); + const styles = useStyles(theme); + + const { dailyCost, metric } = { + dailyCost: { + id: NULL_METRIC, + name: name, + format: 'number', + data: data[0], + }, + metric: { + id: compare?.kind ?? 'Unknown', + name: compare?.name ?? 'Unknown', + format: data[1]?.format ?? 'number', + data: data[1], + }, + }; + + const metricsByDate = metric.data + ? metric.data.aggregation.reduce(groupByDate, {}) + : {}; + + const chartData: ChartData[] = dailyCost + .data!.aggregation.slice() + .sort(aggregationSort) + .map(entry => ({ + date: Date.parse(entry.date), + trend: trendFrom(dailyCost.data!.trendline, Date.parse(entry.date)), + dailyCost: entry.amount, + ...(metric && metric.data + ? { [metric.id]: metricsByDate[`${entry.date}`] } + : {}), + })); + + const metricDataMax: AxisDomain = metric + ? toDataMax(metric.id, chartData) + : 'dataMax'; + + function tooltipFormatter(payload: TooltipPayload): TooltipItemProps { + return { + label: payload.dataKey === dailyCost.id ? dailyCost.name : metric.name, + value: + payload.dataKey === dailyCost.id + ? formatGraphValue(payload.value as number, dailyCost.format) + : formatGraphValue(payload.value as number, metric.format), + fill: + payload.dataKey === dailyCost.id + ? theme.palette.blue + : theme.palette.magenta, + }; + } + + return ( + + + + + 0, 'dataMax']} + tick={{ fill: styles.axis.fill }} + tickFormatter={formatGraphValue} + width={styles.yAxis.width} + yAxisId={dailyCost.id} + /> + {metric && ( + 0, metricDataMax]} + width={styles.yAxis.width} + yAxisId={metric.id} + /> + )} + + + {metric && ( + + )} + + } + animationDuration={100} + /> + + + ); +}; + +export default CostOverviewChart; diff --git a/plugins/cost-insights/src/components/CostOverviewChart/CostOverviewTooltip.tsx b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewTooltip.tsx similarity index 72% rename from plugins/cost-insights/src/components/CostOverviewChart/CostOverviewTooltip.tsx rename to plugins/cost-insights/src/components/CostOverviewCard/CostOverviewTooltip.tsx index 17d466441d..a30f370166 100644 --- a/plugins/cost-insights/src/components/CostOverviewChart/CostOverviewTooltip.tsx +++ b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewTooltip.tsx @@ -16,29 +16,24 @@ import React from 'react'; import moment from 'moment'; import { TooltipPayload, TooltipProps } from 'recharts'; -import Tooltip from '../../components/Tooltip'; +import Tooltip, { TooltipItemProps } from '../../components/Tooltip'; import { DEFAULT_DATE_FORMAT } from '../../types'; -import { formatGraphValue } from '../../utils/graphs'; -type CostOverviewTooltipProps = TooltipProps & { - metric: string; - name: string; +export type CostOverviewTooltipProps = TooltipProps & { + dataKeys: Array; + format: (payload: TooltipPayload) => TooltipItemProps; }; const CostOverviewTooltip = ({ label, payload, - metric, - name, + dataKeys, + format, }: CostOverviewTooltipProps) => { const tooltipLabel = moment(label).format(DEFAULT_DATE_FORMAT); const items = payload - ?.filter(data => data.name === metric) - .map((data: TooltipPayload) => ({ - label: name, - value: formatGraphValue(data.value as number), - fill: data.fill as string, - })); + ?.filter((p: TooltipPayload) => dataKeys.includes(p.dataKey as string)) + .map(p => format(p)); return ; }; diff --git a/plugins/cost-insights/src/components/CostOverviewChart/CostOverviewChart.test.tsx b/plugins/cost-insights/src/components/CostOverviewChart/CostOverviewChart.test.tsx deleted file mode 100644 index e6311d147a..0000000000 --- a/plugins/cost-insights/src/components/CostOverviewChart/CostOverviewChart.test.tsx +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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 CostOverviewChart from './CostOverviewChart'; -import { renderInTestApp } from '@backstage/test-utils'; -import React from 'react'; -import { DateAggregation, Trendline } from '../../types'; -import { CostInsightsThemeProvider } from '../CostInsightsPage/CostInsightsThemeProvider'; - -const mockAggregation = [ - { date: '2020-04-01', amount: 100 }, - { date: '2020-04-02', amount: 101 }, - { date: '2020-04-03', amount: 102 }, - { date: '2020-04-04', amount: 103 }, -] as Array; - -const mockTrendline = { slope: 0.3, intercept: 101.5 } as Trendline; -const mockMetric = 'mock-metric'; - -describe('', () => { - it('Renders without exploding', async () => { - const rendered = await renderInTestApp( - - - , - ); - expect( - rendered.container.querySelector('.cost-overview-chart'), - ).toBeInTheDocument(); - }); -}); diff --git a/plugins/cost-insights/src/components/CostOverviewChart/CostOverviewChart.tsx b/plugins/cost-insights/src/components/CostOverviewChart/CostOverviewChart.tsx deleted file mode 100644 index 2b40668409..0000000000 --- a/plugins/cost-insights/src/components/CostOverviewChart/CostOverviewChart.tsx +++ /dev/null @@ -1,120 +0,0 @@ -/* - * 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 { - ComposedChart, - XAxis, - YAxis, - Tooltip, - CartesianGrid, - Area, - Line, - ResponsiveContainer, -} from 'recharts'; -import { - Maybe, - DateAggregation, - Trendline, - CostInsightsTheme, -} from '../../types'; -import { - overviewGraphTickFormatter, - formatGraphValue, -} from '../../utils/graphs'; -import CostOverviewTooltip from './CostOverviewTooltip'; -import { useTheme } from '@material-ui/core'; -import { useCostOverviewStyles as useStyles } from '../../utils/styles'; -import { NULL_METRIC } from '../../hooks/useConfig'; - -type CostOverviewChartProps = { - responsive: boolean; - aggregation: Array; - trendline?: Maybe; - metric: string | null; - tooltip: string; -}; - -const CostOverviewChart = ({ - responsive = true, - aggregation, - trendline, - metric, - tooltip, -}: CostOverviewChartProps) => { - const theme = useTheme(); - const styles = useStyles(theme); - - const id = metric ? metric : NULL_METRIC; - - const dailyCostData = aggregation.map((entry: DateAggregation) => ({ - date: Date.parse(entry.date), - [id]: entry.amount, - trend: trendline - ? trendline.slope * (Date.parse(entry.date) / 1000) + trendline.intercept - : null, - })); - - return ( - - - - - 0, 'dataMax']} - tick={{ fill: styles.axis.fill }} - tickFormatter={formatGraphValue} - width={styles.yAxis.width} - yAxisId={id} - /> - - - } - animationDuration={100} - /> - - - ); -}; - -export default CostOverviewChart; diff --git a/plugins/cost-insights/src/components/CostOverviewChart/index.ts b/plugins/cost-insights/src/components/CostOverviewChart/index.ts deleted file mode 100644 index 21f345a282..0000000000 --- a/plugins/cost-insights/src/components/CostOverviewChart/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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 './CostOverviewChart'; diff --git a/plugins/cost-insights/src/components/CostOverviewChartLegend/CostOverviewChartLegend.test.tsx b/plugins/cost-insights/src/components/CostOverviewChartLegend/CostOverviewChartLegend.test.tsx deleted file mode 100644 index a40ff58ec1..0000000000 --- a/plugins/cost-insights/src/components/CostOverviewChartLegend/CostOverviewChartLegend.test.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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 { renderInTestApp } from '@backstage/test-utils'; -import CostOverviewChartLegend from './CostOverviewChartLegend'; -import React from 'react'; -import { ChangeStatistic } from '../../types'; - -describe('', () => { - it('Correctly displays text if change is not supplied', async () => { - const rendered = await renderInTestApp( - , - ); - expect(rendered.queryByText('Unclear')).toBeInTheDocument(); - }); - it('Correctly displays formatted change percentage', async () => { - const change = { - ratio: 0.3456, - amount: 40000, - } as ChangeStatistic; - const rendered = await renderInTestApp( - , - ); - expect(rendered.queryByText('Unclear')).not.toBeInTheDocument(); - expect(rendered.queryByText('35%')).toBeInTheDocument(); - }); -}); diff --git a/plugins/cost-insights/src/components/CostOverviewChartLegend/CostOverviewChartLegend.tsx b/plugins/cost-insights/src/components/CostOverviewChartLegend/CostOverviewChartLegend.tsx deleted file mode 100644 index 538cd2a63a..0000000000 --- a/plugins/cost-insights/src/components/CostOverviewChartLegend/CostOverviewChartLegend.tsx +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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 { Box, useTheme } from '@material-ui/core'; -import LegendItem from '../LegendItem'; -import { formatPercent } from '../../utils/formatters'; -import { ChangeStatistic, CostInsightsTheme } from '../../types'; - -type CostOverviewChartLegendProps = { - change?: ChangeStatistic; - title: string; - tooltip?: string; -}; - -const CostOverviewChartLegend = ({ - change, - title, - tooltip, -}: CostOverviewChartLegendProps) => { - const theme = useTheme(); - - return ( - - - {change ? formatPercent(change.ratio) : 'Unclear'} - - - ); -}; - -export default CostOverviewChartLegend; diff --git a/plugins/cost-insights/src/hooks/useConfig.tsx b/plugins/cost-insights/src/hooks/useConfig.tsx index aee6567056..a73a7bf301 100644 --- a/plugins/cost-insights/src/hooks/useConfig.tsx +++ b/plugins/cost-insights/src/hooks/useConfig.tsx @@ -42,6 +42,9 @@ export const NULL_METRIC_NAME = 'Daily Cost'; * name: Product B * icon: data * metrics: + * dailyCost: + * name: Daily Cost + * compare: metricA * metricA: * name: Metric A * metricB: @@ -89,6 +92,7 @@ export const ConfigProvider = ({ children }: { children: ReactNode }) => { return metrics.keys().map(key => ({ kind: key === NULL_METRIC ? null : key, name: metrics.getString(`${key}.name`), + compare: metrics.getOptionalString(`${key}.compare`), })); } @@ -115,23 +119,14 @@ export const ConfigProvider = ({ children }: { children: ReactNode }) => { const engineerCost = getEngineerCost(); const icons = getIcons(); - if (metrics.find((m: Metric) => m.kind === null)) { - setConfig(prevState => ({ - ...prevState, - metrics, - products, - engineerCost, - icons, - })); - } else { - setConfig(prevState => ({ - ...prevState, - metrics: [...prevState.metrics, ...metrics], - products, - engineerCost, - icons, - })); - } + setConfig(prevState => ({ + ...prevState, + metrics, + products, + engineerCost, + icons, + })); + setLoading(false); } diff --git a/plugins/cost-insights/src/hooks/useFilters.tsx b/plugins/cost-insights/src/hooks/useFilters.tsx index c2053d6b0f..0ad4c24267 100644 --- a/plugins/cost-insights/src/hooks/useFilters.tsx +++ b/plugins/cost-insights/src/hooks/useFilters.tsx @@ -69,6 +69,7 @@ export const FilterContext = React.createContext< >(undefined); export const FilterProvider = ({ children }: FilterProviderProps) => { + const config = useConfig(); const navigate = useNavigate(); const location = useLocation(); const queryParams = useQueryParams(); @@ -101,7 +102,9 @@ export const FilterProvider = ({ children }: FilterProviderProps) => { // TODO: Figure out why pageFilters doesn't get updated by the above when groups are loaded. useEffect(() => { - setPageFilters(getInitialPageState(groups, queryParams.pageFilters)); + const initialState = getInitialPageState(groups, queryParams.pageFilters); + const compared = config.metrics.find(m => m.kind === null)?.compare; + setPageFilters({ ...initialState, metric: compared || null }); }, [groups]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { diff --git a/plugins/cost-insights/src/types/ChangeStatistic.ts b/plugins/cost-insights/src/types/ChangeStatistic.ts index 538692bb1d..cf7310ef97 100644 --- a/plugins/cost-insights/src/types/ChangeStatistic.ts +++ b/plugins/cost-insights/src/types/ChangeStatistic.ts @@ -14,6 +14,9 @@ * limitations under the License. */ +import { Cost } from './Cost'; +import { aggregationSort } from '../utils/sort'; + export interface ChangeStatistic { // The ratio of change from one duration to another, expressed as: (newSum - oldSum) / oldSum ratio: number; @@ -46,3 +49,13 @@ export function growthOf(amount: number, ratio: number) { return Growth.Negligible; } + +// Used by for displaying engineer totals +export function getComparedChange(a: Cost, b: Cost): ChangeStatistic { + const ratio = a.change.ratio - b.change.ratio; + const amount = a.aggregation.slice().sort(aggregationSort)[0].amount; + return { + ratio: ratio, + amount: amount * ratio, + }; +} diff --git a/plugins/cost-insights/src/components/CostOverviewChartLegend/index.ts b/plugins/cost-insights/src/types/ChartData.tsx similarity index 84% rename from plugins/cost-insights/src/components/CostOverviewChartLegend/index.ts rename to plugins/cost-insights/src/types/ChartData.tsx index 1b6c1fe975..d99e9d9bc3 100644 --- a/plugins/cost-insights/src/components/CostOverviewChartLegend/index.ts +++ b/plugins/cost-insights/src/types/ChartData.tsx @@ -14,4 +14,9 @@ * limitations under the License. */ -export { default } from './CostOverviewChartLegend'; +export type ChartData = { + date: number; + trend: number; + dailyCost: number; + [key: string]: number; +}; diff --git a/plugins/cost-insights/src/types/Cost.ts b/plugins/cost-insights/src/types/Cost.ts index 84a8bfae51..8a9ce3c69b 100644 --- a/plugins/cost-insights/src/types/Cost.ts +++ b/plugins/cost-insights/src/types/Cost.ts @@ -24,3 +24,7 @@ export interface Cost { change: ChangeStatistic; trendline: Trendline; } + +export interface MetricData extends Cost { + format: 'number' | 'currency'; +} diff --git a/plugins/cost-insights/src/types/Filters.ts b/plugins/cost-insights/src/types/Filters.ts index def1d74d1a..f90b9b4c78 100644 --- a/plugins/cost-insights/src/types/Filters.ts +++ b/plugins/cost-insights/src/types/Filters.ts @@ -22,7 +22,7 @@ export interface PageFilters { group: Maybe; project: Maybe; duration: Duration; - metric: Maybe; + metric: string | null; } export type ProductFilters = Array; diff --git a/plugins/cost-insights/src/types/Metric.ts b/plugins/cost-insights/src/types/Metric.ts index 5a440a8d1f..4fa9cd189b 100644 --- a/plugins/cost-insights/src/types/Metric.ts +++ b/plugins/cost-insights/src/types/Metric.ts @@ -14,9 +14,10 @@ * limitations under the License. */ -import { Maybe } from './Maybe'; +import { Maybe } from '../types'; export type Metric = { kind: Maybe; name: string; + compare?: string; }; diff --git a/plugins/cost-insights/src/types/index.ts b/plugins/cost-insights/src/types/index.ts index c3d9a87886..8523c7db7b 100644 --- a/plugins/cost-insights/src/types/index.ts +++ b/plugins/cost-insights/src/types/index.ts @@ -16,6 +16,7 @@ export * from './Alert'; export * from './ChangeStatistic'; +export * from './ChartData'; export * from './Cost'; export * from './DateAggregation'; export * from './Duration'; diff --git a/plugins/cost-insights/src/utils/charts.ts b/plugins/cost-insights/src/utils/charts.ts new file mode 100644 index 0000000000..60a8e85816 --- /dev/null +++ b/plugins/cost-insights/src/utils/charts.ts @@ -0,0 +1,39 @@ +/* + * 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 { DateAggregation, Trendline, ChartData } from '../types'; + +export function trendFrom(trendline: Trendline, date: number): number { + return trendline.slope * (date / 1000) + trendline.intercept; +} + +export function groupByDate( + acc: Record, + entry: DateAggregation, +): Record { + return { ...acc, [entry.date]: entry.amount }; +} + +export function toMaxCost(acc: ChartData, entry: ChartData): ChartData { + return acc.dailyCost > entry.dailyCost ? acc : entry; +} + +export function toDataMax(metric: string, data: ChartData[]): number { + return ( + (data.reduce(toMaxCost).dailyCost / Math.abs(data[0].trend)) * + data[0][metric] + ); +} diff --git a/plugins/cost-insights/src/utils/graphs.tsx b/plugins/cost-insights/src/utils/graphs.tsx index 5200b14b1f..167132cb36 100644 --- a/plugins/cost-insights/src/utils/graphs.tsx +++ b/plugins/cost-insights/src/utils/graphs.tsx @@ -20,10 +20,15 @@ import { lengthyCurrencyFormatter, } from './formatters'; -export function formatGraphValue(value: number) { +export function formatGraphValue(value: number, format?: string) { + if (format === 'number') { + return value.toLocaleString(); + } + if (value < 1) { return lengthyCurrencyFormatter.format(value); } + return currencyFormatter.format(value); } From 94b248e6826bfb9301b2312601e8743e578c94b2 Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Tue, 13 Oct 2020 16:25:24 -0400 Subject: [PATCH 037/139] set default null metric --- plugins/cost-insights/src/hooks/useConfig.tsx | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/plugins/cost-insights/src/hooks/useConfig.tsx b/plugins/cost-insights/src/hooks/useConfig.tsx index a73a7bf301..830cafc4f1 100644 --- a/plugins/cost-insights/src/hooks/useConfig.tsx +++ b/plugins/cost-insights/src/hooks/useConfig.tsx @@ -119,13 +119,23 @@ export const ConfigProvider = ({ children }: { children: ReactNode }) => { const engineerCost = getEngineerCost(); const icons = getIcons(); - setConfig(prevState => ({ - ...prevState, - metrics, - products, - engineerCost, - icons, - })); + if (metrics.find((m: Metric) => m.kind === null)) { + setConfig(prevState => ({ + ...prevState, + metrics, + products, + engineerCost, + icons, + })); + } else { + setConfig(prevState => ({ + ...prevState, + metrics: [...prevState.metrics, ...metrics], + products, + engineerCost, + icons, + })); + } setLoading(false); } From 54504deb135ce71395ad8a5b5505575dac7e220f Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Tue, 13 Oct 2020 16:43:45 -0400 Subject: [PATCH 038/139] tweak custom metric verbiage --- app-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app-config.yaml b/app-config.yaml index aad474ea33..d1414582ed 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -242,7 +242,7 @@ costInsights: DAU: name: Daily Active Users MSC: - name: Monthly Active Subscribers + name: Monthly Subscribers homepage: clocks: - label: UTC From 01876a713a2da98b941d6665a951a027a5bc4582 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Wed, 14 Oct 2020 01:26:19 -0400 Subject: [PATCH 039/139] Fix typo (#2888) --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 85834076d4..7edbc6d17b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -87,7 +87,7 @@ We use [changesets](https://github.com/atlassian/changesets) to help us prepare 1. Run `yarn changeset` 2. Select which packages you want to include a changeset for 3. Select impact of change that you're introducing (minor, major or patch) -4. Add generated changset to Git +4. Add generated changeset to Git 5. Push the commit with your changeset to the branch associated with your PR 6. Accept our gratitude for making the release process easier on the maintainer From c926765a2ceff1beaf7f16ffa186ac1e7023ea37 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Wed, 14 Oct 2020 02:24:41 -0400 Subject: [PATCH 040/139] Allow templates to be located on non-default branch (#2839) * Checkout branch if not on default * Use checkout Ref * Get first directory instead of relying on component_id * Checkout branch when clonning * Update test * Default to empty string * Added changeset * Added test for Cookie Cutter installed & nothing generated * Added comment --- .changeset/happy-ads-behave.md | 5 ++ .../scaffolder/stages/prepare/github.test.ts | 9 ++- .../src/scaffolder/stages/prepare/github.ts | 25 +++--- .../stages/templater/cookiecutter.test.ts | 81 ++++++++++++++++++- .../stages/templater/cookiecutter.ts | 10 ++- 5 files changed, 115 insertions(+), 15 deletions(-) create mode 100644 .changeset/happy-ads-behave.md diff --git a/.changeset/happy-ads-behave.md b/.changeset/happy-ads-behave.md new file mode 100644 index 0000000000..f8b889f85c --- /dev/null +++ b/.changeset/happy-ads-behave.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Allow templates to be located on non-default branch diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts index babdd65c04..de57043a50 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts @@ -76,7 +76,9 @@ describe('GitHubPreparer', () => { 1, 'https://github.com/benjdlambert/backstage-graphql-template', expect.any(String), - {}, + { + checkoutBranch: 'master', + }, ); }); it('calls the clone command with the correct arguments for a repository when no path is provided', async () => { @@ -87,7 +89,9 @@ describe('GitHubPreparer', () => { 1, 'https://github.com/benjdlambert/backstage-graphql-template', expect.any(String), - {}, + { + checkoutBranch: 'master', + }, ); }); it('return the temp directory with the path to the folder if it is specified', async () => { @@ -107,6 +111,7 @@ describe('GitHubPreparer', () => { 'https://github.com/benjdlambert/backstage-graphql-template', expect.any(String), { + checkoutBranch: 'master', fetchOpts: { callbacks: { credentials: expect.any(Function), diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts index 49703c5616..6eee00f9cb 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts @@ -21,7 +21,7 @@ import { parseLocationAnnotation } from '../helpers'; import { InputError } from '@backstage/backend-common'; import { PreparerBase } from './types'; import GitUriParser from 'git-url-parse'; -import { Clone, Cred } from 'nodegit'; +import { Clone, CloneOptions, Cred } from 'nodegit'; export class GithubPreparer implements PreparerBase { token?: string; @@ -52,17 +52,22 @@ export class GithubPreparer implements PreparerBase { template.spec.path ?? '.', ); - const cloneOptions = token - ? { - fetchOpts: { - callbacks: { - credentials() { - return Cred.userpassPlaintextNew(token, 'x-oauth-basic'); - }, + let cloneOptions: CloneOptions = { + checkoutBranch: parsedGitLocation.ref, + }; + + if (token) { + cloneOptions = { + ...cloneOptions, + fetchOpts: { + callbacks: { + credentials() { + return Cred.userpassPlaintextNew(token, 'x-oauth-basic'); }, }, - } - : {}; + }, + }; + } await Clone.clone(repositoryCheckoutUrl, tempDir, cloneOptions); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.test.ts index 31bcdf189b..106a448d7c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.test.ts @@ -13,16 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -jest.mock('./helpers', () => ({ runDockerContainer: jest.fn() })); +jest.mock('./helpers', () => ({ + runDockerContainer: jest.fn(), + runCommand: jest.fn(), +})); +jest.mock('command-exists-promise', () => jest.fn()); import { CookieCutter } from './cookiecutter'; import fs from 'fs-extra'; import os from 'os'; import path from 'path'; -import { RunDockerContainerOptions } from './helpers'; +import { RunDockerContainerOptions, RunCommandOptions } from './helpers'; import { PassThrough } from 'stream'; import Docker from 'dockerode'; +const commandExists = require('command-exists-promise'); + describe('CookieCutter Templater', () => { const cookie = new CookieCutter(); const mockDocker = {} as Docker; @@ -32,6 +38,10 @@ describe('CookieCutter Templater', () => { runDockerContainer: jest.Mock; } = require('./helpers'); + jest + .spyOn(fs, 'readdir') + .mockImplementation(() => Promise.resolve(['newthing'])); + beforeEach(async () => { jest.clearAllMocks(); }); @@ -174,4 +184,71 @@ describe('CookieCutter Templater', () => { dockerClient: mockDocker, }); }); + + describe('when cookiecutter is available', () => { + beforeAll(() => { + commandExists.mockImplementation(() => () => true); + }); + + it('use the binary', async () => { + const { + runCommand, + }: { + runCommand: jest.Mock; + } = require('./helpers'); + + const stream = new PassThrough(); + + const tempdir = await mkTemp(); + + const values = { + owner: 'blobby', + storePath: 'spotify/end-repo', + component_id: 'newthing', + }; + + await cookie.run({ + directory: tempdir, + values, + logStream: stream, + dockerClient: mockDocker, + }); + + expect(runCommand).toHaveBeenCalledWith({ + command: 'cookiecutter', + args: expect.arrayContaining([ + '--no-input', + '-o', + tempdir, + expect.stringContaining(`${tempdir}-result`), + '--verbose', + ]), + logStream: stream, + }); + }); + }); + + describe('when nothing was generated', () => { + beforeEach(() => { + jest.spyOn(fs, 'readdir').mockImplementation(() => Promise.resolve([])); + }); + + it('throws an error', async () => { + const stream = new PassThrough(); + + const tempdir = await mkTemp(); + + return expect( + cookie.run({ + directory: tempdir, + values: { + owner: 'blobby', + storePath: 'spotify/end-repo', + }, + logStream: stream, + dockerClient: mockDocker, + }), + ).rejects.toThrow(/Cookie Cutter did not generate anything/); + }); + }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts index 21b6fc3dde..7be8730bd9 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts @@ -78,8 +78,16 @@ export class CookieCutter implements TemplaterBase { }); } + // if cookiecutter was successful, resultDir will contain + // exactly one directory. + const [generated] = await fs.readdir(resultDir); + + if (generated === undefined) { + throw new Error('Cookie Cutter did not generate anything'); + } + return { - resultDir: path.resolve(resultDir, options.values.component_id as string), + resultDir: path.resolve(resultDir, generated), }; } } From 99710b102de57b688bb94e46b4d3e39f333e5a6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 12 Oct 2020 15:38:12 +0200 Subject: [PATCH 041/139] feat(catalog-backend): make a builder to help wire up the catalog --- .changeset/2874.md | 12 + .../backend-common/src/database/postgres.ts | 2 +- packages/backend/src/plugins/catalog.ts | 32 +- .../catalog-model/src/EntityPolicies.test.ts | 110 ++++ packages/catalog-model/src/EntityPolicies.ts | 60 +- .../packages/backend/src/plugins/catalog.ts | 34 +- .../src/database/DatabaseManager.ts | 24 +- plugins/catalog-backend/src/index.ts | 2 +- .../src/ingestion/LocationReaders.ts | 141 ++--- .../processors/PlaceholderProcessor.test.ts | 39 +- .../processors/PlaceholderProcessor.ts | 11 - .../src/ingestion/processors/YamlProcessor.ts | 5 + .../src/ingestion/processors/index.ts | 1 + .../src/service/CatalogBuilder.test.ts | 139 +++++ .../src/service/CatalogBuilder.ts | 523 ++++++++++++++++++ plugins/catalog-backend/src/service/index.ts | 19 + .../src/service/standaloneServer.ts | 28 +- 17 files changed, 925 insertions(+), 257 deletions(-) create mode 100644 .changeset/2874.md create mode 100644 packages/catalog-model/src/EntityPolicies.test.ts create mode 100644 plugins/catalog-backend/src/service/CatalogBuilder.test.ts create mode 100644 plugins/catalog-backend/src/service/CatalogBuilder.ts create mode 100644 plugins/catalog-backend/src/service/index.ts diff --git a/.changeset/2874.md b/.changeset/2874.md new file mode 100644 index 0000000000..693962314f --- /dev/null +++ b/.changeset/2874.md @@ -0,0 +1,12 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +The way that wiring together a catalog happens, has changed drastically. Now +there is a new class `CatalogBuilder` that does almost all of the heavy lifting +of how to augment/replace pieces of catalog functionality, such as adding +support for custom entities or adding additional processors. + +As the builder was added, a lot of the static methods and builders for default +setups have been removed from classes deep in the hierarchy. Instead, the +builder contains the knowledge of what the defaults are. diff --git a/packages/backend-common/src/database/postgres.ts b/packages/backend-common/src/database/postgres.ts index 82e971f081..04bd3615b6 100644 --- a/packages/backend-common/src/database/postgres.ts +++ b/packages/backend-common/src/database/postgres.ts @@ -57,7 +57,7 @@ export function buildPgDatabaseConfig( * Gets the postgres connection config * * @param dbConfig The database config - * @param parseConnectionString Flag to explictly control connection string parsing + * @param parseConnectionString Flag to explicitly control connection string parsing */ export function getPgConnectionConfig( dbConfig: Config, diff --git a/packages/backend/src/plugins/catalog.ts b/packages/backend/src/plugins/catalog.ts index b8e99e6398..3c205a3800 100644 --- a/packages/backend/src/plugins/catalog.ts +++ b/packages/backend/src/plugins/catalog.ts @@ -14,37 +14,21 @@ * limitations under the License. */ +import { useHotCleanup } from '@backstage/backend-common'; import { + CatalogBuilder, createRouter, - DatabaseEntitiesCatalog, - DatabaseLocationsCatalog, - DatabaseManager, - HigherOrderOperations, - LocationReaders, runPeriodically, } from '@backstage/plugin-catalog-backend'; import { PluginEnvironment } from '../types'; -import { useHotCleanup } from '@backstage/backend-common'; -export default async function createPlugin({ - logger, - config, - reader, - database, -}: PluginEnvironment) { - const locationReader = new LocationReaders({ logger, reader, config }); - - const db = await DatabaseManager.createDatabase(await database.getClient(), { - logger, - }); - const entitiesCatalog = new DatabaseEntitiesCatalog(db); - const locationsCatalog = new DatabaseLocationsCatalog(db); - const higherOrderOperation = new HigherOrderOperations( +export default async function createPlugin(env: PluginEnvironment) { + const builder = new CatalogBuilder(env); + const { entitiesCatalog, locationsCatalog, - locationReader, - logger, - ); + higherOrderOperation, + } = await builder.build(); useHotCleanup( module, @@ -55,6 +39,6 @@ export default async function createPlugin({ entitiesCatalog, locationsCatalog, higherOrderOperation, - logger, + logger: env.logger, }); } diff --git a/packages/catalog-model/src/EntityPolicies.test.ts b/packages/catalog-model/src/EntityPolicies.test.ts new file mode 100644 index 0000000000..cd869f798c --- /dev/null +++ b/packages/catalog-model/src/EntityPolicies.test.ts @@ -0,0 +1,110 @@ +import { Entity } from './entity'; +/* + * 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 { EntityPolicies } from './EntityPolicies'; +import { EntityPolicy } from './types'; + +describe('EntityPolicies', () => { + const p1: jest.Mocked = { enforce: jest.fn() }; + const p2: jest.Mocked = { enforce: jest.fn() }; + const entity1: Entity = { + apiVersion: 'a1', + kind: 'k1', + metadata: { name: 'n1' }, + }; + const entity2: Entity = { + apiVersion: 'a2', + kind: 'k2', + metadata: { name: 'n2' }, + }; + + afterEach(() => jest.resetAllMocks()); + + describe('allOf', () => { + it('resolves when no policies', async () => { + const policy = EntityPolicies.allOf([]); + await expect(policy.enforce(entity1)).resolves.toBe(entity1); + }); + + it('resolves when all resolve', async () => { + p1.enforce.mockResolvedValue(entity1); + p2.enforce.mockResolvedValue(entity1); + const policy = EntityPolicies.allOf([p1, p2]); + await expect(policy.enforce(entity1)).resolves.toBe(entity1); + }); + + it('rejects when any rejects', async () => { + p1.enforce.mockResolvedValue(entity1); + p2.enforce.mockRejectedValue(new Error('a')); + const policy = EntityPolicies.allOf([p1, p2]); + await expect(policy.enforce(entity1)).rejects.toThrow('a'); + }); + + it('rejects when any ignores', async () => { + p1.enforce.mockResolvedValue(entity1); + p2.enforce.mockResolvedValue(undefined); + const policy = EntityPolicies.allOf([p1, p2]); + await expect(policy.enforce(entity1)).rejects.toThrow( + /did not return a result/, + ); + }); + + it('passes through transforms properly', async () => { + p1.enforce.mockResolvedValue(entity2); + p2.enforce.mockResolvedValue(entity2); + const policy = EntityPolicies.allOf([p1, p2]); + await expect(policy.enforce(entity1)).resolves.toBe(entity2); + expect(p1.enforce).toBeCalledWith(entity1); + expect(p2.enforce).toBeCalledWith(entity2); + }); + }); + + describe('oneOf', () => { + it('rejects when no policies', async () => { + const policy = EntityPolicies.oneOf([]); + await expect(policy.enforce(entity1)).rejects.toThrow(/did not match/); + }); + + it('resolves when one resolves', async () => { + p1.enforce.mockResolvedValue(undefined); + p2.enforce.mockResolvedValue(entity1); + const policy = EntityPolicies.oneOf([p1, p2]); + await expect(policy.enforce(entity1)).resolves.toBe(entity1); + }); + + it('rejects when one rejects first', async () => { + p1.enforce.mockRejectedValue(new Error('a')); + p2.enforce.mockResolvedValue(entity1); + const policy = EntityPolicies.oneOf([p1, p2]); + await expect(policy.enforce(entity1)).rejects.toThrow('a'); + }); + + it('resolves first resolution when several resolve', async () => { + p1.enforce.mockResolvedValue(entity1); + p2.enforce.mockResolvedValue(entity2); + const policy = EntityPolicies.oneOf([p1, p2]); + await expect(policy.enforce(entity1)).resolves.toBe(entity1); + }); + + it('rejects when all ignore', async () => { + p1.enforce.mockResolvedValue(undefined); + p2.enforce.mockResolvedValue(undefined); + const policy = EntityPolicies.oneOf([p1, p2]); + await expect(policy.enforce(entity1)).rejects.toThrow(/did not match/); + }); + }); +}); diff --git a/packages/catalog-model/src/EntityPolicies.ts b/packages/catalog-model/src/EntityPolicies.ts index acdfb4df2a..1f304f8f27 100644 --- a/packages/catalog-model/src/EntityPolicies.ts +++ b/packages/catalog-model/src/EntityPolicies.ts @@ -14,22 +14,7 @@ * limitations under the License. */ -import { - DefaultNamespaceEntityPolicy, - Entity, - FieldFormatEntityPolicy, - NoForeignRootFieldsEntityPolicy, - ReservedFieldsEntityPolicy, - SchemaValidEntityPolicy, -} from './entity'; -import { - apiEntityV1alpha1Policy, - componentEntityV1alpha1Policy, - groupEntityV1alpha1Policy, - locationEntityV1alpha1Policy, - templateEntityV1alpha1Policy, - userEntityV1alpha1Policy, -} from './kinds'; +import { Entity } from './entity'; import { EntityPolicy } from './types'; // Helper that requires that all of a set of policies can be successfully @@ -68,42 +53,11 @@ class AnyEntityPolicy implements EntityPolicy { } } -export class EntityPolicies implements EntityPolicy { - private readonly policy: EntityPolicy; - - static defaultPolicies(): EntityPolicy { - return EntityPolicies.allOf([ - EntityPolicies.allOf([ - new SchemaValidEntityPolicy(), - new DefaultNamespaceEntityPolicy(), - new NoForeignRootFieldsEntityPolicy(), - new FieldFormatEntityPolicy(), - new ReservedFieldsEntityPolicy(), - ]), - EntityPolicies.anyOf([ - componentEntityV1alpha1Policy, - groupEntityV1alpha1Policy, - userEntityV1alpha1Policy, - locationEntityV1alpha1Policy, - templateEntityV1alpha1Policy, - apiEntityV1alpha1Policy, - ]), - ]); - } - - static allOf(policies: EntityPolicy[]): EntityPolicy { +export const EntityPolicies = { + allOf(policies: EntityPolicy[]) { return new AllEntityPolicies(policies); - } - - static anyOf(policies: EntityPolicy[]): EntityPolicy { + }, + oneOf(policies: EntityPolicy[]) { return new AnyEntityPolicy(policies); - } - - constructor(policy: EntityPolicy = EntityPolicies.defaultPolicies()) { - this.policy = policy; - } - - enforce(entity: Entity): Promise { - return this.policy.enforce(entity); - } -} + }, +}; diff --git a/packages/create-app/templates/default-app/packages/backend/src/plugins/catalog.ts b/packages/create-app/templates/default-app/packages/backend/src/plugins/catalog.ts index aee38a5fa0..10ac116ded 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/plugins/catalog.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/plugins/catalog.ts @@ -1,44 +1,28 @@ +import { useHotCleanup } from '@backstage/backend-common'; import { + CatalogBuilder, createRouter, - DatabaseEntitiesCatalog, - DatabaseLocationsCatalog, - DatabaseManager, - HigherOrderOperations, - LocationReaders, runPeriodically, } from '@backstage/plugin-catalog-backend'; import { PluginEnvironment } from '../types'; -import { useHotCleanup } from '@backstage/backend-common'; -export default async function createPlugin({ - logger, - config, - reader, - database, -}: PluginEnvironment) { - const locationReader = new LocationReaders({ logger, reader, config }); - - const db = await DatabaseManager.createDatabase(await database.getClient(), - { logger }, - ); - const entitiesCatalog = new DatabaseEntitiesCatalog(db); - const locationsCatalog = new DatabaseLocationsCatalog(db); - const higherOrderOperation = new HigherOrderOperations( +export default async function createPlugin(env: PluginEnvironment) { + const builder = new CatalogBuilder(env); + const { entitiesCatalog, locationsCatalog, - locationReader, - logger, - ); + higherOrderOperation, + } = await builder.build(); useHotCleanup( module, - runPeriodically(() => higherOrderOperation.refreshAllLocations(), 10000), + runPeriodically(() => higherOrderOperation.refreshAllLocations(), 100000), ); return await createRouter({ entitiesCatalog, locationsCatalog, higherOrderOperation, - logger, + logger: env.logger, }); } diff --git a/plugins/catalog-backend/src/database/DatabaseManager.ts b/plugins/catalog-backend/src/database/DatabaseManager.ts index eb307c9478..465bf654f2 100644 --- a/plugins/catalog-backend/src/database/DatabaseManager.ts +++ b/plugins/catalog-backend/src/database/DatabaseManager.ts @@ -46,21 +46,31 @@ export class DatabaseManager { return new CommonDatabase(knex, logger); } - public static async createInMemoryDatabase( - options: Partial = {}, - ): Promise { + public static async createInMemoryDatabase(): Promise { + const knex = await this.createInMemoryDatabaseConnection(); + return await this.createDatabase(knex); + } + + public static async createInMemoryDatabaseConnection(): Promise { const knex = Knex({ client: 'sqlite3', connection: ':memory:', useNullAsDefault: true, }); + knex.client.pool.on('createSuccess', (_eventId: any, resource: any) => { resource.run('PRAGMA foreign_keys = ON', () => {}); }); - return DatabaseManager.createDatabase(knex, options); + + return knex; } public static async createTestDatabase(): Promise { + const knex = await this.createTestDatabaseConnection(); + return await this.createDatabase(knex); + } + + public static async createTestDatabaseConnection(): Promise { const config: Knex.Config = { /* client: 'pg', @@ -91,11 +101,7 @@ export class DatabaseManager { knex.client.pool.on('createSuccess', (_eventId: any, resource: any) => { resource.run('PRAGMA foreign_keys = ON', () => {}); }); - await knex.migrate.latest({ - directory: migrationsDir, - }); - const { logger } = defaultOptions; - return new CommonDatabase(knex, logger); + return knex; } } diff --git a/plugins/catalog-backend/src/index.ts b/plugins/catalog-backend/src/index.ts index 89fd4d37ed..f621930d10 100644 --- a/plugins/catalog-backend/src/index.ts +++ b/plugins/catalog-backend/src/index.ts @@ -17,5 +17,5 @@ export * from './catalog'; export * from './database'; export * from './ingestion'; -export * from './service/router'; +export * from './service'; export * from './util'; diff --git a/plugins/catalog-backend/src/ingestion/LocationReaders.ts b/plugins/catalog-backend/src/ingestion/LocationReaders.ts index 87c75e21e5..0f6ad18f97 100644 --- a/plugins/catalog-backend/src/ingestion/LocationReaders.ts +++ b/plugins/catalog-backend/src/ingestion/LocationReaders.ts @@ -14,32 +14,16 @@ * limitations under the License. */ -import { getVoidLogger, UrlReader } from '@backstage/backend-common'; +import { UrlReader } from '@backstage/backend-common'; import { Entity, - EntityPolicies, - EntityPolicy, ENTITY_DEFAULT_NAMESPACE, LocationSpec, } from '@backstage/catalog-model'; -import { Config, ConfigReader } from '@backstage/config'; +import { Config } from '@backstage/config'; import { Logger } from 'winston'; import { CatalogRulesEnforcer } from './CatalogRules'; -import { AnnotateLocationEntityProcessor } from './processors/AnnotateLocationEntityProcessor'; -import { AzureApiReaderProcessor } from './processors/AzureApiReaderProcessor'; -import { BitbucketApiReaderProcessor } from './processors/BitbucketApiReaderProcessor'; -import { CodeOwnersProcessor } from './processors/CodeOwnersProcessor'; -import { EntityPolicyProcessor } from './processors/EntityPolicyProcessor'; -import { FileReaderProcessor } from './processors/FileReaderProcessor'; -import { GithubOrgReaderProcessor } from './processors/GithubOrgReaderProcessor'; -import { GithubReaderProcessor } from './processors/GithubReaderProcessor'; -import { GitlabApiReaderProcessor } from './processors/GitlabApiReaderProcessor'; -import { GitlabReaderProcessor } from './processors/GitlabReaderProcessor'; -import { LdapOrgReaderProcessor } from './processors/LdapOrgReaderProcessor'; -import { LocationRefProcessor } from './processors/LocationEntityProcessor'; -import { PlaceholderProcessor } from './processors/PlaceholderProcessor'; import * as result from './processors/results'; -import { StaticLocationProcessor } from './processors/StaticLocationProcessor'; import { LocationProcessor, LocationProcessorDataResult, @@ -49,8 +33,6 @@ import { LocationProcessorLocationResult, LocationProcessorResult, } from './processors/types'; -import { UrlReaderProcessor } from './processors/UrlReaderProcessor'; -import { YamlProcessor } from './processors/YamlProcessor'; import { LocationReader, ReadLocationResult } from './types'; // The max amount of nesting depth of generated work items @@ -58,94 +40,25 @@ const MAX_DEPTH = 10; type Options = { reader: UrlReader; - logger?: Logger; - config?: Config; - processors?: LocationProcessor[]; + logger: Logger; + config: Config; + processors: LocationProcessor[]; + rulesEnforcer: CatalogRulesEnforcer; }; /** * Implements the reading of a location through a series of processor tasks. */ export class LocationReaders implements LocationReader { - private readonly logger: Logger; - private readonly processors: LocationProcessor[]; - private readonly rulesEnforcer: CatalogRulesEnforcer; + private readonly options: Options; - static defaultProcessors(options: { - logger: Logger; - reader: UrlReader; - config?: Config; - entityPolicy?: EntityPolicy; - }): LocationProcessor[] { - const { - logger, - config = new ConfigReader({}, 'missing-config'), - entityPolicy = new EntityPolicies(), - } = options; - - // TODO(Rugvip): These are added for backwards compatibility if config exists - // The idea is to have everyone migrate from using the old processors to the new - // integration config driven UrlReaders. In an upcoming release we can then completely - // remove support for the old processors, but still keep handling the deprecated location - // types for a while, but with a warning. - const oldProcessors = []; - const pc = config.getOptionalConfig('catalog.processors'); - if (pc?.has('github')) { - logger.warn( - `Using deprecated configuration for catalog.processors.github, move to using integrations.github instead`, - ); - oldProcessors.push(GithubReaderProcessor.fromConfig(config, logger)); - } - if (pc?.has('gitlabApi')) { - logger.warn( - `Using deprecated configuration for catalog.processors.gitlabApi, move to using integrations.gitlab instead`, - ); - oldProcessors.push(new GitlabApiReaderProcessor(config)); - oldProcessors.push(new GitlabReaderProcessor()); - } - if (pc?.has('bitbucketApi')) { - logger.warn( - `Using deprecated configuration for catalog.processors.bitbucketApi, move to using integrations.bitbucket instead`, - ); - oldProcessors.push(new BitbucketApiReaderProcessor(config)); - } - if (pc?.has('azureApi')) { - logger.warn( - `Using deprecated configuration for catalog.processors.azureApi, move to using integrations.azure instead`, - ); - oldProcessors.push(new AzureApiReaderProcessor(config)); - } - - return [ - StaticLocationProcessor.fromConfig(config), - new FileReaderProcessor(), - ...oldProcessors, - GithubOrgReaderProcessor.fromConfig(config, { logger }), - LdapOrgReaderProcessor.fromConfig(config, { logger }), - new UrlReaderProcessor(options), - new YamlProcessor(), - PlaceholderProcessor.default({ reader: options.reader }), - new CodeOwnersProcessor({ reader: options.reader }), - new EntityPolicyProcessor(entityPolicy), - new LocationRefProcessor(), - new AnnotateLocationEntityProcessor(), - ]; - } - - constructor({ - logger = getVoidLogger(), - config, - reader, - processors = LocationReaders.defaultProcessors({ logger, reader, config }), - }: Options) { - this.logger = logger; - this.processors = processors; - this.rulesEnforcer = config - ? CatalogRulesEnforcer.fromConfig(config) - : new CatalogRulesEnforcer(CatalogRulesEnforcer.defaultRules); + constructor(options: Options) { + this.options = options; } async read(location: LocationSpec): Promise { + const { rulesEnforcer, logger } = this.options; + const output: ReadLocationResult = { entities: [], errors: [] }; let items: LocationProcessorResult[] = [result.location(location, false)]; @@ -159,7 +72,7 @@ export class LocationReaders implements LocationReader { } else if (item.type === 'data') { await this.handleData(item, emit); } else if (item.type === 'entity') { - if (this.rulesEnforcer.isAllowed(item.entity, item.location)) { + if (rulesEnforcer.isAllowed(item.entity, item.location)) { const entity = await this.handleEntity(item, emit); output.entities.push({ entity, @@ -190,7 +103,7 @@ export class LocationReaders implements LocationReader { } const message = `Max recursion depth ${MAX_DEPTH} reached for ${location.type} ${location.target}`; - this.logger.warn(message); + logger.warn(message); output.errors.push({ location, error: new Error(message) }); return output; } @@ -199,7 +112,9 @@ export class LocationReaders implements LocationReader { item: LocationProcessorLocationResult, emit: LocationProcessorEmit, ) { - for (const processor of this.processors) { + const { processors, logger } = this.options; + + for (const processor of processors) { if (processor.readLocation) { try { if ( @@ -210,21 +125,23 @@ export class LocationReaders implements LocationReader { } catch (e) { const message = `Processor ${processor.constructor.name} threw an error while reading location ${item.location.type} ${item.location.target}, ${e}`; emit(result.generalError(item.location, message)); - this.logger.warn(message); + logger.warn(message); } } } const message = `No processor was able to read location ${item.location.type} ${item.location.target}`; emit(result.inputError(item.location, message)); - this.logger.warn(message); + logger.warn(message); } private async handleData( item: LocationProcessorDataResult, emit: LocationProcessorEmit, ) { - for (const processor of this.processors) { + const { processors, logger } = this.options; + + for (const processor of processors) { if (processor.parseData) { try { if (await processor.parseData(item.data, item.location, emit)) { @@ -233,7 +150,7 @@ export class LocationReaders implements LocationReader { } catch (e) { const message = `Processor ${processor.constructor.name} threw an error while parsing ${item.location.type} ${item.location.target}, ${e}`; emit(result.generalError(item.location, message)); - this.logger.warn(message); + logger.warn(message); } } } @@ -246,9 +163,11 @@ export class LocationReaders implements LocationReader { item: LocationProcessorEntityResult, emit: LocationProcessorEmit, ): Promise { + const { processors, logger } = this.options; + let current = item.entity; - for (const processor of this.processors) { + for (const processor of processors) { if (processor.processEntity) { try { current = await processor.processEntity(current, item.location, emit); @@ -261,7 +180,7 @@ export class LocationReaders implements LocationReader { const name = !current.metadata ? '' : current.metadata.name; const message = `Processor ${processor.constructor.name} threw an error while processing entity ${current.kind}:${namespace}/${name} at ${item.location.type} ${item.location.target}, ${e}`; emit(result.generalError(item.location, message)); - this.logger.warn(message); + logger.warn(message); } } } @@ -273,18 +192,20 @@ export class LocationReaders implements LocationReader { item: LocationProcessorErrorResult, emit: LocationProcessorEmit, ) { - this.logger.debug( + const { processors, logger } = this.options; + + logger.debug( `Encountered error at location ${item.location.type} ${item.location.target}, ${item.error}`, ); - for (const processor of this.processors) { + for (const processor of processors) { if (processor.handleError) { try { await processor.handleError(item.error, item.location, emit); } catch (e) { const message = `Processor ${processor.constructor.name} threw an error while handling another error at ${item.location.type} ${item.location.target}, ${e}`; emit(result.generalError(item.location, message)); - this.logger.warn(message); + logger.warn(message); } } } diff --git a/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.test.ts index c196458e2a..294cb10f29 100644 --- a/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.test.ts @@ -21,6 +21,7 @@ import { PlaceholderResolver, ResolverParams, ResolverRead, + textPlaceholderResolver, yamlPlaceholderResolver, } from './PlaceholderProcessor'; @@ -134,9 +135,12 @@ describe('PlaceholderProcessor', () => { expect(read).not.toBeCalled(); }); - it('has builtin text support', async () => { + it('works with the text resolver', async () => { read.mockResolvedValue(Buffer.from('TEXT', 'utf-8')); - const processor = PlaceholderProcessor.default({ reader }); + const processor = new PlaceholderProcessor({ + resolvers: { text: textPlaceholderResolver }, + reader, + }); await expect( processor.processEntity( @@ -163,11 +167,14 @@ describe('PlaceholderProcessor', () => { ); }); - it('has builtin json support', async () => { + it('works with the json resolver', async () => { read.mockResolvedValue( Buffer.from(JSON.stringify({ a: ['b', 7] }), 'utf-8'), ); - const processor = PlaceholderProcessor.default({ reader }); + const processor = new PlaceholderProcessor({ + resolvers: { json: jsonPlaceholderResolver }, + reader, + }); await expect( processor.processEntity( @@ -194,9 +201,12 @@ describe('PlaceholderProcessor', () => { ); }); - it('has builtin yaml support', async () => { + it('works with the yaml resolver', async () => { read.mockResolvedValue(Buffer.from('foo:\n - bar: 7', 'utf-8')); - const processor = PlaceholderProcessor.default({ reader }); + const processor = new PlaceholderProcessor({ + resolvers: { yaml: yamlPlaceholderResolver }, + reader, + }); await expect( processor.processEntity( @@ -225,7 +235,10 @@ describe('PlaceholderProcessor', () => { it('resolves absolute path for absolute location', async () => { read.mockResolvedValue(Buffer.from('TEXT', 'utf-8')); - const processor = PlaceholderProcessor.default({ reader }); + const processor = new PlaceholderProcessor({ + resolvers: { text: textPlaceholderResolver }, + reader, + }); await expect( processor.processEntity( @@ -258,7 +271,10 @@ describe('PlaceholderProcessor', () => { it('resolves absolute path for relative file location', async () => { read.mockResolvedValue(Buffer.from('TEXT', 'utf-8')); - const processor = PlaceholderProcessor.default({ reader }); + const processor = new PlaceholderProcessor({ + resolvers: { text: textPlaceholderResolver }, + reader, + }); await expect( processor.processEntity( @@ -291,10 +307,13 @@ describe('PlaceholderProcessor', () => { it('not resolves relative file path for relative file location', async () => { // We explicitly don't support this case, as it would allow for file system - // traversel attacks. If we want to implement this, we need to have additional + // traversal attacks. If we want to implement this, we need to have additional // security measures in place! read.mockResolvedValue(Buffer.from('TEXT', 'utf-8')); - const processor = PlaceholderProcessor.default({ reader }); + const processor = new PlaceholderProcessor({ + resolvers: { text: textPlaceholderResolver }, + reader, + }); await expect( processor.processEntity( diff --git a/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.ts index 91257b68a4..630c78d600 100644 --- a/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.ts @@ -43,17 +43,6 @@ type Options = { * that it then fills in with actual data. */ export class PlaceholderProcessor implements LocationProcessor { - static default({ reader }: { reader: UrlReader }) { - return new PlaceholderProcessor({ - resolvers: { - json: jsonPlaceholderResolver, - yaml: yamlPlaceholderResolver, - text: textPlaceholderResolver, - }, - reader, - }); - } - constructor(private readonly options: Options) {} async processEntity(entity: Entity, location: LocationSpec): Promise { diff --git a/plugins/catalog-backend/src/ingestion/processors/YamlProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/YamlProcessor.ts index 79ae55fae1..860f9ff752 100644 --- a/plugins/catalog-backend/src/ingestion/processors/YamlProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/YamlProcessor.ts @@ -20,6 +20,11 @@ import yaml from 'yaml'; import * as result from './results'; import { LocationProcessor, LocationProcessorEmit } from './types'; +/** + * Handles incoming raw data buffers, and if they have a yaml extension, + * attempts to parse them into structured data and emitting them as un- + * validated entities. + */ export class YamlProcessor implements LocationProcessor { async parseData( data: Buffer, diff --git a/plugins/catalog-backend/src/ingestion/processors/index.ts b/plugins/catalog-backend/src/ingestion/processors/index.ts index c9453d0247..f966ef3348 100644 --- a/plugins/catalog-backend/src/ingestion/processors/index.ts +++ b/plugins/catalog-backend/src/ingestion/processors/index.ts @@ -31,6 +31,7 @@ export { GitlabApiReaderProcessor } from './GitlabApiReaderProcessor'; export { GitlabReaderProcessor } from './GitlabReaderProcessor'; export { LocationRefProcessor } from './LocationEntityProcessor'; export { PlaceholderProcessor } from './PlaceholderProcessor'; +export type { PlaceholderResolver } from './PlaceholderProcessor'; export { StaticLocationProcessor } from './StaticLocationProcessor'; export { UrlReaderProcessor } from './UrlReaderProcessor'; export { YamlProcessor } from './YamlProcessor'; diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.test.ts b/plugins/catalog-backend/src/service/CatalogBuilder.test.ts new file mode 100644 index 0000000000..e2467654d8 --- /dev/null +++ b/plugins/catalog-backend/src/service/CatalogBuilder.test.ts @@ -0,0 +1,139 @@ +/* + * 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 { getVoidLogger, UrlReader } from '@backstage/backend-common'; +import { Entity, LocationSpec } from '@backstage/catalog-model'; +import { ConfigReader } from '@backstage/config'; +import { DatabaseManager } from '../database'; +import { LocationProcessorEmit } from '../ingestion'; +import { CatalogBuilder, CatalogEnvironment } from './CatalogBuilder'; +import * as result from '../ingestion/processors/results'; + +describe('CatalogBuilder', () => { + const db = DatabaseManager.createTestDatabaseConnection(); + const reader: jest.Mocked = { read: jest.fn() }; + const env: CatalogEnvironment = { + logger: getVoidLogger(), + database: { getClient: () => db }, + config: ConfigReader.fromConfigs([]), + reader, + }; + + afterEach(() => jest.resetAllMocks()); + + it('works with no changes', async () => { + const builder = new CatalogBuilder(env); + const built = await builder.build(); + await expect(built.entitiesCatalog.entities()).resolves.toEqual([]); + await expect(built.locationsCatalog.locations()).resolves.toEqual([ + expect.objectContaining({ + data: expect.objectContaining({ type: 'bootstrap' }), + }), + ]); + }); + + it('works with everything replaced', async () => { + reader.read.mockResolvedValue(Buffer.from('junk')); + + const builder = new CatalogBuilder(env) + .replaceReaderProcessors([ + { + async readLocation( + location: LocationSpec, + _optional: boolean, + emit: LocationProcessorEmit, + ) { + expect(location.type).toBe('test'); + emit(result.data(location, await reader.read('ignored'))); + return true; + }, + }, + ]) + .replaceParserProcessors([ + { + async parseData( + data: Buffer, + location: LocationSpec, + emit: LocationProcessorEmit, + ) { + expect(data.toString()).toEqual('junk'); + emit( + result.entity(location, { + apiVersion: 'av', + kind: 'Component', + metadata: { name: 'n' }, + }), + ); + return true; + }, + }, + ]) + .replacePreProcessors([ + { + async processEntity(entity: Entity) { + expect(entity.apiVersion).toBe('av'); + return { + ...entity, + metadata: { ...entity.metadata, namespace: 'ns' }, + }; + }, + }, + ]) + .replaceEntityPolicies([ + { + async enforce(entity: Entity) { + expect(entity.metadata.namespace).toBe('ns'); + return entity; + }, + }, + ]) + .replaceEntityKinds([ + { + async enforce(entity: Entity) { + expect(entity.metadata.namespace).toBe('ns'); + return entity; + }, + }, + ]) + .replacePostProcessors([ + { + async processEntity(entity: Entity) { + return { + ...entity, + metadata: { ...entity.metadata, post: 'p' }, + }; + }, + }, + ]); + const out = await builder.build(); + + const added = await out.higherOrderOperation.addLocation({ + type: 'test', + target: '', + }); + expect(added.entities).toEqual([ + { + apiVersion: 'av', + kind: 'Component', + metadata: expect.objectContaining({ + name: 'n', + namespace: 'ns', + post: 'p', + }), + }, + ]); + }); +}); diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts new file mode 100644 index 0000000000..e02005b817 --- /dev/null +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -0,0 +1,523 @@ +/* + * 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 { PluginDatabaseManager, UrlReader } from '@backstage/backend-common'; +import { + apiEntityV1alpha1Policy, + componentEntityV1alpha1Policy, + DefaultNamespaceEntityPolicy, + EntityPolicies, + EntityPolicy, + FieldFormatEntityPolicy, + groupEntityV1alpha1Policy, + locationEntityV1alpha1Policy, + makeValidator, + NoForeignRootFieldsEntityPolicy, + ReservedFieldsEntityPolicy, + SchemaValidEntityPolicy, + templateEntityV1alpha1Policy, + userEntityV1alpha1Policy, + Validators, +} from '@backstage/catalog-model'; +import { Config } from '@backstage/config'; +import lodash from 'lodash'; +import { Logger } from 'winston'; +import { + DatabaseEntitiesCatalog, + DatabaseLocationsCatalog, + EntitiesCatalog, + LocationsCatalog, +} from '../catalog'; +import { DatabaseManager } from '../database'; +import { + AnnotateLocationEntityProcessor, + AzureApiReaderProcessor, + BitbucketApiReaderProcessor, + CodeOwnersProcessor, + EntityPolicyProcessor, + FileReaderProcessor, + GithubOrgReaderProcessor, + GithubReaderProcessor, + GitlabApiReaderProcessor, + GitlabReaderProcessor, + HigherOrderOperation, + HigherOrderOperations, + LocationProcessor, + LocationReaders, + LocationRefProcessor, + PlaceholderProcessor, + PlaceholderResolver, + StaticLocationProcessor, + UrlReaderProcessor, + YamlProcessor, +} from '../ingestion'; +import { CatalogRulesEnforcer } from '../ingestion/CatalogRules'; +import { LdapOrgReaderProcessor } from '../ingestion/processors/LdapOrgReaderProcessor'; +import { + jsonPlaceholderResolver, + textPlaceholderResolver, + yamlPlaceholderResolver, +} from '../ingestion/processors/PlaceholderProcessor'; + +export type CatalogEnvironment = { + logger: Logger; + database: PluginDatabaseManager; + config: Config; + reader: UrlReader; +}; + +/** + * A builder that helps wire up all of the component parts of the catalog. + * + * The touch points where you can replace or extend behavior are as follows: + * + * - Reader processors can be added or replaced. These implement the + * functionality of reading raw data from a location, in the form of an + * entity definition file. + * - Parser processors can be added or replaced. These accept the raw data as + * read by the previous processors and parse it into raw structured data + * (for example, from a binary buffer containing yaml text, to a JS object + * structure). + * - Placeholder resolvers can be replaced or added. These run on the raw + * structured data between the parsing and pre-processing steps, to replace + * dollar-prefixed entries with their actual values (like $file). + * - Pre-processors can be added or replaced. These take the raw unvalidated + * data from the parser processors and can enrich or extend it before + * validation. This is the place where for example codeowners data can be + * injected into partial entity definitions. + * - Entity policies can be added or replaced. These are the first line of + * validation from the output of the pre-processing step. All policies are + * given the chance to inspect the entity, and all of them have to pass in + * order for the entity to be considered valid from an overall point of + * view. + * - Field format validators can be replaced. These check the format of + * individual core fields such as metadata.name, such that they adhere to + * certain rules. + * - Entity kinds can be added or replaced. These are the second line of + * validation that is applied after the entity policies, which add additional + * kind-specific validation (usually based on a schema). Only one of the + * entity kinds has to accept the entity, but if none of them do, the + * entity is rejected as a whole. + * - Post-processors can be added or replaced. These take the validated + * entities out of the validation step and can perform additional actions on + * them. + */ +export class CatalogBuilder { + private readonly env: CatalogEnvironment; + private entityPolicies: EntityPolicy[]; + private entityPoliciesReplace: boolean; + private entityKinds: EntityPolicy[]; + private entityKindsReplace: boolean; + private readerProcessors: LocationProcessor[]; + private readerProcessorsReplace: boolean; + private parserProcessors: LocationProcessor[]; + private parserProcessorsReplace: boolean; + private preProcessors: LocationProcessor[]; + private preProcessorsReplace: boolean; + private postProcessors: LocationProcessor[]; + private postProcessorsReplace: boolean; + private placeholderResolvers: Record; + private fieldFormatValidators: Partial; + + constructor(env: CatalogEnvironment) { + this.env = env; + this.entityPolicies = []; + this.entityPoliciesReplace = false; + this.entityKinds = []; + this.entityKindsReplace = false; + this.readerProcessors = []; + this.readerProcessorsReplace = false; + this.parserProcessors = []; + this.parserProcessorsReplace = false; + this.preProcessors = []; + this.preProcessorsReplace = false; + this.postProcessors = []; + this.postProcessorsReplace = false; + this.placeholderResolvers = {}; + this.fieldFormatValidators = {}; + } + + /** + * Adds policies that are used to validate entities between the pre- + * processing and post-processing stages. All such policies must pass for the + * entity to be considered valid. + * + * If what you want to do is to replace the rules for what format is allowed + * in various core entity fields (such as metadata.name), you may want to use + * {@link CatalogBuilder#setFieldFormatValidators} instead. + * + * @param policies One or more policies + */ + addEntityPolicy(...policies: EntityPolicy[]): CatalogBuilder { + this.entityPolicies.push(...policies); + return this; + } + + /** + * Sets what policies to use for validation of entities between the pre- + * processing and post-processing stages. All such policies must pass for the + * entity to be considered valid. + * + * If what you want to do is to replace the rules for what format is allowed + * in various core entity fields (such as metadata.name), you may want to use + * {@link CatalogBuilder#setFieldFormatValidators} instead. + * + * This function replaces the default set of policies; use with care. + * + * @param policies One or more policies + */ + replaceEntityPolicies(policies: EntityPolicy[]): CatalogBuilder { + this.entityPolicies = [...policies]; + this.entityPoliciesReplace = true; + return this; + } + + /** + * Adds entity kinds that are used to validate a certain apiVersion/kind. One + * of the entity kind policies must match a given entity for it to be + * considered valid. + * + * @param policies One or more policies + */ + addEntityKind(...policies: EntityPolicy[]): CatalogBuilder { + this.entityKinds.push(...policies); + return this; + } + + /** + * Sets what entity policies that are used to validate a certain apiVersion/ + * kind. One of the entity kind policies must match a given entity for it to + * be considered valid. + * + * This function replaces the default set of kinds; use with care. + * + * @param policies One or more policies + */ + replaceEntityKinds(policies: EntityPolicy[]): CatalogBuilder { + this.entityKinds = [...policies]; + this.entityKindsReplace = true; + return this; + } + + /** + * Adds processors that support reading of definition files. These are run + * before the entities are parsed, pre-processed, validated and post- + * processed. + * + * @param processors One or more processors + */ + addReaderProcessor(...processors: LocationProcessor[]): CatalogBuilder { + this.readerProcessors.push(...processors); + return this; + } + + /** + * Sets what processors to use for the reading of definition files. These are + * run before the entities are parsed, pre-processed, validated and post- + * processed. + * + * This function replaces the default set of processors in this stage; use + * with care. + * + * @param processors One or more processors + */ + replaceReaderProcessors(processors: LocationProcessor[]): CatalogBuilder { + this.readerProcessors = [...processors]; + this.readerProcessorsReplace = true; + return this; + } + + /** + * Adds processors that run after each definition file has been read, in + * order to parse the raw data. These are run before the entities are + * pre-processed, validated and post-processed. + * + * @param processors One or more processors + */ + addParserProcessor(...processors: LocationProcessor[]): CatalogBuilder { + this.parserProcessors.push(...processors); + return this; + } + + /** + * Sets what processors to run after each definition file has been read, in + * order to parse the raw data. These are run before the entities are + * pre-processed, validated and post-processed. + * + * This function replaces the default set of processors in this stage; use + * with care. + * + * @param processors One or more processors + */ + replaceParserProcessors(processors: LocationProcessor[]): CatalogBuilder { + this.parserProcessors = [...processors]; + this.parserProcessorsReplace = true; + return this; + } + + /** + * Adds processors that run after each entity has been read and parsed, + * but before being validated and post-processed. + * + * @param processors One or more processors + */ + addPreProcessor(...processors: LocationProcessor[]): CatalogBuilder { + this.preProcessors.push(...processors); + return this; + } + + /** + * Sets what processors to run after each entity has been read and parsed, + * but before being validated and post-processed. + * + * This function replaces the default set of processors in this stage; use + * with care. + * + * @param processors One or more processors + */ + replacePreProcessors(processors: LocationProcessor[]): CatalogBuilder { + this.preProcessors = [...processors]; + this.preProcessorsReplace = true; + return this; + } + + /** + * Adds processors that run after each entity has been read, parsed, + * run through the pre-processors, and validated. + * + * @param processors One or more processors + */ + addPostProcessor(...processors: LocationProcessor[]): CatalogBuilder { + this.postProcessors.push(...processors); + return this; + } + + /** + * Sets what processors to run after each entity has been read, parsed, + * run through the pre-processors, and validated. + * + * This function replaces the default set of processors in this stage; use + * with care. + * + * @param processors One or more processors + */ + replacePostProcessors(processors: LocationProcessor[]): CatalogBuilder { + this.postProcessors = [...processors]; + this.postProcessorsReplace = true; + return this; + } + + /** + * Adds, or overwrites, a handler for placeholders (e.g. $file) in entity + * definition files. + * + * @param key The key that identifies the placeholder, e.g. "file" + * @param resolver The resolver that gets values for this placeholder + */ + setPlaceholderResolver(key: string, resolver: PlaceholderResolver) { + this.placeholderResolvers[key] = resolver; + } + + /** + * Sets the validator function to use for one or more special fields of an + * entity. This is useful if the default rules for formatting of fields are + * not sufficient. + * + * This function has no effect if used together with + * {@link CatalogBuilder#replaceEntityPolicies}. + * + * @param validators The (subset of) validators to set + */ + setFieldFormatValidators(validators: Partial) { + lodash.merge(this.fieldFormatValidators, validators); + } + + /** + * Wires up and returns all of the component parts of the catalog + */ + async build(): Promise<{ + entitiesCatalog: EntitiesCatalog; + locationsCatalog: LocationsCatalog; + higherOrderOperation: HigherOrderOperation; + }> { + const { config, database, logger } = this.env; + + const entityPolicy = this.buildEntityPolicy(); + const processors = this.buildProcessors(entityPolicy); + const rulesEnforcer = CatalogRulesEnforcer.fromConfig(config); + + const locationReader = new LocationReaders({ + ...this.env, + processors, + rulesEnforcer, + }); + + const db = await DatabaseManager.createDatabase( + await database.getClient(), + { logger }, + ); + + const entitiesCatalog = new DatabaseEntitiesCatalog(db); + const locationsCatalog = new DatabaseLocationsCatalog(db); + const higherOrderOperation = new HigherOrderOperations( + entitiesCatalog, + locationsCatalog, + locationReader, + logger, + ); + + return { + entitiesCatalog, + locationsCatalog, + higherOrderOperation, + }; + } + + private buildEntityPolicy(): EntityPolicy { + const entityPolicies: EntityPolicy[] = this.entityPoliciesReplace + ? [new SchemaValidEntityPolicy(), ...this.entityPolicies] + : [ + new SchemaValidEntityPolicy(), + new DefaultNamespaceEntityPolicy(), + new NoForeignRootFieldsEntityPolicy(), + new FieldFormatEntityPolicy( + makeValidator(this.fieldFormatValidators), + ), + new ReservedFieldsEntityPolicy(), + ...this.entityPolicies, + ]; + + const entityKinds: EntityPolicy[] = this.entityKindsReplace + ? this.entityKinds + : [ + componentEntityV1alpha1Policy, + groupEntityV1alpha1Policy, + userEntityV1alpha1Policy, + locationEntityV1alpha1Policy, + templateEntityV1alpha1Policy, + apiEntityV1alpha1Policy, + ...this.entityKinds, + ]; + + return EntityPolicies.allOf([ + EntityPolicies.allOf(entityPolicies), + EntityPolicies.oneOf(entityKinds), + ]); + } + + private buildProcessors(entityPolicy: EntityPolicy): LocationProcessor[] { + const { config, reader } = this.env; + + const placeholderResolvers = lodash.merge( + { + json: jsonPlaceholderResolver, + yaml: yamlPlaceholderResolver, + text: textPlaceholderResolver, + }, + this.placeholderResolvers, + ); + + return [ + StaticLocationProcessor.fromConfig(config), + ...this.buildReaderProcessors(), + ...this.buildParserProcessors(), + new PlaceholderProcessor({ resolvers: placeholderResolvers, reader }), + ...this.buildPreProcessors(), + new EntityPolicyProcessor(entityPolicy), + ...this.buildPostProcessors(), + ]; + } + + private buildReaderProcessors(): LocationProcessor[] { + const { config, logger, reader } = this.env; + + if (this.readerProcessorsReplace) { + return this.readerProcessors; + } + + // TODO(Rugvip): These are added for backwards compatibility if config exists + // The idea is to have everyone migrate from using the old processors to the new + // integration config driven UrlReaders. In an upcoming release we can then completely + // remove support for the old processors, but still keep handling the deprecated location + // types for a while, but with a warning. + const oldProcessors = []; + const pc = config.getOptionalConfig('catalog.processors'); + if (pc?.has('github')) { + logger.warn( + `Using deprecated configuration for catalog.processors.github, move to using integrations.github instead`, + ); + oldProcessors.push(GithubReaderProcessor.fromConfig(config, logger)); + } + if (pc?.has('gitlabApi')) { + logger.warn( + `Using deprecated configuration for catalog.processors.gitlabApi, move to using integrations.gitlab instead`, + ); + oldProcessors.push(new GitlabApiReaderProcessor(config)); + oldProcessors.push(new GitlabReaderProcessor()); + } + if (pc?.has('bitbucketApi')) { + logger.warn( + `Using deprecated configuration for catalog.processors.bitbucketApi, move to using integrations.bitbucket instead`, + ); + oldProcessors.push(new BitbucketApiReaderProcessor(config)); + } + if (pc?.has('azureApi')) { + logger.warn( + `Using deprecated configuration for catalog.processors.azureApi, move to using integrations.azure instead`, + ); + oldProcessors.push(new AzureApiReaderProcessor(config)); + } + + return [ + new FileReaderProcessor(), + ...oldProcessors, + GithubOrgReaderProcessor.fromConfig(config, { logger }), + LdapOrgReaderProcessor.fromConfig(config, { logger }), + new UrlReaderProcessor({ reader, logger }), + ...this.readerProcessors, + ]; + } + + private buildParserProcessors(): LocationProcessor[] { + if (this.parserProcessorsReplace) { + return this.parserProcessors; + } + + return [new YamlProcessor(), ...this.parserProcessors]; + } + + private buildPreProcessors(): LocationProcessor[] { + const { reader } = this.env; + + if (this.preProcessorsReplace) { + return this.preProcessors; + } + + return [new CodeOwnersProcessor({ reader }), ...this.preProcessors]; + } + + private buildPostProcessors(): LocationProcessor[] { + if (this.postProcessorsReplace) { + return this.postProcessors; + } + + return [ + new LocationRefProcessor(), + new AnnotateLocationEntityProcessor(), + ...this.postProcessors, + ]; + } +} diff --git a/plugins/catalog-backend/src/service/index.ts b/plugins/catalog-backend/src/service/index.ts new file mode 100644 index 0000000000..c6da8db486 --- /dev/null +++ b/plugins/catalog-backend/src/service/index.ts @@ -0,0 +1,19 @@ +/* + * 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 { CatalogBuilder } from './CatalogBuilder'; +export { createRouter } from './router'; +export type { RouterOptions } from './router'; diff --git a/plugins/catalog-backend/src/service/standaloneServer.ts b/plugins/catalog-backend/src/service/standaloneServer.ts index 5939c7eb58..713a5783ba 100644 --- a/plugins/catalog-backend/src/service/standaloneServer.ts +++ b/plugins/catalog-backend/src/service/standaloneServer.ts @@ -18,16 +18,14 @@ import { createServiceBuilder, loadBackendConfig, UrlReaders, + useHotMemoize, } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { Server } from 'http'; import { Logger } from 'winston'; -import { HigherOrderOperations } from '..'; -import { DatabaseEntitiesCatalog } from '../catalog/DatabaseEntitiesCatalog'; -import { DatabaseLocationsCatalog } from '../catalog/DatabaseLocationsCatalog'; -import { DatabaseManager } from '../database/DatabaseManager'; +import { DatabaseManager } from '../database'; +import { CatalogBuilder } from './CatalogBuilder'; import { createRouter } from './router'; -import { LocationReaders } from '../ingestion'; export interface ServerOptions { port: number; @@ -41,18 +39,22 @@ export async function startStandaloneServer( const logger = options.logger.child({ service: 'catalog-backend' }); const config = ConfigReader.fromConfigs(await loadBackendConfig()); const reader = UrlReaders.default({ logger, config }); + const db = useHotMemoize(module, () => + DatabaseManager.createInMemoryDatabaseConnection(), + ); logger.debug('Creating application...'); - const db = await DatabaseManager.createInMemoryDatabase({ logger }); - const entitiesCatalog = new DatabaseEntitiesCatalog(db); - const locationsCatalog = new DatabaseLocationsCatalog(db); - const locationReader = new LocationReaders({ logger, config, reader }); - const higherOrderOperation = new HigherOrderOperations( + const builder = new CatalogBuilder({ + logger, + database: { getClient: () => db }, + config, + reader, + }); + const { entitiesCatalog, locationsCatalog, - locationReader, - logger, - ); + higherOrderOperation, + } = await builder.build(); logger.debug('Starting application server...'); const router = await createRouter({ From 612dbf66aa71d9d4436f9ec2b53d5c9ecd9b8c09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 14 Oct 2020 09:39:08 +0200 Subject: [PATCH 042/139] chore(catalog-model): add missing fields to the swapi def --- packages/catalog-model/examples/swapi-graphql.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/catalog-model/examples/swapi-graphql.yaml b/packages/catalog-model/examples/swapi-graphql.yaml index 152d9c0afa..bc5e8e60ac 100644 --- a/packages/catalog-model/examples/swapi-graphql.yaml +++ b/packages/catalog-model/examples/swapi-graphql.yaml @@ -5,6 +5,8 @@ metadata: description: SWAPI GraphQL Schema spec: type: graphql + lifecycle: production + owner: yoda@dagobah.space definition: | schema { query: Root From 7f48256f76366d8fc34436410ff1a61c56d80b90 Mon Sep 17 00:00:00 2001 From: David Tuite Date: Wed, 14 Oct 2020 08:45:45 +0100 Subject: [PATCH 043/139] Add newrelic rest api key to helm charts (#2875) --- contrib/chart/backstage/Chart.yaml | 2 +- contrib/chart/backstage/templates/backend-secret.yaml | 1 + contrib/chart/backstage/values.yaml | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/contrib/chart/backstage/Chart.yaml b/contrib/chart/backstage/Chart.yaml index 7d2a489199..24c6f2a4d7 100644 --- a/contrib/chart/backstage/Chart.yaml +++ b/contrib/chart/backstage/Chart.yaml @@ -5,7 +5,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. -version: 0.1.0 +version: 0.1.1 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. diff --git a/contrib/chart/backstage/templates/backend-secret.yaml b/contrib/chart/backstage/templates/backend-secret.yaml index 63eddc2742..dc65f5777a 100644 --- a/contrib/chart/backstage/templates/backend-secret.yaml +++ b/contrib/chart/backstage/templates/backend-secret.yaml @@ -18,4 +18,5 @@ stringData: GITHUB_TOKEN: {{ .Values.auth.githubToken }} GITLAB_TOKEN: {{ .Values.auth.gitlabToken }} AZURE_TOKEN: {{ .Values.auth.azure.api.token }} + NEW_RELIC_REST_API_KEY: {{ .Values.auth.newRelicRestApiKey }} {{- end }} diff --git a/contrib/chart/backstage/values.yaml b/contrib/chart/backstage/values.yaml index 3ce45c5330..eec9c8fbe9 100644 --- a/contrib/chart/backstage/values.yaml +++ b/contrib/chart/backstage/values.yaml @@ -243,3 +243,4 @@ auth: # Used by the scaffolder to create GitHub repos. Must have 'repo' scope. githubToken: g gitlabToken: g + newRelicRestApiKey: r From ae183f96b3bba24bc4d297d18ec9cfb23b9ec76f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 14 Oct 2020 09:46:31 +0200 Subject: [PATCH 044/139] chore(catalog-model): use unknown instead of any for validators --- .../CommonValidatorFunctions.test.ts | 2 +- .../src/validation/CommonValidatorFunctions.ts | 8 ++++---- .../validation/KubernetesValidatorFunctions.ts | 16 ++++++++-------- packages/catalog-model/src/validation/types.ts | 18 +++++++++--------- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts b/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts index 457ef64611..129fdd2b62 100644 --- a/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts +++ b/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts @@ -113,7 +113,7 @@ describe('CommonValidatorFunctions', () => { [{}, true], [{ a: 1 }, true], [{ a: undefined }, false], - ] as [any, boolean][])(`isJsonSafe %p ? %p`, (value, result) => { + ] as [unknown, boolean][])(`isJsonSafe %p ? %p`, (value, result) => { expect(CommonValidatorFunctions.isJsonSafe(value)).toBe(result); }); diff --git a/packages/catalog-model/src/validation/CommonValidatorFunctions.ts b/packages/catalog-model/src/validation/CommonValidatorFunctions.ts index 349d6d7f72..90b4e1a611 100644 --- a/packages/catalog-model/src/validation/CommonValidatorFunctions.ts +++ b/packages/catalog-model/src/validation/CommonValidatorFunctions.ts @@ -31,7 +31,7 @@ export class CommonValidatorFunctions { * @param isValidSuffix Checks that the part after the separator (or the entire value if there is no separator) is valid */ static isValidPrefixAndOrSuffix( - value: any, + value: unknown, separator: string, isValidPrefix: (value: string) => boolean, isValidSuffix: (value: string) => boolean, @@ -55,7 +55,7 @@ export class CommonValidatorFunctions { * * @param value The value to check */ - static isJsonSafe(value: any): boolean { + static isJsonSafe(value: unknown): boolean { try { return lodash.isEqual(value, JSON.parse(JSON.stringify(value))); } catch { @@ -69,7 +69,7 @@ export class CommonValidatorFunctions { * @param value The value to check * @see https://tools.ietf.org/html/rfc1123 */ - static isValidDnsSubdomain(value: any): boolean { + static isValidDnsSubdomain(value: unknown): boolean { return ( typeof value === 'string' && value.length >= 1 && @@ -84,7 +84,7 @@ export class CommonValidatorFunctions { * @param value The value to check * @see https://tools.ietf.org/html/rfc1123 */ - static isValidDnsLabel(value: any): boolean { + static isValidDnsLabel(value: unknown): boolean { return ( typeof value === 'string' && value.length >= 1 && diff --git a/packages/catalog-model/src/validation/KubernetesValidatorFunctions.ts b/packages/catalog-model/src/validation/KubernetesValidatorFunctions.ts index fa938f5fcb..ada0aa71ff 100644 --- a/packages/catalog-model/src/validation/KubernetesValidatorFunctions.ts +++ b/packages/catalog-model/src/validation/KubernetesValidatorFunctions.ts @@ -25,7 +25,7 @@ import { CommonValidatorFunctions } from './CommonValidatorFunctions'; * @see https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/#syntax-and-character-set */ export class KubernetesValidatorFunctions { - static isValidApiVersion(value: any): boolean { + static isValidApiVersion(value: unknown): boolean { return CommonValidatorFunctions.isValidPrefixAndOrSuffix( value, '/', @@ -34,7 +34,7 @@ export class KubernetesValidatorFunctions { ); } - static isValidKind(value: any): boolean { + static isValidKind(value: unknown): boolean { return ( typeof value === 'string' && value.length >= 1 && @@ -43,7 +43,7 @@ export class KubernetesValidatorFunctions { ); } - static isValidObjectName(value: any): boolean { + static isValidObjectName(value: unknown): boolean { return ( typeof value === 'string' && value.length >= 1 && @@ -52,11 +52,11 @@ export class KubernetesValidatorFunctions { ); } - static isValidNamespace(value: any): boolean { + static isValidNamespace(value: unknown): boolean { return CommonValidatorFunctions.isValidDnsLabel(value); } - static isValidLabelKey(value: any): boolean { + static isValidLabelKey(value: unknown): boolean { return CommonValidatorFunctions.isValidPrefixAndOrSuffix( value, '/', @@ -65,13 +65,13 @@ export class KubernetesValidatorFunctions { ); } - static isValidLabelValue(value: any): boolean { + static isValidLabelValue(value: unknown): boolean { return ( value === '' || KubernetesValidatorFunctions.isValidObjectName(value) ); } - static isValidAnnotationKey(value: any): boolean { + static isValidAnnotationKey(value: unknown): boolean { return CommonValidatorFunctions.isValidPrefixAndOrSuffix( value, '/', @@ -80,7 +80,7 @@ export class KubernetesValidatorFunctions { ); } - static isValidAnnotationValue(value: any): boolean { + static isValidAnnotationValue(value: unknown): boolean { return typeof value === 'string'; } } diff --git a/packages/catalog-model/src/validation/types.ts b/packages/catalog-model/src/validation/types.ts index 14706485ca..a4475c4809 100644 --- a/packages/catalog-model/src/validation/types.ts +++ b/packages/catalog-model/src/validation/types.ts @@ -15,13 +15,13 @@ */ export type Validators = { - isValidApiVersion(value: any): boolean; - isValidKind(value: any): boolean; - isValidEntityName(value: any): boolean; - isValidNamespace(value: any): boolean; - isValidLabelKey(value: any): boolean; - isValidLabelValue(value: any): boolean; - isValidAnnotationKey(value: any): boolean; - isValidAnnotationValue(value: any): boolean; - isValidTag(value: any): boolean; + isValidApiVersion(value: unknown): boolean; + isValidKind(value: unknown): boolean; + isValidEntityName(value: unknown): boolean; + isValidNamespace(value: unknown): boolean; + isValidLabelKey(value: unknown): boolean; + isValidLabelValue(value: unknown): boolean; + isValidAnnotationKey(value: unknown): boolean; + isValidAnnotationValue(value: unknown): boolean; + isValidTag(value: unknown): boolean; }; From c805350b727a98f32e7067adc6c073987fdc3fd0 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Wed, 14 Oct 2020 09:50:41 +0200 Subject: [PATCH 045/139] Add documentation section about the project structure (#2880) --- docs/support/project-structure.md | 239 ++++++++++++++++++++++++++ docs/{overview => support}/support.md | 0 microsite/sidebars.json | 2 +- mkdocs.yml | 4 +- 4 files changed, 243 insertions(+), 2 deletions(-) create mode 100644 docs/support/project-structure.md rename docs/{overview => support}/support.md (100%) diff --git a/docs/support/project-structure.md b/docs/support/project-structure.md new file mode 100644 index 0000000000..277ca3929f --- /dev/null +++ b/docs/support/project-structure.md @@ -0,0 +1,239 @@ +--- +id: project-structure +title: Backstage Project Structure +description: + Introduction to files and folders in the Backstage Project repository +--- + +Backstage is a complex project, and the GitHub repository contains many +different files and folders. This document aims to clarify what purpose of those +files and folders are. + +## General purpose files and folders + +In the project root, there are a set of files and folders which are not part of +the project as such, and may or may not be familiar to someone looking through +the code. + +- [`.changeset/`](https://github.com/spotify/backstage/tree/master/.changeset) - + This folder contains files outlining which changes occurred in the project + since the last release. These files are added manually, but managed by + [changesets](https://github.com/atlassian/changesets) and will be removed at + every new release. They are essentially building-blocks of a CHANGELOG. + +- [`.github/`](https://github.com/spotify/backstage/tree/master/.github) - + Standard GitHub folder. It contains - amongst other things - our workflow + definitions and templates. Worth noting is the + [styles](https://github.com/spotify/backstage/tree/master/.github/styles) + folder which is used for a markdown spellchecker. + +- [`.yarn/`](https://github.com/spotify/backstage/tree/master/.yarn) - Backstage + ships with it's own `yarn` implementation. This allows us to have better + control over our `yarn.lock` file and hopefully avoid problems due to yarn + versioning differences. + +- [`docker/`](https://github.com/spotify/backstage/tree/master/docker) - Files + related to our root Dockerfile. We are planning to refactor this, so expect + this folder to be moved in the future. + +- [`contrib/`](https://github.com/spotify/backstage/tree/master/contrib) - + Collection of examples or resources provided by the community. We really + appreciate contributions in here and encourage them being kept up to date. + +- [`docs/`](https://github.com/spotify/backstage/tree/master/docs) - This is + where we keep all of our documentation markdown files. These ends up on + http://backstage.io/docs. Just keep in mind that changes to + [this](https://github.com/spotify/backstage/blob/master/microsite/sidebars.json) + file also needs to be updated. + +- [`.editorconfig`](https://github.com/spotify/backstage/tree/master/.editorconfig) - + A configuration file used by most common code editors. + +- [`.imgbotconfig`](https://github.com/spotify/backstage/tree/master/.imgbotconfig) - + Configuration for a [bot](https://imgbot.net/) + +## Monorepo packages + +Every folder in both `packages/` and `plugins/` is within our monorepo setup, as +defined in +[`package.json`](https://github.com/spotify/backstage/blob/master/package.json): + +```json + "workspaces": { + "packages": [ + "packages/*", + "plugins/*" + ] + }, +``` + +Let's look at them individually. + +### `packages/` + +These are all the packages that is we use within the project. +[Plugins](#plugins) are separated out into their own folder, see further down. + +- [`app/`](https://github.com/spotify/backstage/tree/master/packages/app) - This + is our take on how an App could look like, bringing together a set of packages + and plugins into a working Backstage App. This is not a published package, and + the main goals is to provide a demo of what an App could look like, and also + enabling local development. + +- [`backend/`](https://github.com/spotify/backstage/tree/master/packages/backend) - + Every standalone backstage project will have both an `app` _and_ a `backend` + package. The `backend` uses plugins to construct a working backend that the + frontend (`app`) can use. + +- [`backend-common/`](https://github.com/spotify/backstage/tree/master/packages/backend-common) - + There are no "core" packages in the backend. Instead we have `backend-common` + which contains helper middleware´s and other utils. + +- [`catalog-model/`](https://github.com/spotify/backstage/tree/master/packages/catalog-model) - + You can considers this to be a library for working with the catalog of sorts. + It contains the definition of an + [Entity](https://backstage.io/docs/features/software-catalog/references#docsNav), + as well as validation an other logic related to it. This package can be used + in both the frontend and the backend. + +- [`cli/`](https://github.com/spotify/backstage/tree/master/packages/cli) - One + of the biggest packages in our project, the `cli` is used to build, serve, + diff, create-plugins and more. In the early days of this project, we started + out with calling tools directly - such as `eslint` - through package.json. But + as it was tricky to have a good development experience around that when we + change named tooling, we opted for wrapping those in our own cli. That way + everything looks the same in package.json. Much like + [react-scripts](https://github.com/facebook/create-react-app/tree/master/packages/react-scripts). + +- [`cli-common/`](https://github.com/spotify/backstage/tree/master/packages/cli-common) - + This package mainly handles path resolving. It is a separate package to reduce + bugs in [cli](https://github.com/spotify/backstage/tree/master/packages/cli). + We also want as few dependencies as possible to reduce download time when + running the cli which is another reason this is a separate package. + +* [`config/`](https://github.com/spotify/backstage/tree/master/packages/config) - + The way we read configuration data. This package can take a bunch of config + objects and merge them together. + [app-config.yaml](https://github.com/spotify/backstage/blob/master/app-config.yaml) + is an example of an config object. + +* [`config-loader/`](https://github.com/spotify/backstage/tree/master/packages/config-loader) - + This package is used to read config objects. It does not know how to merge, + this only reads files and passes them on to the config. As this part os only + used by the backend, we chose to separate `config` and `config-loader` into + two different packages. + +- [`core/`](https://github.com/spotify/backstage/tree/master/packages/core) - + This package contains our visual React components, some of which you can find + [here](https://backstage.io/storybook/?path=/story/plugins-examples--plugin-with-data). + Apart from that it re-exports everything from [`core-api`] so that users only + need to rely on one package. + +* [`core-api/`](https://github.com/spotify/backstage/tree/master/packages/core-api) - + This package contains apis and definitions of such. It is it's own package + because we needed to split our `test-utils` package. It's an implementation + detail that we try to hide from our users, and no-one should have to depend on + it directly. + +* [`test-utils/`](https://github.com/spotify/backstage/tree/master/packages/test-utils) - + This package contains specific testing facilities used when testing + `core-api`. + +* [`test-utils-core/`](https://github.com/spotify/backstage/tree/master/packages/test-utils-core) - + This package contains more general purpose testing facilities for testing an + Backstage App. + +* [`create-app/`](https://github.com/spotify/backstage/tree/master/packages/create-app) - + An CLI to specifically scaffold a new Backstage App. It does so by using a + [template](https://github.com/spotify/backstage/tree/master/packages/create-app/templates/default-app). + +- [`dev-utils/`](https://github.com/spotify/backstage/tree/master/packages/dev-utils) - + Helps you setup a plugin for isolated development so that it can be served + separately. + +* [`docgen/`](https://github.com/spotify/backstage/tree/master/packages/docgen) - + Uses the + [Typescript Compiler API](https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API) + to read out definitions and generate documentation for it. + +* [`e2e-test/`](https://github.com/spotify/backstage/tree/master/packages/e2e-test) - + Another CLI that can be run to try out what would happen if you built all the + packages, publish them, created a new app, and the run it. CI uses this for + e2e-tests. + +* [`storybook/`](https://github.com/spotify/backstage/tree/master/packages/storybook) - + This folder contains only the storybook config. Stories are within the core + package. The Backstage Storybook is found + [here](https://backstage.io/storybook) + +* [`techdocs-cli/`](https://github.com/spotify/backstage/tree/master/packages/techdocs-cli) - + Used for verifying TechDocs locally. + +* [`techdocs-container/`](https://github.com/spotify/backstage/tree/master/packages/techdocs-container) - + Used by the `techdocs-cli` + +* [`test-utils-core/`](https://github.com/spotify/backstage/tree/master/packages/test-utils-core) + +* [`test-utils/`](https://github.com/spotify/backstage/tree/master/packages/test-utils) + +* [`theme/`](https://github.com/spotify/backstage/tree/master/packages/theme) - + Holds the Backstage Theme. + +### `plugins/` + +Most of the functionality of an Backstage App comes from plugins. Even core +features can be plugins, take the +[catalog](https://github.com/spotify/backstage/tree/master/plugins/catalog) as +an example. + +We can categorize plugins into three different types; **Frontend**, **Backend** +and **GraphQL**. We differentiate these types of plugins when we name them, with +a dash-suffix. `-backend` means it’s a backend plugin and so on. + +One reason for splitting a plugin is because of to it's dependencies. Another +reason is for clear separation of concerns. + +Take a look at our [Plugin Gallery](https://backstage.io/plugins) or browse +through the +[`plugins/`](https://github.com/spotify/backstage/tree/master/plugins) folder. + +## Packages outside of the monorepo + +For convenience we include packages in our project that is not part of our +monorepo setup. + +- [`microsite/`](https://github.com/spotify/backstage/blob/master/microsite) - + This folder contains the source code for backstage.io. It is built with + [Docusaurus](https://docusaurus.io/). This folder is not part of the monorepo + due to dependency reasons. Look at the + [README](https://github.com/spotify/backstage/blob/master/microsite/README.md) + for instructions on how to run it locally. + +## Root files specifically used by the `app` + +These files are kept in the root of the project mostly by historical reasons. +Some of these files may be subject to be moved out of the root sometime in the +future. + +- [`.npmrc`](https://github.com/spotify/backstage/tree/master/.npmrc) - It's + common for companies to have their own npm registry, this files makes sure + that this folder use the public registry. + +- [`.vale.ini`](https://github.com/spotify/backstage/tree/master/.vale.ini) - + [Spell checker](https://github.com/errata-ai/vale) for markdown files + +- [`.yarnrc`](https://github.com/spotify/backstage/tree/master/.yarnrc) - + Enforces "our" version of yarn. + +- [`app-config.yaml`](https://github.com/spotify/backstage/tree/master/app-config.yaml) - + Configuration for the app, both frontend and backend + +- [`app-config.development.yaml`](https://github.com/spotify/backstage/tree/master/app-config.development.yaml) - + Used for overriding configuration when developing locally. + +- [`catalog-info.yaml`](https://github.com/spotify/backstage/tree/master/catalog-info.yaml) - + Description of Backstage in the Backstage Entity format. + +- [`lerna.json`](https://github.com/spotify/backstage/tree/master/lerna.json) - + [lerna](https://github.com/lerna/lerna) monorepo config. We are using + `yarn workspaces`, so this will only be used for executing scripts. diff --git a/docs/overview/support.md b/docs/support/support.md similarity index 100% rename from docs/overview/support.md rename to docs/support/support.md diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 7081e2bb1c..ad48fb1e7c 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -163,7 +163,7 @@ "architecture-decisions/adrs-adr009" ], "Contribute": ["../CONTRIBUTING"], - "Support": ["overview/support"], + "Support": ["support/support", "support/project-structure"], "FAQ": ["FAQ"] } } diff --git a/mkdocs.yml b/mkdocs.yml index 4fbec4650d..b4606131a7 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -107,7 +107,9 @@ nav: - ADR008 - Default Catalog File Name: 'architecture-decisions/adr008-default-catalog-file-name.md' - ADR009 - Entity References: 'architecture-decisions/adr009-entity-references.md' - Contribute: '../CONTRIBUTING.md' - - Support: 'overview/support.md' + - Support: + - 'support/support.md' + - 'support/project-structure.md' - FAQ: FAQ.md plugins: From 3727322ecd4645431d62f6c261080f35d56f04a9 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 14 Oct 2020 09:58:21 +0200 Subject: [PATCH 046/139] Clarify catalog info YAML, TechDocs (#2882) * Add note about catalog-info.yaml convention to format docs. * Improve TechDocs Create & Publish Docs: - Clarify the two paths to getting started - Clarify `catalog-info.yaml` / component registration - Minor grammar cleanup * Correct link to software catalog overview --- .../software-catalog/descriptor-format.md | 3 +++ .../techdocs/creating-and-publishing.md | 18 +++++++++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index 0c50e3669a..3a1db235a0 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -14,6 +14,9 @@ natively. In the API request/response cycle, a JSON representation is used, while the descriptor files are on YAML format to be more easily maintainable by humans. However, the structure and semantics is the same in both cases. +Although it's possible to name catalog entity descriptor files however you wish, +we recommend that you name them `catalog-info.yaml`. + ## Contents - [Overall Shape Of An Entity](#overall-shape-of-an-entity) diff --git a/docs/features/techdocs/creating-and-publishing.md b/docs/features/techdocs/creating-and-publishing.md index e1adedf25e..947e6de371 100644 --- a/docs/features/techdocs/creating-and-publishing.md +++ b/docs/features/techdocs/creating-and-publishing.md @@ -19,6 +19,12 @@ This section will guide you through: ## Create a basic documentation setup +If you have an existing repository that you'd like to add documentation to, skip +to the +[Manually add documentation setup](#manually-add-documentation-setup-to-already-existing-repository) +section below. Otherwise, continue reading to start a documentation repo from +scratch. + ### Use the documentation template Your working Backstage instance should by default have a documentation template @@ -35,9 +41,11 @@ setup for free. Prerequisities: -- `catalog-info.yml` file registered to Backstage. +- An existing component + [registered in backstage](../software-catalog/index.md#adding-components-to-the-catalog) + (e.g. via a `catalog-info.yaml` file). -Create a `mkdocs.yml` file in the root of the repository with the following +Create an `mkdocs.yml` file in the root of your repository with the following content: ```yaml @@ -50,8 +58,8 @@ plugins: - techdocs-core ``` -Update your `catalog-info.yaml` file in the root of the repository with the -following content: +Update your component's entity description by adding the following lines to its +`catalog-info.yaml` in the root of its repository: ```yaml metadata: @@ -59,7 +67,7 @@ metadata: backstage.io/techdocs-ref: dir:./ ``` -Create a `/docs` folder in the root of the project with at least a `index.md` +Create a `/docs` folder in the root of the project with at least an `index.md` file. _(If you add more markdown files, make sure to update the nav in the mkdocs.yml file to get a proper navigation for your documentation.)_ From 08f5e0311fa7e7b577a9ef10f7943d65110c538f Mon Sep 17 00:00:00 2001 From: Eric Nilsson Date: Wed, 14 Oct 2020 11:03:41 +0200 Subject: [PATCH 047/139] TechDocsPageHeader: Changed repo location icon (#2895) --- plugins/techdocs/src/reader/components/TechDocsPageHeader.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/techdocs/src/reader/components/TechDocsPageHeader.tsx b/plugins/techdocs/src/reader/components/TechDocsPageHeader.tsx index c59cce2b11..a7db342544 100644 --- a/plugins/techdocs/src/reader/components/TechDocsPageHeader.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsPageHeader.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import GitHubIcon from '@material-ui/icons/GitHub'; +import CodeIcon from '@material-ui/icons/Code'; import { Header, HeaderLabel, Link } from '@backstage/core'; import { CircularProgress } from '@material-ui/core'; import { ParsedEntityId } from '../../types'; @@ -73,7 +73,7 @@ export const TechDocsPageHeader = ({ target="_blank" rel="noopener noreferrer" > - + } /> From 3b17fa02f87eef8c3ea0a7a5ccf18ba43737e4ce Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Wed, 14 Oct 2020 11:07:56 +0200 Subject: [PATCH 048/139] Vale: Use files provided by lint staged to check --- scripts/check-docs-quality.js | 63 ++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/scripts/check-docs-quality.js b/scripts/check-docs-quality.js index 4f5f689105..cd41fa25c2 100755 --- a/scripts/check-docs-quality.js +++ b/scripts/check-docs-quality.js @@ -1,4 +1,3 @@ -#!/usr/bin/env node /* * Copyright 2020 Spotify AB * @@ -15,67 +14,71 @@ * limitations under the License. */ const { execSync, spawnSync } = require('child_process'); -const path = require('path'); // eslint-disable-next-line import/no-extraneous-dependencies const commandExists = require('command-exists'); -const listFilesTrackedByGit = 'git ls-files'; - const inheritStdIo = { stdio: 'inherit', }; const LINT_SKIPPED_MESSAGE = 'Skipping documentation quality check (vale not found). Install vale linter (https://docs.errata.ai/vale/install) to enable.\n'; -const ERROR_MESSAGE = - 'Language linter (vale) generated errors. Please check the errors and review any markdown files that you changed.\n'; +const ERROR_MESSAGE = `Language linter (vale) generated errors. Please check the errors and review any markdown files that you changed. + Possibly update .github/styles/vocab.txt to add new valid words.\n`; + +// Note: Make sure the script is run as `node check-docs-quality.js [FILES]` instead of `./check-docs-quality.js [FILES]` +// If the script receives arguments (file paths), the script is run exclusively on them. (e.g. when run via pre-commit hook) +const getFilesToLint = () => { + // Files have been provided as arguments + if (process.argv.length > 2) { + return process.argv.slice(2); + } + + let command = `git ls-files | ./node_modules/.bin/shx grep ".md"`; + if (process.platform === 'win32') { + command = `git ls-files | .\\node_modules\\.bin\\shx grep ".md"`; + } + + return execSync(command, { + stdio: ['ignore', 'pipe', 'inherit'], + }) + .toString() + .split('\n'); +}; // Proceed with the script only if Vale linter is installed. Limit the friction and surprises caused by the script. commandExists('vale') .catch(() => { console.log(LINT_SKIPPED_MESSAGE); - process.exit(0); + // process.exit(0); + process.exit(1); }) .then(() => { + const filesToLint = getFilesToLint(); + console.log('files to lint'); + console.log(filesToLint); + // xargs is not supported by shx. if (process.platform === 'win32') { - const validMDFilesCommand = `${listFilesTrackedByGit} | .\\node_modules\\.bin\\shx grep ".md"`; try { - // get list of all md files except in directories of gitignore. - let filesToLint = execSync(validMDFilesCommand, { - stdio: ['ignore', 'pipe', 'inherit'], - }); - - // set all file(s) path as absolute path - filesToLint = filesToLint - .toString() - .split('\n') - .map(filepath => - filepath ? path.join(process.cwd(), filepath) : null, - ) - .filter(Boolean); - const output = spawnSync('vale', filesToLint, inheritStdIo); // if the command does not succeed if (output.status !== 0) { // if it contains system level error. [in this case vale does not exist] if (output.error) { - console.error(ERROR_MESSAGE); + console.log(ERROR_MESSAGE); } process.exit(1); } } catch (e) { - console.error(e.message); + console.log(e.message); process.exit(1); } } else { - const validMDFilesCommand = `${listFilesTrackedByGit} | ./node_modules/.bin/shx grep ".md"`; - // use xargs - try { - execSync(`${validMDFilesCommand} | xargs vale`, inheritStdIo); - } catch (e) { - console.error(ERROR_MESSAGE); + const output = spawnSync('vale', filesToLint, inheritStdIo); + if (output.status !== 0) { + console.log(ERROR_MESSAGE); process.exit(1); } } From c1d6bc03f6ac7722987fcde48a8e89359149260f Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Wed, 14 Oct 2020 11:16:23 +0200 Subject: [PATCH 049/139] Vale: Enforce vale to be run on CI --- scripts/check-docs-quality.js | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/scripts/check-docs-quality.js b/scripts/check-docs-quality.js index cd41fa25c2..f29463939b 100755 --- a/scripts/check-docs-quality.js +++ b/scripts/check-docs-quality.js @@ -23,8 +23,9 @@ const inheritStdIo = { const LINT_SKIPPED_MESSAGE = 'Skipping documentation quality check (vale not found). Install vale linter (https://docs.errata.ai/vale/install) to enable.\n'; -const ERROR_MESSAGE = `Language linter (vale) generated errors. Please check the errors and review any markdown files that you changed. +const LINT_ERROR_MESSAGE = `Language linter (vale) generated errors. Please check the errors and review any markdown files that you changed. Possibly update .github/styles/vocab.txt to add new valid words.\n`; +const VALE_NOT_FOUND_MESSAGE = `Language linter (vale) was not found. Please install vale linter (https://docs.errata.ai/vale/install).\n`; // Note: Make sure the script is run as `node check-docs-quality.js [FILES]` instead of `./check-docs-quality.js [FILES]` // If the script receives arguments (file paths), the script is run exclusively on them. (e.g. when run via pre-commit hook) @@ -47,27 +48,29 @@ const getFilesToLint = () => { }; // Proceed with the script only if Vale linter is installed. Limit the friction and surprises caused by the script. +// On CI, we want to ensure vale linter is run. commandExists('vale') .catch(() => { + if (process.env.CI) { + console.log(VALE_NOT_FOUND_MESSAGE); + process.exit(1); + } console.log(LINT_SKIPPED_MESSAGE); - // process.exit(0); - process.exit(1); + process.exit(0); }) .then(() => { const filesToLint = getFilesToLint(); - console.log('files to lint'); - console.log(filesToLint); - // xargs is not supported by shx. if (process.platform === 'win32') { + // Windows try { const output = spawnSync('vale', filesToLint, inheritStdIo); - // if the command does not succeed + // If the command does not succeed if (output.status !== 0) { - // if it contains system level error. [in this case vale does not exist] + // If it contains system level error. In this case vale does not exist. if (output.error) { - console.log(ERROR_MESSAGE); + console.log(LINT_ERROR_MESSAGE); } process.exit(1); } @@ -76,9 +79,10 @@ commandExists('vale') process.exit(1); } } else { + // Unix const output = spawnSync('vale', filesToLint, inheritStdIo); if (output.status !== 0) { - console.log(ERROR_MESSAGE); + console.log(LINT_ERROR_MESSAGE); process.exit(1); } } From e4f53f0e30354e52ab6718418d4f59c62098b757 Mon Sep 17 00:00:00 2001 From: Nicholas Chew <52598569+niccheww@users.noreply.github.com> Date: Wed, 14 Oct 2020 18:42:19 +0800 Subject: [PATCH 050/139] customisation design for navbar (#2873) Co-authored-by: nicholas.chew --- packages/core/src/layout/Sidebar/Items.tsx | 4 ++-- packages/theme/src/themes.ts | 4 ++++ packages/theme/src/types.ts | 2 ++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/core/src/layout/Sidebar/Items.tsx b/packages/core/src/layout/Sidebar/Items.tsx index 5a476e2209..2eca89f873 100644 --- a/packages/core/src/layout/Sidebar/Items.tsx +++ b/packages/core/src/layout/Sidebar/Items.tsx @@ -40,7 +40,7 @@ const useStyles = makeStyles(theme => { return { root: { - color: '#b5b5b5', + color: theme.palette.navigation.color, display: 'flex', flexFlow: 'row nowrap', alignItems: 'center', @@ -96,7 +96,7 @@ const useStyles = makeStyles(theme => { selected: { '&$root': { borderLeft: `solid ${selectedIndicatorWidth}px ${theme.palette.navigation.indicator}`, - color: '#ffffff', + color: theme.palette.navigation.selectedColor, }, '&$closed': { width: drawerWidthClosed - selectedIndicatorWidth, diff --git a/packages/theme/src/themes.ts b/packages/theme/src/themes.ts index 0ec3304ac5..82076aec3c 100644 --- a/packages/theme/src/themes.ts +++ b/packages/theme/src/themes.ts @@ -64,6 +64,8 @@ export const lightTheme = createTheme({ navigation: { background: '#171717', indicator: '#9BF0E1', + color: '#b5b5b5', + selectedColor: '#FFF', }, pinSidebarButton: { icon: '#181818', @@ -122,6 +124,8 @@ export const darkTheme = createTheme({ navigation: { background: '#424242', indicator: '#9BF0E1', + color: '#b5b5b5', + selectedColor: '#FFF', }, pinSidebarButton: { icon: '#404040', diff --git a/packages/theme/src/types.ts b/packages/theme/src/types.ts index 1d244f8cd4..8eeeddd17d 100644 --- a/packages/theme/src/types.ts +++ b/packages/theme/src/types.ts @@ -46,6 +46,8 @@ type PaletteAdditions = { navigation: { background: string; indicator: string; + color: string; + selectedColor: string; }; tabbar: { indicator: string; From d4f221bee223e9f86cb1684eba39ff549a97a60b Mon Sep 17 00:00:00 2001 From: Althaf Hameez Date: Wed, 14 Oct 2020 19:26:58 +0800 Subject: [PATCH 051/139] Update techdocs url to the new default --- docs/features/techdocs/getting-started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/techdocs/getting-started.md b/docs/features/techdocs/getting-started.md index 2e5c8d000d..72efb6eee0 100644 --- a/docs/features/techdocs/getting-started.md +++ b/docs/features/techdocs/getting-started.md @@ -58,7 +58,7 @@ The default storage and request URLs: ```yaml techdocs: storageUrl: http://localhost:7000/api/techdocs/static/docs - requestUrl: http://localhost:7000/api/techdocs/docs + requestUrl: http://localhost:7000/api/techdocs/ ``` If you want `techdocs-backend` to manage building and publishing, you want From cb867d885445cf204a59e1758b447f0a857647d1 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 14 Oct 2020 13:36:12 +0200 Subject: [PATCH 052/139] Friendlier error when NR APM API Key isn't configured. --- .../NewRelicFetchComponent/NewRelicFetchComponent.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/plugins/newrelic/src/components/NewRelicFetchComponent/NewRelicFetchComponent.tsx b/plugins/newrelic/src/components/NewRelicFetchComponent/NewRelicFetchComponent.tsx index 9b3791e062..4322bba10b 100644 --- a/plugins/newrelic/src/components/NewRelicFetchComponent/NewRelicFetchComponent.tsx +++ b/plugins/newrelic/src/components/NewRelicFetchComponent/NewRelicFetchComponent.tsx @@ -107,10 +107,15 @@ const NewRelicFetchComponent: FC<{}> = () => { const configApi = useApi(configApiRef); const apiBaseUrl = configApi.getString('newrelic.api.baseUrl'); const apiKey = configApi.getString('newrelic.api.key'); + const apiKeyIsSet = apiKey && apiKey !== 'NEW_RELIC_REST_API_KEY'; const { value, loading, error } = useAsync(async (): Promise< NewRelicApplication[] > => { + if (!apiKeyIsSet) { + return []; + } + const response = await fetch(`${apiBaseUrl}/applications.json`, { headers: { 'X-Api-Key': apiKey, @@ -126,6 +131,12 @@ const NewRelicFetchComponent: FC<{}> = () => { return ; } else if (error) { return {error.message}; + } else if (!apiKeyIsSet) { + return ( + + No REST API Key configured. Check your app-config.yaml. + + ); } return ; From 179a960b15b71d2f699f76c3e35b71377b4e5728 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20=C3=85lund?= Date: Wed, 14 Oct 2020 16:43:39 +0200 Subject: [PATCH 053/139] Tweak missing annotation emptystate (#2897) --- packages/core/src/components/EmptyState/EmptyState.tsx | 2 +- .../components/EmptyState/MissingAnnotationEmptyState.tsx | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/core/src/components/EmptyState/EmptyState.tsx b/packages/core/src/components/EmptyState/EmptyState.tsx index 1af5ff6dd5..fdd9738735 100644 --- a/packages/core/src/components/EmptyState/EmptyState.tsx +++ b/packages/core/src/components/EmptyState/EmptyState.tsx @@ -33,7 +33,7 @@ const useStyles = makeStyles(theme => ({ type Props = { title: string; - description?: string; + description?: string | JSX.Element; missing: 'field' | 'info' | 'content' | 'data'; action?: JSX.Element; }; diff --git a/packages/core/src/components/EmptyState/MissingAnnotationEmptyState.tsx b/packages/core/src/components/EmptyState/MissingAnnotationEmptyState.tsx index d8847fdf47..1c27ba27d3 100644 --- a/packages/core/src/components/EmptyState/MissingAnnotationEmptyState.tsx +++ b/packages/core/src/components/EmptyState/MissingAnnotationEmptyState.tsx @@ -48,11 +48,17 @@ const useStyles = makeStyles(theme => ({ export const MissingAnnotationEmptyState = ({ annotation }: Props) => { const classes = useStyles(); + const description = ( + + The {annotation} annotation is missing. You need to add the + annotation to your component if you want to enable this tool. + + ); return ( From 0ede39fd085307ddc72ff4daf43736651a220e16 Mon Sep 17 00:00:00 2001 From: Matthew Clarke Date: Wed, 14 Oct 2020 15:46:11 +0100 Subject: [PATCH 054/139] split service locator and cluster locators (#2900) * split service locator and cluster locators * fix test --- app-config.yaml | 4 +- plugins/kubernetes-backend/README.md | 22 +++-- .../examples/dice-roller/README.md | 4 +- ...r.test.ts => ConfigClusterLocator.test.ts} | 16 ++-- ...sterLocator.ts => ConfigClusterLocator.ts} | 15 ++-- .../src/cluster-locator/index.test.ts | 68 +++++++++++++++ .../src/cluster-locator/index.ts | 47 +++++++++++ .../src/cluster-locator/types.ts | 18 ---- .../MultiTenantServiceLocator.test.ts | 82 +++++++++++++++++++ .../MultiTenantServiceLocator.ts | 33 ++++++++ ...ubernetesObjectsByServiceIdHandler.test.ts | 14 ++-- .../getKubernetesObjectsByServiceIdHandler.ts | 8 +- .../src/service/router.test.ts | 10 +-- .../kubernetes-backend/src/service/router.ts | 52 +++++++----- plugins/kubernetes-backend/src/types/types.ts | 13 ++- 15 files changed, 325 insertions(+), 81 deletions(-) rename plugins/kubernetes-backend/src/cluster-locator/{MultiTenantConfigClusterLocator.test.ts => ConfigClusterLocator.test.ts} (82%) rename plugins/kubernetes-backend/src/cluster-locator/{MultiTenantConfigClusterLocator.ts => ConfigClusterLocator.ts} (65%) create mode 100644 plugins/kubernetes-backend/src/cluster-locator/index.test.ts create mode 100644 plugins/kubernetes-backend/src/cluster-locator/index.ts delete mode 100644 plugins/kubernetes-backend/src/cluster-locator/types.ts create mode 100644 plugins/kubernetes-backend/src/service-locator/MultiTenantServiceLocator.test.ts create mode 100644 plugins/kubernetes-backend/src/service-locator/MultiTenantServiceLocator.ts diff --git a/app-config.yaml b/app-config.yaml index 228eafc588..8ec43c6007 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -60,7 +60,9 @@ lighthouse: baseUrl: http://localhost:3003 kubernetes: - clusterLocatorMethod: 'configMultiTenant' + serviceLocatorMethod: 'multiTenant' + clusterLocatorMethods: + - 'config' clusters: [] integrations: diff --git a/plugins/kubernetes-backend/README.md b/plugins/kubernetes-backend/README.md index fbbde1fe65..a9fdc14707 100644 --- a/plugins/kubernetes-backend/README.md +++ b/plugins/kubernetes-backend/README.md @@ -8,21 +8,33 @@ It responds to Kubernetes requests from the frontend. ## Configuration -### clusterLocatorMethod +### serviceLocatorMethod This configures how to determine which clusters a component is running in. -Currently, the only valid locator method is: +Currently, the only valid serviceLocatorMethod is: -#### configMultiTenant +#### multiTenant This configuration assumes that all components run on all the provided clusters. +### clusterLocatorMethods + +This is used to determine where to retrieve cluster configuration from. + +Currently, the only valid serviceLocatorMethod is: + +#### config + +This clusterLocatorMethod will read cluster information in from config + Example: ```yaml kubernetes: - clusterLocatorMethod: 'configMultiTenant' + serviceLocatorMethod: 'multiTenant' + clusterLocatorMethods: + - 'config' clusters: - url: http://127.0.0.1:9999 name: minikube @@ -35,7 +47,7 @@ kubernetes: ##### clusters -Used by the `configMultiTenant` `clusterLocatorMethod` to construct Kubernetes clients. +Used by the `config` `clusterLocatorMethods` to construct Kubernetes clients. ###### url diff --git a/plugins/kubernetes-backend/examples/dice-roller/README.md b/plugins/kubernetes-backend/examples/dice-roller/README.md index f97f760c9f..774867de04 100644 --- a/plugins/kubernetes-backend/examples/dice-roller/README.md +++ b/plugins/kubernetes-backend/examples/dice-roller/README.md @@ -27,7 +27,9 @@ Update `app-config.development.yaml` as follows. ```yaml kubernetes: - clusterLocatorMethod: 'configMultiTenant' + serviceLocatorMethod: 'multiTenant' + clusterLocatorMethods: + - 'config' clusters: - url: name: minikube diff --git a/plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts similarity index 82% rename from plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.test.ts rename to plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts index dd77a11bae..550703e430 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.test.ts @@ -15,10 +15,10 @@ */ import '@backstage/backend-common'; -import { MultiTenantConfigClusterLocator } from './MultiTenantConfigClusterLocator'; import { ConfigReader, Config } from '@backstage/config'; +import { ConfigClusterLocator } from './ConfigClusterLocator'; -describe('MultiTenantConfigClusterLocator', () => { +describe('ConfigClusterLocator', () => { it('empty clusters returns empty cluster details', async () => { const config: Config = new ConfigReader( { @@ -27,11 +27,11 @@ describe('MultiTenantConfigClusterLocator', () => { 'ctx', ); - const sut = MultiTenantConfigClusterLocator.fromConfig( + const sut = ConfigClusterLocator.fromConfig( config.getConfigArray('clusters'), ); - const result = await sut.getClusterByServiceId('ignored'); + const result = await sut.getClusters(); expect(result).toStrictEqual([]); }); @@ -50,11 +50,11 @@ describe('MultiTenantConfigClusterLocator', () => { 'ctx', ); - const sut = MultiTenantConfigClusterLocator.fromConfig( + const sut = ConfigClusterLocator.fromConfig( config.getConfigArray('clusters'), ); - const result = await sut.getClusterByServiceId('ignored'); + const result = await sut.getClusters(); expect(result).toStrictEqual([ { @@ -86,11 +86,11 @@ describe('MultiTenantConfigClusterLocator', () => { 'ctx', ); - const sut = MultiTenantConfigClusterLocator.fromConfig( + const sut = ConfigClusterLocator.fromConfig( config.getConfigArray('clusters'), ); - const result = await sut.getClusterByServiceId('ignored'); + const result = await sut.getClusters(); expect(result).toStrictEqual([ { diff --git a/plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts similarity index 65% rename from plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.ts rename to plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts index 5a1f0f3839..5ff4581f71 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/ConfigClusterLocator.ts @@ -14,23 +14,20 @@ * limitations under the License. */ +import { ClusterDetails, KubernetesClustersSupplier } from '..'; import { Config } from '@backstage/config'; -import { ClusterDetails, KubernetesClusterLocator } from '..'; -// This cluster locator assumes that every service is located on every cluster -// Therefore it will always return all clusters in an app configuration file -export class MultiTenantConfigClusterLocator - implements KubernetesClusterLocator { +export class ConfigClusterLocator implements KubernetesClustersSupplier { private readonly clusterDetails: ClusterDetails[]; constructor(clusterDetails: ClusterDetails[]) { this.clusterDetails = clusterDetails; } - static fromConfig(config: Config[]): MultiTenantConfigClusterLocator { + static fromConfig(config: Config[]): ConfigClusterLocator { // TODO: Add validation that authProvider is required and serviceAccountToken // is required if authProvider is serviceAccount - return new MultiTenantConfigClusterLocator( + return new ConfigClusterLocator( config.map(c => { return { name: c.getString('name'), @@ -42,9 +39,7 @@ export class MultiTenantConfigClusterLocator ); } - // As this implementation always returns all clusters serviceId is ignored here - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async getClusterByServiceId(_serviceId: string): Promise { + async getClusters(): Promise { return this.clusterDetails; } } diff --git a/plugins/kubernetes-backend/src/cluster-locator/index.test.ts b/plugins/kubernetes-backend/src/cluster-locator/index.test.ts new file mode 100644 index 0000000000..32a09cb496 --- /dev/null +++ b/plugins/kubernetes-backend/src/cluster-locator/index.test.ts @@ -0,0 +1,68 @@ +/* + * 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 { Config, ConfigReader } from '../../../../packages/config/src'; +import { getCombinedClusterDetails } from './index'; + +describe('getCombinedClusterDetails', () => { + it('should retrieve cluster details from config', async () => { + const config: Config = new ConfigReader( + { + kubernetes: { + clusters: [ + { + name: 'cluster1', + serviceAccountToken: 'token', + url: 'http://localhost:8080', + authProvider: 'serviceAccount', + }, + { + name: 'cluster2', + url: 'http://localhost:8081', + authProvider: 'google', + }, + ], + }, + }, + 'ctx', + ); + + const result = await getCombinedClusterDetails(['config'], config); + + expect(result).toStrictEqual([ + { + name: 'cluster1', + serviceAccountToken: 'token', + url: 'http://localhost:8080', + authProvider: 'serviceAccount', + }, + { + name: 'cluster2', + serviceAccountToken: undefined, + url: 'http://localhost:8081', + authProvider: 'google', + }, + ]); + }); + + it('throws an error when using an unsupported cluster locator', async () => { + await expect( + getCombinedClusterDetails(['magic' as any], new ConfigReader({}, 'ctx')), + ).rejects.toStrictEqual( + new Error('Unsupported kubernetes.clusterLocatorMethods: "magic"'), + ); + }); +}); diff --git a/plugins/kubernetes-backend/src/cluster-locator/index.ts b/plugins/kubernetes-backend/src/cluster-locator/index.ts new file mode 100644 index 0000000000..ea418768b4 --- /dev/null +++ b/plugins/kubernetes-backend/src/cluster-locator/index.ts @@ -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 { ClusterDetails, ClusterLocatorMethod } from '..'; +import { Config } from '../../../../packages/config/src'; +import { ConfigClusterLocator } from './ConfigClusterLocator'; + +export { ConfigClusterLocator } from './ConfigClusterLocator'; + +export const getCombinedClusterDetails = async ( + clusterLocatorMethods: ClusterLocatorMethod[], + rootConfig: Config, +): Promise => { + return Promise.all( + clusterLocatorMethods.map(clusterLocatorMethod => { + switch (clusterLocatorMethod) { + case 'config': + return ConfigClusterLocator.fromConfig( + rootConfig.getConfigArray('kubernetes.clusters'), + ).getClusters(); + default: + throw new Error( + `Unsupported kubernetes.clusterLocatorMethods: "${clusterLocatorMethod}"`, + ); + } + }), + ) + .then(res => { + return res.flat(); + }) + .catch(e => { + throw e; + }); +}; diff --git a/plugins/kubernetes-backend/src/cluster-locator/types.ts b/plugins/kubernetes-backend/src/cluster-locator/types.ts deleted file mode 100644 index dae8eb6d60..0000000000 --- a/plugins/kubernetes-backend/src/cluster-locator/types.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * 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 type ClusterLocatorMethod = 'configMultiTenant' | 'http'; -export type AuthProviderType = 'google' | 'serviceAccount'; diff --git a/plugins/kubernetes-backend/src/service-locator/MultiTenantServiceLocator.test.ts b/plugins/kubernetes-backend/src/service-locator/MultiTenantServiceLocator.test.ts new file mode 100644 index 0000000000..13761e2671 --- /dev/null +++ b/plugins/kubernetes-backend/src/service-locator/MultiTenantServiceLocator.test.ts @@ -0,0 +1,82 @@ +/* + * 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 '@backstage/backend-common'; +import { MultiTenantServiceLocator } from './MultiTenantServiceLocator'; + +describe('MultiTenantConfigClusterLocator', () => { + it('empty clusters returns empty cluster details', async () => { + const sut = new MultiTenantServiceLocator([]); + + const result = await sut.getClustersByServiceId('ignored'); + + expect(result).toStrictEqual([]); + }); + + it('one clusters returns one cluster details', async () => { + const sut = new MultiTenantServiceLocator([ + { + name: 'cluster1', + url: 'http://localhost:8080', + authProvider: 'serviceAccount', + serviceAccountToken: '12345', + }, + ]); + + const result = await sut.getClustersByServiceId('ignored'); + + expect(result).toStrictEqual([ + { + name: 'cluster1', + serviceAccountToken: '12345', + url: 'http://localhost:8080', + authProvider: 'serviceAccount', + }, + ]); + }); + + it('two clusters returns two cluster details', async () => { + const sut = new MultiTenantServiceLocator([ + { + name: 'cluster1', + serviceAccountToken: 'token', + url: 'http://localhost:8080', + authProvider: 'serviceAccount', + }, + { + name: 'cluster2', + url: 'http://localhost:8081', + authProvider: 'google', + }, + ]); + + const result = await sut.getClustersByServiceId('ignored'); + + expect(result).toStrictEqual([ + { + name: 'cluster1', + serviceAccountToken: 'token', + url: 'http://localhost:8080', + authProvider: 'serviceAccount', + }, + { + name: 'cluster2', + url: 'http://localhost:8081', + authProvider: 'google', + }, + ]); + }); +}); diff --git a/plugins/kubernetes-backend/src/service-locator/MultiTenantServiceLocator.ts b/plugins/kubernetes-backend/src/service-locator/MultiTenantServiceLocator.ts new file mode 100644 index 0000000000..a5822a2078 --- /dev/null +++ b/plugins/kubernetes-backend/src/service-locator/MultiTenantServiceLocator.ts @@ -0,0 +1,33 @@ +/* + * 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 { ClusterDetails, KubernetesServiceLocator } from '..'; + +// This locator assumes that every service is located on every cluster +// Therefore it will always return all clusters provided +export class MultiTenantServiceLocator implements KubernetesServiceLocator { + private readonly clusterDetails: ClusterDetails[]; + + constructor(clusterDetails: ClusterDetails[]) { + this.clusterDetails = clusterDetails; + } + + // As this implementation always returns all clusters serviceId is ignored here + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async getClustersByServiceId(_serviceId: string): Promise { + return this.clusterDetails; + } +} diff --git a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts index 4218c64e80..8c26ded2d4 100644 --- a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts +++ b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts @@ -22,7 +22,7 @@ const TEST_SERVICE_ID = 'my-service'; const fetchObjectsByServiceId = jest.fn(); -const getClusterByServiceId = jest.fn(); +const getClustersByServiceId = jest.fn(); const mockFetch = (mock: jest.Mock) => { mock.mockImplementation((serviceId: string, clusterDetails: ClusterDetails) => @@ -70,7 +70,7 @@ describe('handleGetKubernetesObjectsByServiceId', () => { }); it('retrieve objects for one cluster', async () => { - getClusterByServiceId.mockImplementation(() => + getClustersByServiceId.mockImplementation(() => Promise.resolve([ { name: 'test-cluster', @@ -87,13 +87,13 @@ describe('handleGetKubernetesObjectsByServiceId', () => { fetchObjectsByServiceId, }, { - getClusterByServiceId, + getClustersByServiceId, }, getVoidLogger(), {}, ); - expect(getClusterByServiceId.mock.calls.length).toBe(1); + expect(getClustersByServiceId.mock.calls.length).toBe(1); expect(fetchObjectsByServiceId.mock.calls.length).toBe(1); expect(result).toStrictEqual({ items: [ @@ -140,7 +140,7 @@ describe('handleGetKubernetesObjectsByServiceId', () => { }); it('retrieve objects for two clusters', async () => { - getClusterByServiceId.mockImplementation(() => + getClustersByServiceId.mockImplementation(() => Promise.resolve([ { name: 'test-cluster', @@ -161,7 +161,7 @@ describe('handleGetKubernetesObjectsByServiceId', () => { fetchObjectsByServiceId, }, { - getClusterByServiceId, + getClustersByServiceId, }, getVoidLogger(), { @@ -171,7 +171,7 @@ describe('handleGetKubernetesObjectsByServiceId', () => { }, ); - expect(getClusterByServiceId.mock.calls.length).toBe(1); + expect(getClustersByServiceId.mock.calls.length).toBe(1); expect(fetchObjectsByServiceId.mock.calls.length).toBe(2); expect(result).toStrictEqual({ items: [ diff --git a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.ts b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.ts index f0b21cbc0c..6fc6092bd6 100644 --- a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.ts +++ b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.ts @@ -18,7 +18,7 @@ import { Logger } from 'winston'; import { AuthRequestBody, ClusterDetails, - KubernetesClusterLocator, + KubernetesServiceLocator, KubernetesFetcher, KubernetesObjectTypes, ObjectsByServiceIdResponse, @@ -29,7 +29,7 @@ import { KubernetesAuthTranslatorGenerator } from '../kubernetes-auth-translator export type GetKubernetesObjectsByServiceIdHandler = ( serviceId: string, fetcher: KubernetesFetcher, - clusterLocator: KubernetesClusterLocator, + serviceLocator: KubernetesServiceLocator, logger: Logger, requestBody: AuthRequestBody, objectsToFetch?: Set, @@ -49,12 +49,12 @@ const DEFAULT_OBJECTS = new Set([ export const handleGetKubernetesObjectsByServiceId: GetKubernetesObjectsByServiceIdHandler = async ( serviceId, fetcher, - clusterLocator, + serviceLocator, logger, requestBody, objectsToFetch = DEFAULT_OBJECTS, ) => { - const clusterDetails: ClusterDetails[] = await clusterLocator.getClusterByServiceId( + const clusterDetails: ClusterDetails[] = await serviceLocator.getClustersByServiceId( serviceId, ); diff --git a/plugins/kubernetes-backend/src/service/router.test.ts b/plugins/kubernetes-backend/src/service/router.test.ts index 4e9a206eb4..6f9fbbf70f 100644 --- a/plugins/kubernetes-backend/src/service/router.test.ts +++ b/plugins/kubernetes-backend/src/service/router.test.ts @@ -19,7 +19,7 @@ import express from 'express'; import request from 'supertest'; import { makeRouter } from './router'; import { - KubernetesClusterLocator, + KubernetesServiceLocator, KubernetesFetcher, ObjectsByServiceIdResponse, } from '..'; @@ -27,7 +27,7 @@ import { describe('router', () => { let app: express.Express; let kubernetesFetcher: jest.Mocked; - let kubernetesClusterLocator: jest.Mocked; + let kubernetesServiceLocator: jest.Mocked; let handleGetByServiceId: jest.Mock>; beforeAll(async () => { @@ -35,8 +35,8 @@ describe('router', () => { fetchObjectsByServiceId: jest.fn(), }; - kubernetesClusterLocator = { - getClusterByServiceId: jest.fn(), + kubernetesServiceLocator = { + getClustersByServiceId: jest.fn(), }; handleGetByServiceId = jest.fn(); @@ -44,7 +44,7 @@ describe('router', () => { const router = makeRouter( getVoidLogger(), kubernetesFetcher, - kubernetesClusterLocator, + kubernetesServiceLocator, handleGetByServiceId as any, ); app = express().use(router); diff --git a/plugins/kubernetes-backend/src/service/router.ts b/plugins/kubernetes-backend/src/service/router.ts index 9e9b4ab2f5..b73115e1e4 100644 --- a/plugins/kubernetes-backend/src/service/router.ts +++ b/plugins/kubernetes-backend/src/service/router.ts @@ -18,8 +18,7 @@ import express from 'express'; import Router from 'express-promise-router'; import { Logger } from 'winston'; import { Config } from '@backstage/config'; -import { ClusterLocatorMethod } from '../cluster-locator/types'; -import { MultiTenantConfigClusterLocator } from '../cluster-locator/MultiTenantConfigClusterLocator'; +import { MultiTenantServiceLocator } from '../service-locator/MultiTenantServiceLocator'; import { KubernetesClientBasedFetcher } from './KubernetesFetcher'; import { KubernetesClientProvider } from './KubernetesClientProvider'; import { @@ -28,30 +27,35 @@ import { } from './getKubernetesObjectsByServiceIdHandler'; import { AuthRequestBody, - KubernetesClusterLocator, + KubernetesServiceLocator, KubernetesFetcher, -} from '../types/types'; + ServiceLocatorMethod, + ClusterLocatorMethod, + ClusterDetails, +} from '..'; +import { getCombinedClusterDetails } from '../cluster-locator'; export interface RouterOptions { logger: Logger; config: Config; } -const getClusterLocator = (config: Config): KubernetesClusterLocator => { - const clusterLocatorMethod = config.getString( - 'kubernetes.clusterLocatorMethod', - ) as ClusterLocatorMethod; +const getServiceLocator = ( + config: Config, + clusterDetails: ClusterDetails[], +): KubernetesServiceLocator => { + const serviceLocatorMethod = config.getString( + 'kubernetes.serviceLocatorMethod', + ) as ServiceLocatorMethod; - switch (clusterLocatorMethod) { - case 'configMultiTenant': - return MultiTenantConfigClusterLocator.fromConfig( - config.getConfigArray('kubernetes.clusters'), - ); + switch (serviceLocatorMethod) { + case 'multiTenant': + return new MultiTenantServiceLocator(clusterDetails); case 'http': throw new Error('not implemented'); default: throw new Error( - `Unsupported kubernetes.clusterLocatorMethod "${clusterLocatorMethod}"`, + `Unsupported kubernetes.clusterLocatorMethod "${serviceLocatorMethod}"`, ); } }; @@ -59,13 +63,12 @@ const getClusterLocator = (config: Config): KubernetesClusterLocator => { export const makeRouter = ( logger: Logger, fetcher: KubernetesFetcher, - clusterLocator: KubernetesClusterLocator, + serviceLocator: KubernetesServiceLocator, handleGetByServiceId: GetKubernetesObjectsByServiceIdHandler, ): express.Router => { const router = Router(); router.use(express.json()); - // TODO error handling router.post('/services/:serviceId', async (req, res) => { const serviceId = req.params.serviceId; const requestBody: AuthRequestBody = req.body; @@ -73,7 +76,7 @@ export const makeRouter = ( const response = await handleGetByServiceId( serviceId, fetcher, - clusterLocator, + serviceLocator, logger, requestBody, ); @@ -96,17 +99,26 @@ export async function createRouter( logger.info('Initializing Kubernetes backend'); - const clusterLocator = getClusterLocator(options.config); - const fetcher = new KubernetesClientBasedFetcher({ kubernetesClientProvider: new KubernetesClientProvider(), logger, }); + const clusterLocatorMethods = options.config.getStringArray( + 'kubernetes.clusterLocatorMethods', + ) as ClusterLocatorMethod[]; + + const clusterDetails = await getCombinedClusterDetails( + clusterLocatorMethods, + options.config, + ); + + const serviceLocator = getServiceLocator(options.config, clusterDetails); + return makeRouter( logger, fetcher, - clusterLocator, + serviceLocator, handleGetKubernetesObjectsByServiceId, ); } diff --git a/plugins/kubernetes-backend/src/types/types.ts b/plugins/kubernetes-backend/src/types/types.ts index eb2817f268..1422511c21 100644 --- a/plugins/kubernetes-backend/src/types/types.ts +++ b/plugins/kubernetes-backend/src/types/types.ts @@ -118,8 +118,13 @@ export interface KubernetesFetcher { } // Used to locate which cluster(s) a service is running on -export interface KubernetesClusterLocator { - getClusterByServiceId(serviceId: string): Promise; +export interface KubernetesServiceLocator { + getClustersByServiceId(serviceId: string): Promise; +} + +// Used to load cluster details from different sources +export interface KubernetesClustersSupplier { + getClusters(): Promise; } export type KubernetesErrorTypes = @@ -132,3 +137,7 @@ export interface KubernetesFetchError { statusCode?: number; resourcePath?: string; } + +export type ServiceLocatorMethod = 'multiTenant' | 'http'; // TODO implement http +export type ClusterLocatorMethod = 'config'; +export type AuthProviderType = 'google' | 'serviceAccount'; From 8e0016f7ad3d80681e95c7afcbce57d0a1485046 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 14 Oct 2020 17:49:41 +0200 Subject: [PATCH 055/139] Introduce a New Relic API class for future expansion. --- plugins/newrelic/src/api/index.ts | 96 +++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 plugins/newrelic/src/api/index.ts diff --git a/plugins/newrelic/src/api/index.ts b/plugins/newrelic/src/api/index.ts new file mode 100644 index 0000000000..692cb19e6f --- /dev/null +++ b/plugins/newrelic/src/api/index.ts @@ -0,0 +1,96 @@ +/* + * 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, DiscoveryApi } from '@backstage/core'; + +export type NewRelicApplication = { + id: number; + application_summary: NewRelicApplicationSummary; + name: string; + language: string; + health_status: string; + reporting: boolean; + settings: NewRelicApplicationSettings; + links?: NewRelicApplicationLinks; +}; + +export type NewRelicApplicationSummary = { + apdex_score: number; + error_rate: number; + host_count: number; + instance_count: number; + response_time: number; + throughput: number; +}; + +export type NewRelicApplicationSettings = { + app_apdex_threshold: number; + end_user_apdex_threshold: number; + enable_real_user_monitoring: boolean; + use_server_side_config: boolean; +}; + +export type NewRelicApplicationLinks = { + application_instances: Array; + servers: Array; + application_hosts: Array; +}; + +export type NewRelicApplications = { + applications: NewRelicApplication[]; +}; + +export const newRelicApiRef = createApiRef({ + id: 'plugin.newrelic.service', + description: 'Used by the NewRelic plugin to make requests', +}); + +const DEFAULT_PROXY_PATH_BASE = '/newrelic'; + +type Options = { + discoveryApi: DiscoveryApi; + /** + * Path to use for requests via the proxy, defaults to /newrelic + */ + proxyPathBase?: string; +}; + +export class NewRelicApi { + private readonly discoveryApi: DiscoveryApi; + private readonly proxyPathBase: string; + + constructor(options: Options) { + this.discoveryApi = options.discoveryApi; + this.proxyPathBase = options.proxyPathBase ?? DEFAULT_PROXY_PATH_BASE; + } + + async getApplications(): Promise { + const url = await this.getApiUrl('apm', 'applications.json'); + const response = await fetch(url); + const responseJson = await response.json(); + + if (response.status !== 200) { + throw new Error(responseJson?.error?.title || response.statusText); + } + + return responseJson; + } + + private async getApiUrl(product: string, path: string) { + const proxyUrl = await this.discoveryApi.getBaseUrl('proxy'); + return `${proxyUrl}${this.proxyPathBase}/${product}/api/${path}`; + } +} From 2865fcf9cb017a363eec9a8d715965c5a1d78c71 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 14 Oct 2020 17:51:47 +0200 Subject: [PATCH 056/139] Make the plugin aware of the NR API. --- plugins/newrelic/src/plugin.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/plugins/newrelic/src/plugin.ts b/plugins/newrelic/src/plugin.ts index 21aac69e8f..f247158fbd 100644 --- a/plugins/newrelic/src/plugin.ts +++ b/plugins/newrelic/src/plugin.ts @@ -14,7 +14,13 @@ * limitations under the License. */ -import { createPlugin, createRouteRef } from '@backstage/core'; +import { + createApiFactory, + createPlugin, + createRouteRef, + discoveryApiRef, +} from '@backstage/core'; +import { NewRelicApi, newRelicApiRef } from './api'; import NewRelicComponent from './components/NewRelicComponent'; export const rootRouteRef = createRouteRef({ @@ -24,6 +30,13 @@ export const rootRouteRef = createRouteRef({ export const plugin = createPlugin({ id: 'newrelic', + apis: [ + createApiFactory({ + api: newRelicApiRef, + deps: { discoveryApi: discoveryApiRef }, + factory: ({ discoveryApi }) => new NewRelicApi({ discoveryApi }), + }), + ], register({ router }) { router.addRoute(rootRouteRef, NewRelicComponent); }, From 25645e25dbc8516d0cf89b3181fdca47b513d857 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 14 Oct 2020 17:52:59 +0200 Subject: [PATCH 057/139] Refactor config/type usage out of component, use new API instead. --- .../NewRelicFetchComponent.tsx | 76 ++----------------- 1 file changed, 7 insertions(+), 69 deletions(-) diff --git a/plugins/newrelic/src/components/NewRelicFetchComponent/NewRelicFetchComponent.tsx b/plugins/newrelic/src/components/NewRelicFetchComponent/NewRelicFetchComponent.tsx index 4322bba10b..62f52e7875 100644 --- a/plugins/newrelic/src/components/NewRelicFetchComponent/NewRelicFetchComponent.tsx +++ b/plugins/newrelic/src/components/NewRelicFetchComponent/NewRelicFetchComponent.tsx @@ -15,52 +15,10 @@ */ import React, { FC } from 'react'; -import { - configApiRef, - Progress, - Table, - TableColumn, - useApi, -} from '@backstage/core'; +import { Progress, Table, TableColumn, useApi } from '@backstage/core'; import Alert from '@material-ui/lab/Alert'; import { useAsync } from 'react-use'; - -type NewRelicApplication = { - id: number; - application_summary: NewRelicApplicationSummary; - name: string; - language: string; - health_status: string; - reporting: boolean; - settings: NewRelicApplicationSettings; - links?: NewRelicApplicationLinks; -}; - -type NewRelicApplicationSummary = { - apdex_score: number; - error_rate: number; - host_count: number; - instance_count: number; - response_time: number; - throughput: number; -}; - -type NewRelicApplicationSettings = { - app_apdex_threshold: number; - end_user_apdex_threshold: number; - enable_real_user_monitoring: boolean; - use_server_side_config: boolean; -}; - -type NewRelicApplicationLinks = { - application_instances: Array; - servers: Array; - application_hosts: Array; -}; - -type NewRelicApplications = { - applications: NewRelicApplication[]; -}; +import { newRelicApiRef, NewRelicApplications } from '../../api'; export const NewRelicAPMTable: FC = ({ applications, @@ -73,7 +31,7 @@ export const NewRelicAPMTable: FC = ({ { title: 'Instance Count', field: 'instanceCount' }, { title: 'Apdex', field: 'apdexScore' }, ]; - const data = applications.map((app: NewRelicApplication) => { + const data = applications.map(app => { const { name, application_summary: applicationSummary } = app; const { response_time: responseTime, @@ -104,25 +62,11 @@ export const NewRelicAPMTable: FC = ({ }; const NewRelicFetchComponent: FC<{}> = () => { - const configApi = useApi(configApiRef); - const apiBaseUrl = configApi.getString('newrelic.api.baseUrl'); - const apiKey = configApi.getString('newrelic.api.key'); - const apiKeyIsSet = apiKey && apiKey !== 'NEW_RELIC_REST_API_KEY'; + const api = useApi(newRelicApiRef); - const { value, loading, error } = useAsync(async (): Promise< - NewRelicApplication[] - > => { - if (!apiKeyIsSet) { - return []; - } - - const response = await fetch(`${apiBaseUrl}/applications.json`, { - headers: { - 'X-Api-Key': apiKey, - }, - }); - const data: NewRelicApplications = await response.json(); - return data.applications.filter((application: NewRelicApplication) => { + const { value, loading, error } = useAsync(async () => { + const data = await api.getApplications(); + return data.applications.filter(application => { return application.hasOwnProperty('application_summary'); }); }, []); @@ -131,12 +75,6 @@ const NewRelicFetchComponent: FC<{}> = () => { return ; } else if (error) { return {error.message}; - } else if (!apiKeyIsSet) { - return ( - - No REST API Key configured. Check your app-config.yaml. - - ); } return ; From c64a0a47ea0898eebc6f99b1eadd5db0ae806c84 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 14 Oct 2020 17:54:15 +0200 Subject: [PATCH 058/139] Use new expected proxy configs to hit New Relic API. --- app-config.yaml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index 228eafc588..7dee71534f 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -34,6 +34,12 @@ proxy: $env: TRAVISCI_AUTH_TOKEN travis-api-version: 3 + '/newrelic/apm/api': + target: https://api.newrelic.com/v2 + headers: + X-Api-Key: + $env: NEW_RELIC_REST_API_KEY + organization: name: Spotify @@ -51,11 +57,6 @@ rollbar: accountToken: $env: ROLLBAR_ACCOUNT_TOKEN -newrelic: - api: - baseUrl: 'https://api.newrelic.com/v2' - key: NEW_RELIC_REST_API_KEY - lighthouse: baseUrl: http://localhost:3003 From 78fea5041efef33abdeeaff53142e914eb5ef93c Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 14 Oct 2020 17:54:48 +0200 Subject: [PATCH 059/139] Update README with new instructions on how to configure the plugin. --- plugins/newrelic/README.md | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/plugins/newrelic/README.md b/plugins/newrelic/README.md index 19bd24950c..e787d9c91f 100644 --- a/plugins/newrelic/README.md +++ b/plugins/newrelic/README.md @@ -7,16 +7,38 @@ Website: [https://newrelic.com](https://newrelic.com) ## Getting Started -Add New Relic REST API Key to `app-config.yaml` +This plugin uses the Backstage proxy to securely communicate with New Relic's +APIs. Add the following to your `app-config.yaml` to enable this configuration: ```yaml -newrelic: - api: - baseUrl: 'https://api.newrelic.com/v2' - key: +proxy: + '/newrelic/apm/api': + target: https://api.newrelic.com/v2 + headers: + X-Api-Key: + $env: NEW_RELIC_REST_API_KEY ``` -New Relic Plugin Path: [/newrelic](http://localhost:3000/newrelic) +In your production deployment of Backstage, you would also need to ensure that +you've set the `NEW_RELIC_REST_API_KEY` environment variable before starting +the backend. + +While working locally, you may wish to hard-code your API key in your +`app-config.local.yaml` like this: + +```yaml +# app-config.local.yaml +proxy: + 'newrelic/apm/api': + headers: + X-Api-Key: NRRA-YourActualApiKey +``` + +Read more about how to find or generate this key in +[New Relic's Documentation](https://docs.newrelic.com/docs/apis/get-started/intro-apis/types-new-relic-api-keys#rest-api-key). + +See if it's working by visiting the New Relic Plugin Path: +[/newrelic](http://localhost:3000/newrelic) ## Features From 4512b9967889ec6299742cae5c66494c3b7b5806 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 14 Oct 2020 18:10:45 +0200 Subject: [PATCH 060/139] Marked minor, although it is major-ish... --- .changeset/old-falcons-jump.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .changeset/old-falcons-jump.md diff --git a/.changeset/old-falcons-jump.md b/.changeset/old-falcons-jump.md new file mode 100644 index 0000000000..107d75ff88 --- /dev/null +++ b/.changeset/old-falcons-jump.md @@ -0,0 +1,25 @@ +--- +'@backstage/plugin-newrelic': minor +--- + +The New Relic plugin now uses the Backstage proxy to communicate with New Relic's API. + +Please update your `app-config.yaml` as follows: + +```yaml +# Old Config +newrelic: + api: + baseUrl: 'https://api.newrelic.com/v2' + key: NEW_RELIC_REST_API_KEY +``` + +```yaml +# New Config +proxy: + '/newrelic/apm/api': + target: https://api.newrelic.com/v2 + headers: + X-Api-Key: + $env: NEW_RELIC_REST_API_KEY +``` From 72f6cda35879342bb510b78fa6d777f2b4a92cec Mon Sep 17 00:00:00 2001 From: Andrew Thauer <6507159+andrewthauer@users.noreply.github.com> Date: Wed, 14 Oct 2020 13:13:13 -0400 Subject: [PATCH 061/139] feat: add cli build parallel flag --- .changeset/cli-parallel-build-options.md | 12 ++++ .github/styles/vocab.txt | 1 + packages/cli/package.json | 1 + packages/cli/src/commands/app/build.ts | 2 + .../cli/src/commands/backend/buildImage.ts | 4 +- packages/cli/src/lib/bundler/optimization.ts | 12 ++++ packages/cli/src/lib/bundler/types.ts | 3 + packages/cli/src/lib/packager/index.ts | 17 +++++- packages/cli/src/lib/parallel.test.ts | 59 +++++++++++++++++++ packages/cli/src/lib/parallel.ts | 47 +++++++++++++++ packages/cli/src/types.d.ts | 2 + 11 files changed, 156 insertions(+), 4 deletions(-) create mode 100644 .changeset/cli-parallel-build-options.md create mode 100644 packages/cli/src/lib/parallel.test.ts create mode 100644 packages/cli/src/lib/parallel.ts diff --git a/.changeset/cli-parallel-build-options.md b/.changeset/cli-parallel-build-options.md new file mode 100644 index 0000000000..95f9360738 --- /dev/null +++ b/.changeset/cli-parallel-build-options.md @@ -0,0 +1,12 @@ +--- +'@backstage/cli': minor +--- + +Adds a new `BACKSTAGE_CLI_BUILD_PARELLEL` environment variable to control +parallelism for some build steps. + +This is useful in CI to help avoid out of memory issues when using `terser`. The +`BACKSTAGE_CLI_BUILD_PARELLEL` environment variable can be set to +`true | false | [integer]` to override the default behaviour. See +[terser-webpack-plugin](https://github.com/webpack-contrib/terser-webpack-plugin#parallel) +for more details. diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index c20a9862b0..6a2eecdee0 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -196,6 +196,7 @@ validators Voi Wealthsimple Weaveworks +Webpack xyz yaml Zalando diff --git a/packages/cli/package.json b/packages/cli/package.json index 5bd732f1d2..084254d7ff 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -86,6 +86,7 @@ "style-loader": "^1.2.1", "sucrase": "^3.14.1", "tar": "^6.0.1", + "terser-webpack-plugin": "^1.4.3", "ts-jest": "^26.0.0", "ts-loader": "^7.0.4", "typescript": "^3.9.3", diff --git a/packages/cli/src/commands/app/build.ts b/packages/cli/src/commands/app/build.ts index 9713acd233..ae67616515 100644 --- a/packages/cli/src/commands/app/build.ts +++ b/packages/cli/src/commands/app/build.ts @@ -19,6 +19,7 @@ import { loadConfig } from '@backstage/config-loader'; import { ConfigReader } from '@backstage/config'; import { paths } from '../../lib/paths'; import { buildBundle } from '../../lib/bundler'; +import { parseParallel, PARALLEL_ENV_VAR } from '../../lib/parallel'; export default async (cmd: Command) => { const appConfigs = await loadConfig({ @@ -27,6 +28,7 @@ export default async (cmd: Command) => { }); await buildBundle({ entry: 'src/index', + parallel: parseParallel(process.env[PARALLEL_ENV_VAR]), statsJsonEnabled: cmd.stats, config: ConfigReader.fromConfigs(appConfigs), appConfigs, diff --git a/packages/cli/src/commands/backend/buildImage.ts b/packages/cli/src/commands/backend/buildImage.ts index b6e9da7b39..b6ae0dd579 100644 --- a/packages/cli/src/commands/backend/buildImage.ts +++ b/packages/cli/src/commands/backend/buildImage.ts @@ -14,12 +14,13 @@ * limitations under the License. */ +import { Command } from 'commander'; import fs from 'fs-extra'; import { join as joinPath, relative as relativePath } from 'path'; import { createDistWorkspace } from '../../lib/packager'; import { paths } from '../../lib/paths'; import { run } from '../../lib/run'; -import { Command } from 'commander'; +import { parseParallel, PARALLEL_ENV_VAR } from '../../lib/parallel'; const PKG_PATH = 'package.json'; @@ -41,6 +42,7 @@ export default async (cmd: Command) => { ...appConfigs, { src: paths.resolveTarget('Dockerfile'), dest: 'Dockerfile' }, ], + parallel: parseParallel(process.env[PARALLEL_ENV_VAR]), skeleton: 'skeleton.tar', }); console.log(`Dist workspace ready at ${tempDistWorkspace}`); diff --git a/packages/cli/src/lib/bundler/optimization.ts b/packages/cli/src/lib/bundler/optimization.ts index acd6a937d9..665eb10b21 100644 --- a/packages/cli/src/lib/bundler/optimization.ts +++ b/packages/cli/src/lib/bundler/optimization.ts @@ -15,7 +15,9 @@ */ import { Options } from 'webpack'; +import TerserPlugin from 'terser-webpack-plugin'; import { BundlingOptions } from './types'; +import { isParallelDefault } from '../parallel'; export const optimization = ( options: BundlingOptions, @@ -24,6 +26,16 @@ export const optimization = ( return { minimize: !isDev, + // Only configure when parallel is explicitly overriden from the default + ...(!isParallelDefault(options.parallel) + ? { + minimizer: [ + new TerserPlugin({ + parallel: options.parallel, + }), + ], + } + : {}), runtimeChunk: 'single', splitChunks: { automaticNameDelimiter: '-', diff --git a/packages/cli/src/lib/bundler/types.ts b/packages/cli/src/lib/bundler/types.ts index 03d68d9bb8..827d517fef 100644 --- a/packages/cli/src/lib/bundler/types.ts +++ b/packages/cli/src/lib/bundler/types.ts @@ -16,6 +16,7 @@ import { AppConfig, Config } from '@backstage/config'; import { BundlingPathsOptions } from './paths'; +import { ParallelOption } from '../parallel'; export type BundlingOptions = { checksEnabled: boolean; @@ -23,6 +24,7 @@ export type BundlingOptions = { config: Config; appConfigs: AppConfig[]; baseUrl: URL; + parallel?: ParallelOption; }; export type BackendBundlingOptions = Omit & { @@ -37,6 +39,7 @@ export type ServeOptions = BundlingPathsOptions & { export type BuildOptions = BundlingPathsOptions & { statsJsonEnabled: boolean; + parallel?: ParallelOption; config: Config; appConfigs: AppConfig[]; }; diff --git a/packages/cli/src/lib/packager/index.ts b/packages/cli/src/lib/packager/index.ts index 564317e46a..56f1ee453c 100644 --- a/packages/cli/src/lib/packager/index.ts +++ b/packages/cli/src/lib/packager/index.ts @@ -20,10 +20,11 @@ import { resolve as resolvePath, relative as relativePath, } from 'path'; +import { tmpdir } from 'os'; +import tar, { CreateOptions } from 'tar'; import { paths } from '../paths'; import { run } from '../run'; -import tar, { CreateOptions } from 'tar'; -import { tmpdir } from 'os'; +import { ParallelOption } from '../parallel'; type LernaPackage = { name: string; @@ -58,6 +59,11 @@ type Options = { */ buildDependencies?: boolean; + /** + * Enable (true/false) or control amount of (number) parallelism in some build steps. + */ + parallel?: ParallelOption; + /** * If set, creates a skeleton tarball that contains all package.json files * with the same structure as the workspace dir. @@ -85,7 +91,12 @@ export async function createDistWorkspace( if (options.buildDependencies) { const scopeArgs = targets.flatMap(target => ['--scope', target.name]); - await run('yarn', ['lerna', 'run', ...scopeArgs, 'build'], { + const lernaArgs = + options.parallel && Number.isInteger(options.parallel) + ? ['--concurrency', options.parallel.toString()] + : []; + + await run('yarn', ['lerna', ...lernaArgs, 'run', ...scopeArgs, 'build'], { cwd: paths.targetRoot, }); } diff --git a/packages/cli/src/lib/parallel.test.ts b/packages/cli/src/lib/parallel.test.ts new file mode 100644 index 0000000000..c8173603d9 --- /dev/null +++ b/packages/cli/src/lib/parallel.test.ts @@ -0,0 +1,59 @@ +/* + * 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 { isParallelDefault, parseParallel } from './parallel'; + +describe('parallel', () => { + describe(parseParallel, () => { + it('coerces "false" string to boolean', () => { + expect(parseParallel('false')).toBeFalsy(); + }); + + it('coerces "true" to boolean', () => { + expect(parseParallel('true')).toBeTruthy(); + }); + + it('coerces number string to number', () => { + expect(parseParallel('2')).toBe(2); + }); + it.each([[true], [false], [2]])('returns itself for %p', value => { + expect(parseParallel(value as any)).toEqual(value); + }); + + it.each([[undefined], [null]])('returns true for %p', value => { + expect(parseParallel(value as any)).toBe(true); + }); + + it.each([['on'], [2.5], ['2.5']])('throws error for %p', value => { + expect(() => parseParallel(value as any)).toThrowError( + `Parallel option value '${value}' is not a boolean or integer`, + ); + }); + }); + + describe(isParallelDefault, () => { + it('returns true if default value', () => { + expect(isParallelDefault(undefined)).toBeTruthy(); + expect(isParallelDefault(true)).toBeTruthy(); + }); + + it('returns false if not default value', () => { + expect(isParallelDefault(false)).toBeFalsy(); + expect(isParallelDefault(2)).toBeFalsy(); + expect(isParallelDefault('true' as any)).toBeFalsy(); + }); + }); +}); diff --git a/packages/cli/src/lib/parallel.ts b/packages/cli/src/lib/parallel.ts new file mode 100644 index 0000000000..b6926115aa --- /dev/null +++ b/packages/cli/src/lib/parallel.ts @@ -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. + */ + +export const PARALLEL_ENV_VAR = 'BACKSTAGE_CLI_BUILD_PARALLEL'; + +export type ParallelOption = boolean | number | undefined; + +export function isParallelDefault(parallel: ParallelOption) { + return parallel === undefined || parallel === true; +} + +export function parseParallel( + parallel: boolean | string | number | undefined, +): ParallelOption { + if (parallel === undefined || parallel === null) { + return true; + } else if (typeof parallel === 'boolean') { + return parallel; + } else if (typeof parallel === 'number' && Number.isInteger(parallel)) { + return parallel; + } else if (typeof parallel === 'string') { + if (parallel === 'true') { + return true; + } else if (parallel === 'false') { + return false; + } else if (Number.isInteger(parseFloat(parallel.toString()))) { + return Number(parallel); + } + } + + throw Error( + `Parallel option value '${parallel}' is not a boolean or integer`, + ); +} diff --git a/packages/cli/src/types.d.ts b/packages/cli/src/types.d.ts index 94136f1dbc..828819ad17 100644 --- a/packages/cli/src/types.d.ts +++ b/packages/cli/src/types.d.ts @@ -29,3 +29,5 @@ declare module '@svgr/rollup' { } declare module '@rollup/plugin-yaml'; + +declare module 'terser-webpack-plugin'; From 22e1195419d3c39923fb46aebf85fb900a5db636 Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Wed, 14 Oct 2020 14:18:34 -0400 Subject: [PATCH 062/139] cost -> cost trend --- .../src/components/CostOverviewCard/CostOverviewCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx index 7f270e2374..e7d2bbf5be 100644 --- a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx +++ b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx @@ -63,7 +63,7 @@ const CostOverviewCard = ({ data }: CostOverviewCardProps) => { {formatPercent(data[0].change.ratio)} From e142a2767f72cc0c1308c07a1b1b193a4501687b Mon Sep 17 00:00:00 2001 From: Mahmood Hosseini Date: Wed, 14 Oct 2020 14:21:09 -0400 Subject: [PATCH 063/139] bug(passport-strategy-helper): Better presentation of auth errors --- .changeset/spotty-apples-visit.md | 5 +++++ .../passport/PassportStrategyHelper.test.ts | 9 +++++++-- .../lib/passport/PassportStrategyHelper.ts | 19 +++++++++++++++++-- 3 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 .changeset/spotty-apples-visit.md diff --git a/.changeset/spotty-apples-visit.md b/.changeset/spotty-apples-visit.md new file mode 100644 index 0000000000..ba481fa052 --- /dev/null +++ b/.changeset/spotty-apples-visit.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Better presentation of authentication errors diff --git a/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.test.ts b/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.test.ts index d8f6d25d0f..f7a22e9ad9 100644 --- a/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.test.ts +++ b/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.test.ts @@ -16,6 +16,7 @@ import express from 'express'; import passport from 'passport'; +import { InternalOAuthError } from 'passport-oauth2'; import { executeRedirectStrategy, executeFrameHandlerStrategy, @@ -58,7 +59,11 @@ describe('PassportStrategyHelper', () => { } class MyCustomAuthErrorStrategy extends passport.Strategy { authenticate() { - this.error(new Error('MyCustomAuth error')); + this.error( + new InternalOAuthError('MyCustomAuth error', { + data: '{ "message": "Custom message" }', + }), + ); } } class MyCustomAuthRedirectStrategy extends passport.Strategy { @@ -97,7 +102,7 @@ describe('PassportStrategyHelper', () => { ); expect(spyAuthenticate).toBeCalledTimes(1); await expect(frameHandlerStrategyPromise).rejects.toThrow( - 'Authentication failed, Error: MyCustomAuth error', + 'Authentication failed, MyCustomAuth error - Custom message', ); }); diff --git a/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.ts b/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.ts index 7bc34186f4..3264fd8faa 100644 --- a/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.ts +++ b/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.ts @@ -18,6 +18,7 @@ import express from 'express'; import passport from 'passport'; import jwtDecoder from 'jwt-decode'; import { ProfileInfo, RedirectInfo } from '../../providers/types'; +import { InternalOAuthError } from 'passport-oauth2'; export type PassportDoneCallback = ( err?: Error, @@ -95,8 +96,22 @@ export const executeFrameHandlerStrategy = async ( ) => { reject(new Error(`Authentication rejected, ${info.message ?? ''}`)); }; - strategy.error = (error: Error) => { - reject(new Error(`Authentication failed, ${error}`)); + strategy.error = (error: InternalOAuthError) => { + let message = `Authentication failed, ${error.message}`; + + if (error.oauthError?.data) { + try { + const errorData = JSON.parse(error.oauthError.data); + + if (errorData.message) { + message += ` - ${errorData.message}`; + } + } catch (parseError) { + message += ` - ${error.oauthError}`; + } + } + + reject(new Error(message)); }; strategy.redirect = () => { reject(new Error('Unexpected redirect')); From 921d705d510f00b6dc4c90516d478dbadb8fbaf5 Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Wed, 14 Oct 2020 14:21:33 -0400 Subject: [PATCH 064/139] use ratio for engineer cost display --- .../src/components/CostOverviewCard/CostOverviewCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx index e7d2bbf5be..daffe335c5 100644 --- a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx +++ b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx @@ -81,7 +81,7 @@ const CostOverviewCard = ({ data }: CostOverviewCardProps) => { Date: Thu, 15 Oct 2020 09:14:15 +0530 Subject: [PATCH 065/139] chore: update naming convention for page theme options --- packages/theme/src/types.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/theme/src/types.ts b/packages/theme/src/types.ts index b5a07e78c5..e551208f3f 100644 --- a/packages/theme/src/types.ts +++ b/packages/theme/src/types.ts @@ -72,20 +72,20 @@ type PaletteAdditions = { export type BackstagePalette = Palette & PaletteAdditions; export type BackstagePaletteOptions = PaletteOptions & PaletteAdditions; -export type pageThemeOptions = { +export type PageThemeSelector = { themeId: string; }; export interface BackstageTheme extends Theme { palette: BackstagePalette; page: PageTheme; - getPageTheme: ({ themeId }: pageThemeOptions) => PageTheme; + getPageTheme: ({ themeId }: PageThemeSelector) => PageTheme; } export interface BackstageThemeOptions extends ThemeOptions { palette: BackstagePaletteOptions; page: PageTheme; - getPageTheme: ({ themeId }: pageThemeOptions) => PageTheme; + getPageTheme: ({ themeId }: PageThemeSelector) => PageTheme; } /** From 8f0608a5ed24ffc1a27cfecc9bc0f370c98966d8 Mon Sep 17 00:00:00 2001 From: Marvin9 Date: Thu, 15 Oct 2020 09:16:39 +0530 Subject: [PATCH 066/139] chore: refactor --- .../core/src/layout/Header/Header.stories.tsx | 17 +++++++------- .../core/src/layout/Page/Page.stories.tsx | 5 ++--- packages/core/src/layout/Page/Page.tsx | 13 +++++++---- .../core/src/layout/SignInPage/SignInPage.tsx | 6 ++--- .../ApiEntityPage/ApiEntityPage.tsx | 10 ++------- .../ApiExplorerPage/ApiExplorerLayout.tsx | 10 +-------- .../components/CatalogPage/CatalogLayout.tsx | 5 +---- .../EntityPageLayout/EntityPageLayout.tsx | 10 ++------- .../AlertInstructionsLayout.tsx | 12 ++-------- .../CostInsightsLayout/CostInsightsLayout.tsx | 10 ++------- .../src/components/ExplorePluginPage.tsx | 9 ++------ .../NewProjectPage/NewProjectPage.tsx | 11 ++-------- .../ProjectDetailsPage/ProjectDetailsPage.tsx | 9 +------- .../ProjectListPage/ProjectListPage.tsx | 10 +-------- .../components/ClusterList/ClusterList.tsx | 10 ++------- .../components/ClusterPage/ClusterPage.tsx | 10 ++------- .../ProfileCatalog/ProfileCatalog.tsx | 11 ++-------- .../components/GraphiQLPage/GraphiQLPage.tsx | 10 ++------- .../KubernetesContent/KubernetesContent.tsx | 10 ++------- .../src/components/AuditList/index.tsx | 10 ++------- .../src/components/AuditView/index.tsx | 9 +------- .../src/components/CreateAudit/index.tsx | 9 +------- .../NewRelicComponent/NewRelicComponent.tsx | 10 ++------- .../RegisterComponentPage.tsx | 10 ++------- .../components/RollbarHome/RollbarHome.tsx | 9 +------- .../RollbarProjectPage/RollbarProjectPage.tsx | 9 +------- .../ScaffolderPage/ScaffolderPage.tsx | 6 ++--- .../components/TemplatePage/TemplatePage.tsx | 6 ++--- .../SentryPluginPage/SentryPluginPage.tsx | 10 ++------- .../tech-radar/src/components/RadarPage.tsx | 10 ++------- .../src/reader/components/TechDocsHome.tsx | 22 ++++--------------- .../src/reader/components/TechDocsPage.tsx | 9 +------- .../src/components/SettingsPage.tsx | 9 +------- .../components/WelcomePage/WelcomePage.tsx | 9 +------- 34 files changed, 72 insertions(+), 263 deletions(-) diff --git a/packages/core/src/layout/Header/Header.stories.tsx b/packages/core/src/layout/Header/Header.stories.tsx index f4ca74d3f8..62e6e3e52c 100644 --- a/packages/core/src/layout/Header/Header.stories.tsx +++ b/packages/core/src/layout/Header/Header.stories.tsx @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { pageTheme } from '@backstage/theme'; import React from 'react'; import { Header } from '.'; import { HeaderLabel } from '../HeaderLabel'; @@ -33,7 +32,7 @@ const labels = ( ); export const Home = () => ( - +
{labels}
@@ -47,7 +46,7 @@ export const HomeWithSubtitle = () => ( ); export const Tool = () => ( - +
{labels}
@@ -55,7 +54,7 @@ export const Tool = () => ( ); export const Service = () => ( - +
{labels}
@@ -63,7 +62,7 @@ export const Service = () => ( ); export const Website = () => ( - +
{labels}
@@ -71,7 +70,7 @@ export const Website = () => ( ); export const Library = () => ( - +
{labels}
@@ -79,7 +78,7 @@ export const Library = () => ( ); export const App = () => ( - +
{labels}
@@ -87,7 +86,7 @@ export const App = () => ( ); export const Documentation = () => ( - +
{labels}
@@ -95,7 +94,7 @@ export const Documentation = () => ( ); export const Other = () => ( - +
{labels}
diff --git a/packages/core/src/layout/Page/Page.stories.tsx b/packages/core/src/layout/Page/Page.stories.tsx index 882b989bae..b3a75c19e9 100644 --- a/packages/core/src/layout/Page/Page.stories.tsx +++ b/packages/core/src/layout/Page/Page.stories.tsx @@ -15,7 +15,6 @@ */ import React, { useState } from 'react'; -import { pageTheme } from '@backstage/theme'; import { Header, Page, @@ -196,7 +195,7 @@ export const PluginWithData = () => { const [selectedTab, setSelectedTab] = useState(2); return (
- + { export const PluginWithTable = () => { return (
- + diff --git a/packages/core/src/layout/Page/Page.tsx b/packages/core/src/layout/Page/Page.tsx index 4bc1917659..a14f772334 100644 --- a/packages/core/src/layout/Page/Page.tsx +++ b/packages/core/src/layout/Page/Page.tsx @@ -15,7 +15,7 @@ */ import React, { FC } from 'react'; -import { PageTheme } from '@backstage/theme'; +import { BackstageTheme } from '@backstage/theme'; import { makeStyles, ThemeProvider } from '@material-ui/core'; const useStyles = makeStyles(() => ({ @@ -30,13 +30,18 @@ const useStyles = makeStyles(() => ({ })); type Props = { - theme: PageTheme; + themeId: string; }; -export const Page: FC = ({ theme, children }) => { +export const Page: FC = ({ themeId, children }) => { const classes = useStyles(); return ( - ({ ...baseTheme, page: theme })}> + ({ + ...baseTheme, + page: baseTheme.getPageTheme({ themeId }), + })} + >
{children}
); diff --git a/packages/core/src/layout/SignInPage/SignInPage.tsx b/packages/core/src/layout/SignInPage/SignInPage.tsx index b2b049d161..ebc9bf278f 100644 --- a/packages/core/src/layout/SignInPage/SignInPage.tsx +++ b/packages/core/src/layout/SignInPage/SignInPage.tsx @@ -19,8 +19,7 @@ import { Page } from '../Page'; import { Header } from '../Header'; import { Content } from '../Content/Content'; import { ContentHeader } from '../ContentHeader/ContentHeader'; -import { BackstageTheme } from '@backstage/theme'; -import { Grid, useTheme } from '@material-ui/core'; +import { Grid } from '@material-ui/core'; import { SignInPageProps, useApi, configApiRef } from '@backstage/core-api'; import { useSignInProviders, getSignInProviders } from './providers'; import { IdentityProviders } from './types'; @@ -39,7 +38,6 @@ export const SignInPage: FC = ({ title, align = 'left', }) => { - const backstageTheme = useTheme(); const configApi = useApi(configApiRef); const classes = useStyles(); @@ -54,7 +52,7 @@ export const SignInPage: FC = ({ } return ( - +
{title && } diff --git a/plugins/api-docs/src/components/ApiEntityPage/ApiEntityPage.tsx b/plugins/api-docs/src/components/ApiEntityPage/ApiEntityPage.tsx index 85250a8569..f755705ac7 100644 --- a/plugins/api-docs/src/components/ApiEntityPage/ApiEntityPage.tsx +++ b/plugins/api-docs/src/components/ApiEntityPage/ApiEntityPage.tsx @@ -23,9 +23,8 @@ import { Progress, useApi, } from '@backstage/core'; -import { BackstageTheme } from '@backstage/theme'; import { catalogApiRef } from '@backstage/plugin-catalog'; -import { Box, useTheme } from '@material-ui/core'; +import { Box } from '@material-ui/core'; import { Alert } from '@material-ui/lab'; import React, { useEffect } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; @@ -65,7 +64,6 @@ const EntityPageTitle = ({ title }: EntityPageTitleProps) => ( ); export const ApiEntityPage = () => { - const backstageTheme = useTheme(); const { optionalNamespaceAndName } = useParams() as { optionalNamespaceAndName: string; }; @@ -102,11 +100,7 @@ export const ApiEntityPage = () => { ); return ( - +
} pageTitleOverride={headerTitle} diff --git a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerLayout.tsx b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerLayout.tsx index 410432d715..b534489487 100644 --- a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerLayout.tsx +++ b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerLayout.tsx @@ -15,8 +15,6 @@ */ import { Header, Page } from '@backstage/core'; -import { BackstageTheme } from '@backstage/theme'; -import { useTheme } from '@material-ui/core'; import React from 'react'; type Props = { @@ -24,14 +22,8 @@ type Props = { }; export const ApiExplorerLayout = ({ children }: Props) => { - const backstageTheme = useTheme(); - return ( - +
{ - const backstageTheme = useTheme(); const greeting = getTimeBasedGreeting(); const profile = useApi(identityApiRef).getProfile(); const userId = useApi(identityApiRef).getUserId(); return ( - +
{ - const backstageTheme = useTheme(); const { optionalNamespaceAndName, kind } = useParams() as { optionalNamespaceAndName: string; kind: string; @@ -89,11 +87,7 @@ export const EntityPageLayout = ({ const showRemovalDialog = () => setConfirmationDialogOpen(true); return ( - +
} pageTitleOverride={headerTitle} diff --git a/plugins/cost-insights/src/components/AlertInstructionsLayout/AlertInstructionsLayout.tsx b/plugins/cost-insights/src/components/AlertInstructionsLayout/AlertInstructionsLayout.tsx index f4baf227db..82954baa86 100644 --- a/plugins/cost-insights/src/components/AlertInstructionsLayout/AlertInstructionsLayout.tsx +++ b/plugins/cost-insights/src/components/AlertInstructionsLayout/AlertInstructionsLayout.tsx @@ -14,15 +14,8 @@ * limitations under the License. */ -import { BackstageTheme } from '@backstage/theme'; import React, { ReactNode } from 'react'; -import { - Box, - Button, - Container, - makeStyles, - useTheme, -} from '@material-ui/core'; +import { Box, Button, Container, makeStyles } from '@material-ui/core'; import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'; import { Header, Page } from '@backstage/core'; import { CostInsightsThemeProvider } from '../CostInsightsPage/CostInsightsThemeProvider'; @@ -44,13 +37,12 @@ const AlertInstructionsLayout = ({ title, children, }: AlertInstructionsLayoutProps) => { - const backstageTheme = useTheme(); const classes = useStyles(); return ( - +
{ - const backstageTheme = useTheme(); const classes = useStyles(); return ( - +
{ const classes = useStyles(); - const backstageTheme = useTheme(); return ( - +
{ @@ -112,14 +111,8 @@ const labels = ( ); export const NewProjectPage = () => { - const backstageTheme = useTheme(); - return ( - +
{labels}
diff --git a/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx b/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx index eb86b6fd03..7e44618441 100644 --- a/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx +++ b/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { BackstageTheme } from '@backstage/theme'; import { Content, ContentHeader, @@ -36,7 +35,6 @@ import { TableRow, Theme, Typography, - useTheme, } from '@material-ui/core'; import React from 'react'; import { useAsync } from 'react-use'; @@ -148,13 +146,8 @@ const labels = ( ); export const ProjectDetailsPage = () => { - const backstageTheme = useTheme(); return ( - +
{labels}
diff --git a/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx b/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx index 33b3813c2a..b2148bcfc8 100644 --- a/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx +++ b/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx @@ -15,7 +15,6 @@ */ // NEEDS WORK -import { BackstageTheme } from '@backstage/theme'; import { Content, ContentHeader, @@ -38,7 +37,6 @@ import { TableRow, Tooltip, Typography, - useTheme, } from '@material-ui/core'; import React from 'react'; import { useAsync } from 'react-use'; @@ -135,14 +133,8 @@ const PageContents = () => { }; export const ProjectListPage = () => { - const backstageTheme = useTheme(); - return ( - +
{labels}
diff --git a/plugins/gitops-profiles/src/components/ClusterList/ClusterList.tsx b/plugins/gitops-profiles/src/components/ClusterList/ClusterList.tsx index 3b33320abe..70bf4c8c3f 100644 --- a/plugins/gitops-profiles/src/components/ClusterList/ClusterList.tsx +++ b/plugins/gitops-profiles/src/components/ClusterList/ClusterList.tsx @@ -14,7 +14,6 @@ * limitations under the License. */ -import { BackstageTheme } from '@backstage/theme'; import React, { FC, useState } from 'react'; import { Content, @@ -29,13 +28,12 @@ import { } from '@backstage/core'; import ClusterTable from '../ClusterTable/ClusterTable'; -import { Button, useTheme } from '@material-ui/core'; +import { Button } from '@material-ui/core'; import { useAsync } from 'react-use'; import { gitOpsApiRef } from '../../api'; import { Alert } from '@material-ui/lab'; const ClusterList: FC<{}> = () => { - const backstageTheme = useTheme(); const api = useApi(gitOpsApiRef); const githubAuth = useApi(githubAuthApiRef); const [githubUsername, setGithubUsername] = useState(String); @@ -92,11 +90,7 @@ const ClusterList: FC<{}> = () => { } return ( - +
diff --git a/plugins/gitops-profiles/src/components/ClusterPage/ClusterPage.tsx b/plugins/gitops-profiles/src/components/ClusterPage/ClusterPage.tsx index f7205c3326..9b6e5bdb08 100644 --- a/plugins/gitops-profiles/src/components/ClusterPage/ClusterPage.tsx +++ b/plugins/gitops-profiles/src/components/ClusterPage/ClusterPage.tsx @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { BackstageTheme } from '@backstage/theme'; import React, { FC, useEffect, useState } from 'react'; import { Content, @@ -26,13 +25,12 @@ import { githubAuthApiRef, } from '@backstage/core'; -import { Link, useTheme } from '@material-ui/core'; +import { Link } from '@material-ui/core'; import { useParams } from 'react-router-dom'; import { gitOpsApiRef, Status } from '../../api'; import { transformRunStatus } from '../ProfileCatalog'; const ClusterPage: FC<{}> = () => { - const backstageTheme = useTheme(); const params = useParams() as { owner: string; repo: string }; const [pollingLog, setPollingLog] = useState(true); @@ -85,11 +83,7 @@ const ClusterPage: FC<{}> = () => { }, [pollingLog, api, params, githubAuth, githubAccessToken, githubUsername]); return ( - +
diff --git a/plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.tsx b/plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.tsx index fe1e97235a..a70cf97342 100644 --- a/plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.tsx +++ b/plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.tsx @@ -14,7 +14,6 @@ * limitations under the License. */ -import { BackstageTheme } from '@backstage/theme'; import React, { FC, useEffect, useState } from 'react'; import { Header, @@ -37,7 +36,7 @@ import { useApi, githubAuthApiRef, } from '@backstage/core'; -import { TextField, List, ListItem, Link, useTheme } from '@material-ui/core'; +import { TextField, List, ListItem, Link } from '@material-ui/core'; import ClusterTemplateCardList from '../ClusterTemplateCardList'; import ProfileCardList from '../ProfileCardList'; @@ -81,8 +80,6 @@ export const transformRunStatus = (x: Status[]) => { }; const ProfileCatalog: FC<{}> = () => { - const backstageTheme = useTheme(); - // TODO: get data from REST API const [clusterTemplates] = React.useState([ { @@ -261,11 +258,7 @@ const ProfileCatalog: FC<{}> = () => { ]; return ( - +
{ - const backstageTheme = useTheme(); const graphQlBrowseApi = useApi(graphQlBrowseApiRef); const endpoints = useAsync(() => graphQlBrowseApi.getEndpoints()); @@ -60,11 +58,7 @@ export const GraphiQLPage = () => { } return ( - +
diff --git a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx index 451383df58..a3e82e79a4 100644 --- a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx +++ b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx @@ -14,9 +14,8 @@ * limitations under the License. */ -import { BackstageTheme } from '@backstage/theme'; import React, { ReactElement, useEffect, useState } from 'react'; -import { Grid, TabProps, useTheme } from '@material-ui/core'; +import { Grid, TabProps } from '@material-ui/core'; import { Config } from '@backstage/config'; import { CardTab, @@ -102,7 +101,6 @@ const groupResponses = (fetchResponse: FetchResponse[]) => { type KubernetesContentProps = { entity: Entity; children?: React.ReactNode }; export const KubernetesContent = ({ entity }: KubernetesContentProps) => { - const backstageTheme = useTheme(); const kubernetesApi = useApi(kubernetesApiRef); const [kubernetesObjects, setKubernetesObjects] = useState< @@ -149,11 +147,7 @@ export const KubernetesContent = ({ entity }: KubernetesContentProps) => { kubernetesObjects?.items.filter(r => r.errors.length > 0) ?? []; return ( - + {kubernetesObjects === undefined && error === undefined && ( diff --git a/plugins/lighthouse/src/components/AuditList/index.tsx b/plugins/lighthouse/src/components/AuditList/index.tsx index cae1761ecb..117a40a901 100644 --- a/plugins/lighthouse/src/components/AuditList/index.tsx +++ b/plugins/lighthouse/src/components/AuditList/index.tsx @@ -13,11 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { BackstageTheme } from '@backstage/theme'; import React, { useState, useMemo, FC, ReactNode } from 'react'; import { useLocalStorage, useAsync } from 'react-use'; import { useNavigate } from 'react-router-dom'; -import { Grid, Button, useTheme } from '@material-ui/core'; +import { Grid, Button } from '@material-ui/core'; import Alert from '@material-ui/lab/Alert'; import Pagination from '@material-ui/lab/Pagination'; import { @@ -41,7 +40,6 @@ import { createAuditRouteRef } from '../../plugin'; export const LIMIT = 10; const AuditList: FC<{}> = () => { - const backstageTheme = useTheme(); const [dismissedStored] = useLocalStorage(LIGHTHOUSE_INTRO_LOCAL_STORAGE); const [dismissed, setDismissed] = useState(dismissedStored); @@ -95,11 +93,7 @@ const AuditList: FC<{}> = () => { } return ( - +
= () => { }; const ConnectedAuditView = () => { - const backstageTheme = useTheme(); return ( - +
= () => { }; const CreateAudit = () => { - const backstageTheme = useTheme(); return ( - +
= () => { - const backstageTheme = useTheme(); return ( - +
diff --git a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx index 722e2e1964..cd3eb1adbb 100644 --- a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx +++ b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx @@ -14,9 +14,8 @@ * limitations under the License. */ -import { BackstageTheme } from '@backstage/theme'; import React, { useState } from 'react'; -import { Grid, makeStyles, useTheme } from '@material-ui/core'; +import { Grid, makeStyles } from '@material-ui/core'; import { InfoCard, Page, @@ -61,7 +60,6 @@ export const RegisterComponentPage = ({ }: { catalogRouteRef: RouteRef; }) => { - const backstageTheme = useTheme(); const classes = useStyles(); const catalogApi = useApi(catalogApiRef); const [formState, setFormState] = useState>( @@ -114,11 +112,7 @@ export const RegisterComponentPage = ({ }; return ( - +
diff --git a/plugins/rollbar/src/components/RollbarHome/RollbarHome.tsx b/plugins/rollbar/src/components/RollbarHome/RollbarHome.tsx index f219be9615..fe99e70575 100644 --- a/plugins/rollbar/src/components/RollbarHome/RollbarHome.tsx +++ b/plugins/rollbar/src/components/RollbarHome/RollbarHome.tsx @@ -14,23 +14,16 @@ * limitations under the License. */ -import { BackstageTheme } from '@backstage/theme'; -import { useTheme } from '@material-ui/core'; import React from 'react'; import { Content, Header, Page } from '@backstage/core'; import { RollbarProjectTable } from '../RollbarProjectTable/RollbarProjectTable'; import { useRollbarEntities } from '../../hooks/useRollbarEntities'; export const RollbarHome = () => { - const backstageTheme = useTheme(); const { entities, loading, error } = useRollbarEntities(); return ( - +
{ - const backstageTheme = useTheme(); const { entity } = useCatalogEntity(); return ( - +
diff --git a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx index bd70fd9565..d0cb25f254 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx @@ -14,7 +14,6 @@ * limitations under the License. */ -import { BackstageTheme } from '@backstage/theme'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { Content, @@ -29,7 +28,7 @@ import { WarningPanel, } from '@backstage/core'; import { catalogApiRef } from '@backstage/plugin-catalog'; -import { Button, Grid, Link, Typography, useTheme } from '@material-ui/core'; +import { Button, Grid, Link, Typography } from '@material-ui/core'; import React, { useEffect } from 'react'; import { Link as RouterLink } from 'react-router-dom'; import useStaleWhileRevalidate from 'swr'; @@ -49,7 +48,6 @@ const getTemplateCardProps = ( }; export const ScaffolderPage = () => { - const backstageTheme = useTheme(); const catalogApi = useApi(catalogApiRef); const errorApi = useApi(errorApiRef); @@ -67,7 +65,7 @@ export const ScaffolderPage = () => { }, [error, errorApi]); return ( - +
{ - const backstageTheme = useTheme(); const errorApi = useApi(errorApiRef); const catalogApi = useApi(catalogApiRef); const scaffolderApi = useApi(scaffolderApiRef); @@ -146,7 +144,7 @@ export const TemplatePage = () => { } return ( - +
= () => { - const backstageTheme = useTheme(); const [statsFor, setStatsFor] = useState<'12h' | '24h'>('12h'); const toggleStatsFor = () => setStatsFor(statsFor === '12h' ? '12h' : '24h'); const sentryProjectId = 'sample-sentry-project-id'; return ( - +
diff --git a/plugins/tech-radar/src/components/RadarPage.tsx b/plugins/tech-radar/src/components/RadarPage.tsx index d23061b094..d5c4355b0e 100644 --- a/plugins/tech-radar/src/components/RadarPage.tsx +++ b/plugins/tech-radar/src/components/RadarPage.tsx @@ -14,9 +14,8 @@ * limitations under the License. */ -import { BackstageTheme } from '@backstage/theme'; import React from 'react'; -import { Grid, useTheme } from '@material-ui/core'; +import { Grid } from '@material-ui/core'; import { Content, ContentHeader, @@ -40,13 +39,8 @@ export const RadarPage = ({ pageTitle, ...props }: TechRadarPageProps): JSX.Element => { - const backstageTheme = useTheme(); return ( - +
diff --git a/plugins/techdocs/src/reader/components/TechDocsHome.tsx b/plugins/techdocs/src/reader/components/TechDocsHome.tsx index 016f6df4f0..0408a540ca 100644 --- a/plugins/techdocs/src/reader/components/TechDocsHome.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsHome.tsx @@ -14,11 +14,10 @@ * limitations under the License. */ -import { BackstageTheme } from '@backstage/theme'; import React from 'react'; import { useAsync } from 'react-use'; import { useNavigate, generatePath } from 'react-router-dom'; -import { Grid, useTheme } from '@material-ui/core'; +import { Grid } from '@material-ui/core'; import { ItemCard, Progress, @@ -31,7 +30,6 @@ import { catalogApiRef } from '@backstage/plugin-catalog'; import { rootDocsRouteRef } from '../../plugin'; export const TechDocsHome = () => { - const backstageTheme = useTheme(); const catalogApi = useApi(catalogApiRef); const navigate = useNavigate(); @@ -44,11 +42,7 @@ export const TechDocsHome = () => { if (loading) { return ( - +
{ if (error) { return ( - +
{ } return ( - +
{ - const backstageTheme = useTheme(); const [documentReady, setDocumentReady] = useState(false); const { namespace, kind, name } = useParams(); @@ -48,11 +45,7 @@ export const TechDocsPage = () => { }; return ( - + { - const backstageTheme = useTheme(); const [activeTab, setActiveTab] = useState(0); const onTabChange = (index: number) => { setActiveTab(index); @@ -46,11 +43,7 @@ export const SettingsPage = ({ providerSettings }: Props) => { ]; return ( - +
{content[activeTab]} diff --git a/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx b/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx index 7c68ab96bf..7b94024948 100644 --- a/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx +++ b/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx @@ -14,7 +14,6 @@ * limitations under the License. */ -import { BackstageTheme } from '@backstage/theme'; import React from 'react'; import { Link as RouterLink } from 'react-router-dom'; import { @@ -24,7 +23,6 @@ import { ListItem, ListItemText, Link, - useTheme, } from '@material-ui/core'; import { Content, @@ -40,17 +38,12 @@ import { } from '@backstage/core'; const WelcomePage = () => { - const backstageTheme = useTheme(); const appTitle = useApi(configApiRef).getOptionalString('app.title') ?? 'Backstage'; const profile = { givenName: '' }; return ( - +
Date: Thu, 15 Oct 2020 09:37:13 +0530 Subject: [PATCH 067/139] chore: update example component --- .../tutorials/quickstart-app-plugin/ExampleComponent.md | 7 ++----- .../components/ExampleComponent/ExampleComponent.tsx.hbs | 3 +-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/contrib/docs/tutorials/quickstart-app-plugin/ExampleComponent.md b/contrib/docs/tutorials/quickstart-app-plugin/ExampleComponent.md index 83b18bf4c2..5d633544e3 100644 --- a/contrib/docs/tutorials/quickstart-app-plugin/ExampleComponent.md +++ b/contrib/docs/tutorials/quickstart-app-plugin/ExampleComponent.md @@ -3,14 +3,12 @@ ExampleComponent.tsx reference ```tsx -import { BackstageTheme } from '@backstage/theme'; import React, { FC } from 'react'; -import { Typography, Grid, useTheme } from '@material-ui/core'; +import { Typography, Grid } from '@material-ui/core'; import { InfoCard, Header, Page, - pageTheme, Content, ContentHeader, HeaderLabel, @@ -21,13 +19,12 @@ import { useApi } from '@backstage/core-api'; import ExampleFetchComponent from '../ExampleFetchComponent'; const ExampleComponent: FC<{}> = () => { - const backstageTheme = useTheme(); const identityApi = useApi(identityApiRef); const userId = identityApi.getUserId(); const profile = identityApi.getProfile(); return ( - +
= () => { return ( - +
From c861f4415fec4c9081bd1f6f5e91aa3642918261 Mon Sep 17 00:00:00 2001 From: "althaf.hameez" Date: Thu, 15 Oct 2020 14:24:21 +0800 Subject: [PATCH 068/139] Use organization name as the prefix for Service Catalog --- plugins/catalog/src/components/CatalogPage/CatalogLayout.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/catalog/src/components/CatalogPage/CatalogLayout.tsx b/plugins/catalog/src/components/CatalogPage/CatalogLayout.tsx index 92f51ace06..9d658d7122 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogLayout.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogLayout.tsx @@ -15,6 +15,7 @@ */ import { + configApiRef, Header, HomepageTimer, identityApiRef, @@ -33,12 +34,13 @@ const CatalogLayout = ({ children }: Props) => { const greeting = getTimeBasedGreeting(); const profile = useApi(identityApiRef).getProfile(); const userId = useApi(identityApiRef).getUserId(); + const orgName = useApi(configApiRef).getOptionalString('organization.name'); return (
From 9c779c0b09e69602926f8f8aaa3f9700d31637f1 Mon Sep 17 00:00:00 2001 From: "althaf.hameez" Date: Thu, 15 Oct 2020 14:59:30 +0800 Subject: [PATCH 069/139] Fix e2e tests --- .../default-app/packages/app/cypress/integration/app.js | 2 +- packages/e2e-test/src/commands/run.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/create-app/templates/default-app/packages/app/cypress/integration/app.js b/packages/create-app/templates/default-app/packages/app/cypress/integration/app.js index dde26b7b39..2d63193fbf 100644 --- a/packages/create-app/templates/default-app/packages/app/cypress/integration/app.js +++ b/packages/create-app/templates/default-app/packages/app/cypress/integration/app.js @@ -1,6 +1,6 @@ describe('App', () => { it('should render the catalog', () => { cy.visit('/'); - cy.contains('Backstage Service Catalog'); + cy.contains('Spotify Service Catalog'); }); }); diff --git a/packages/e2e-test/src/commands/run.ts b/packages/e2e-test/src/commands/run.ts index cfe0ad9685..7775fb29b2 100644 --- a/packages/e2e-test/src/commands/run.ts +++ b/packages/e2e-test/src/commands/run.ts @@ -301,7 +301,7 @@ async function testAppServe(pluginName: string, appDir: string) { try { const browser = new Browser(); - await waitForPageWithText(browser, '/', 'Backstage Service Catalog'); + await waitForPageWithText(browser, '/', 'Spotify Service Catalog'); await waitForPageWithText( browser, `/${pluginName}`, From bd4029b7752cbd64875f680745457200e3955263 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20=C3=85lund?= Date: Thu, 15 Oct 2020 10:02:36 +0200 Subject: [PATCH 070/139] Add Lighthouse annotation to YAML (#2909) --- catalog-info.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/catalog-info.yaml b/catalog-info.yaml index 937a55bfca..405c3f31b4 100644 --- a/catalog-info.yaml +++ b/catalog-info.yaml @@ -7,6 +7,7 @@ metadata: annotations: github.com/project-slug: spotify/backstage backstage.io/techdocs-ref: github:https://github.com/spotify/backstage.git + lighthouse.com/website-url: https://backstage.io spec: type: library owner: Spotify From fe4926cb7591ab26e0fe7e930b0d421a7138bab1 Mon Sep 17 00:00:00 2001 From: Marvin9 Date: Thu, 15 Oct 2020 13:52:18 +0530 Subject: [PATCH 071/139] chore: remove explicit return --- .../ExampleComponent/ExampleComponent.tsx.hbs | 51 ++++--- packages/core/src/layout/Header/Header.tsx | 134 +++++++++--------- .../ApiExplorerPage/ApiExplorerLayout.tsx | 22 ++- .../NewProjectPage/NewProjectPage.tsx | 32 ++--- .../ProjectDetailsPage/ProjectDetailsPage.tsx | 28 ++-- .../ProjectListPage/ProjectListPage.tsx | 34 +++-- .../src/components/AuditView/index.tsx | 28 ++-- .../src/components/CreateAudit/index.tsx | 27 ++-- .../NewRelicComponent/NewRelicComponent.tsx | 39 +++-- .../tech-radar/src/components/RadarPage.tsx | 45 +++--- 10 files changed, 207 insertions(+), 233 deletions(-) diff --git a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs index 616f2cd2fc..e08f1650d5 100644 --- a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs +++ b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.tsx.hbs @@ -11,31 +11,30 @@ import { } from '@backstage/core'; import ExampleFetchComponent from '../ExampleFetchComponent'; -const ExampleComponent: FC<{}> = () => { - return ( - -
- - -
- - - A description of your plugin goes here. - - - - - - All content should be wrapped in a card like this. - - - - - - +const ExampleComponent: FC<{}> = () => ( + +
+ + +
+ + + A description of your plugin goes here. + + + + + + All content should be wrapped in a card like this. + + - -
- ); -} + + + +
+
+
+); + export default ExampleComponent; diff --git a/packages/core/src/layout/Header/Header.tsx b/packages/core/src/layout/Header/Header.tsx index 25afb5c9d1..ec4a3db6e2 100644 --- a/packages/core/src/layout/Header/Header.tsx +++ b/packages/core/src/layout/Header/Header.tsx @@ -22,76 +22,73 @@ import { Tooltip, makeStyles, Breadcrumbs, - useTheme, } from '@material-ui/core'; import ChevronRightIcon from '@material-ui/icons/ChevronRight'; import { BackstageTheme } from '@backstage/theme'; -const useStyles = makeStyles( - theme => ({ - header: { - gridArea: 'pageHeader', - padding: theme.spacing(3), - minHeight: 118, - width: '100%', - 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', - backgroundImage: props => props.backgroundImage, - backgroundPosition: 'center', - backgroundSize: 'cover', - }, - leftItemsBox: { - flex: '1 1 auto', - }, - rightItemsBox: { - flex: '0 1 auto', - display: 'flex', - flexDirection: 'row', - flexWrap: 'wrap', - alignItems: 'center', - marginRight: theme.spacing(1), - }, - 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: 11, - opacity: 0.8, - marginBottom: theme.spacing(1), - color: theme.palette.bursts.fontColor, - }, - breadcrumb: { - fontSize: 'calc(15px + 1 * ((100vw - 320px) / 680))', - color: theme.palette.bursts.fontColor, - }, - breadcrumbType: { - fontSize: 'inherit', - opacity: 0.7, - marginRight: -theme.spacing(0.3), - marginBottom: theme.spacing(0.3), - }, - breadcrumbTitle: { - fontSize: 'inherit', - marginLeft: -theme.spacing(0.3), - marginBottom: theme.spacing(0.3), - }, - }), -); +const useStyles = makeStyles(theme => ({ + header: { + gridArea: 'pageHeader', + padding: theme.spacing(3), + minHeight: 118, + width: '100%', + 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', + backgroundImage: theme.page.backgroundImage, + backgroundPosition: 'center', + backgroundSize: 'cover', + }, + leftItemsBox: { + flex: '1 1 auto', + }, + rightItemsBox: { + flex: '0 1 auto', + display: 'flex', + flexDirection: 'row', + flexWrap: 'wrap', + alignItems: 'center', + marginRight: theme.spacing(1), + }, + 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: 11, + opacity: 0.8, + marginBottom: theme.spacing(1), + color: theme.palette.bursts.fontColor, + }, + breadcrumb: { + fontSize: 'calc(15px + 1 * ((100vw - 320px) / 680))', + color: theme.palette.bursts.fontColor, + }, + breadcrumbType: { + fontSize: 'inherit', + opacity: 0.7, + marginRight: -theme.spacing(0.3), + marginBottom: theme.spacing(0.3), + }, + breadcrumbTitle: { + fontSize: 'inherit', + marginLeft: -theme.spacing(0.3), + marginBottom: theme.spacing(0.3), + }, +})); type HeaderStyles = ReturnType; @@ -200,10 +197,7 @@ export const Header: FC = ({ type, typeLink, }) => { - const backstageTheme = useTheme(); - const classes = useStyles({ - backgroundImage: backstageTheme.page.backgroundImage, - }); + const classes = useStyles(); const documentTitle = pageTitleOverride || title; const pageTitle = title || pageTitleOverride; const titleTemplate = `${documentTitle} | %s | Backstage`; diff --git a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerLayout.tsx b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerLayout.tsx index b534489487..0720ecf45b 100644 --- a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerLayout.tsx +++ b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerLayout.tsx @@ -21,15 +21,13 @@ type Props = { children?: React.ReactNode; }; -export const ApiExplorerLayout = ({ children }: Props) => { - return ( - -
- {children} - - ); -}; +export const ApiExplorerLayout = ({ children }: Props) => ( + +
+ {children} + +); diff --git a/plugins/gcp-projects/src/components/NewProjectPage/NewProjectPage.tsx b/plugins/gcp-projects/src/components/NewProjectPage/NewProjectPage.tsx index 94cf6ce920..462aa17099 100644 --- a/plugins/gcp-projects/src/components/NewProjectPage/NewProjectPage.tsx +++ b/plugins/gcp-projects/src/components/NewProjectPage/NewProjectPage.tsx @@ -110,20 +110,18 @@ const labels = ( ); -export const NewProjectPage = () => { - return ( - -
- {labels} -
- - - - This plugin allows you to view and interact with your gcp projects. - - - - -
- ); -}; +export const NewProjectPage = () => ( + +
+ {labels} +
+ + + + This plugin allows you to view and interact with your gcp projects. + + + + +
+); diff --git a/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx b/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx index 7e44618441..0c6350b849 100644 --- a/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx +++ b/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx @@ -145,18 +145,16 @@ const labels = ( ); -export const ProjectDetailsPage = () => { - return ( - -
- {labels} -
- - - Support Button - - - -
- ); -}; +export const ProjectDetailsPage = () => ( + +
+ {labels} +
+ + + Support Button + + + +
+); diff --git a/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx b/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx index b2148bcfc8..b911040535 100644 --- a/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx +++ b/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx @@ -132,21 +132,19 @@ const PageContents = () => { ); }; -export const ProjectListPage = () => { - return ( - -
- {labels} -
- - - - All your software catalog entities - - - -
- ); -}; +export const ProjectListPage = () => ( + +
+ {labels} +
+ + + + All your software catalog entities + + + +
+); diff --git a/plugins/lighthouse/src/components/AuditView/index.tsx b/plugins/lighthouse/src/components/AuditView/index.tsx index 4813be8bff..ac38a329f1 100644 --- a/plugins/lighthouse/src/components/AuditView/index.tsx +++ b/plugins/lighthouse/src/components/AuditView/index.tsx @@ -191,20 +191,16 @@ export const AuditViewContent: FC<{}> = () => { ); }; -const ConnectedAuditView = () => { - return ( - -
- - -
- - - -
- ); -}; +const ConnectedAuditView = () => ( + +
+ + +
+ + + +
+); + export default ConnectedAuditView; diff --git a/plugins/lighthouse/src/components/CreateAudit/index.tsx b/plugins/lighthouse/src/components/CreateAudit/index.tsx index ac4d2a2e3c..372992a718 100644 --- a/plugins/lighthouse/src/components/CreateAudit/index.tsx +++ b/plugins/lighthouse/src/components/CreateAudit/index.tsx @@ -168,21 +168,16 @@ export const CreateAuditContent: FC<{}> = () => { ); }; -const CreateAudit = () => { - return ( - -
- - -
- - - -
- ); -}; +const CreateAudit = () => ( + +
+ + +
+ + + +
+); export default CreateAudit; diff --git a/plugins/newrelic/src/components/NewRelicComponent/NewRelicComponent.tsx b/plugins/newrelic/src/components/NewRelicComponent/NewRelicComponent.tsx index 5dea8b7ee6..81e28a8c80 100644 --- a/plugins/newrelic/src/components/NewRelicComponent/NewRelicComponent.tsx +++ b/plugins/newrelic/src/components/NewRelicComponent/NewRelicComponent.tsx @@ -26,25 +26,24 @@ import { } from '@backstage/core'; import NewRelicFetchComponent from '../NewRelicFetchComponent'; -const NewRelicComponent: FC<{}> = () => { - return ( - -
- -
- - - - New Relic Application Performance Monitoring - - - - - - +const NewRelicComponent: FC<{}> = () => ( + +
+ +
+ + + + New Relic Application Performance Monitoring + + + + + - -
- ); -}; +
+
+
+); + export default NewRelicComponent; diff --git a/plugins/tech-radar/src/components/RadarPage.tsx b/plugins/tech-radar/src/components/RadarPage.tsx index d5c4355b0e..84cfef3213 100644 --- a/plugins/tech-radar/src/components/RadarPage.tsx +++ b/plugins/tech-radar/src/components/RadarPage.tsx @@ -38,30 +38,29 @@ export const RadarPage = ({ subtitle, pageTitle, ...props -}: TechRadarPageProps): JSX.Element => { - return ( - -
- - -
- - - - This is used for visualizing the official guidelines of different - areas of software development such as languages, frameworks, - infrastructure and processes. - - - - - - +}: TechRadarPageProps): JSX.Element => ( + +
+ + +
+ + + + This is used for visualizing the official guidelines of different + areas of software development such as languages, frameworks, + infrastructure and processes. + + + + + - -
- ); -}; +
+
+
+); + RadarPage.defaultProps = { title: 'Tech Radar', subtitle: 'Pick the recommended technologies for your projects', From 1f4f856cd4ae8d62d45d374b82a04f18fccf8d34 Mon Sep 17 00:00:00 2001 From: "althaf.hameez" Date: Thu, 15 Oct 2020 16:25:15 +0800 Subject: [PATCH 072/139] Revert --- .../default-app/packages/app/cypress/integration/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/create-app/templates/default-app/packages/app/cypress/integration/app.js b/packages/create-app/templates/default-app/packages/app/cypress/integration/app.js index 2d63193fbf..dde26b7b39 100644 --- a/packages/create-app/templates/default-app/packages/app/cypress/integration/app.js +++ b/packages/create-app/templates/default-app/packages/app/cypress/integration/app.js @@ -1,6 +1,6 @@ describe('App', () => { it('should render the catalog', () => { cy.visit('/'); - cy.contains('Spotify Service Catalog'); + cy.contains('Backstage Service Catalog'); }); }); From 53104074ee594c02c083c10f3b17c38759157c35 Mon Sep 17 00:00:00 2001 From: Marvin9 Date: Thu, 15 Oct 2020 14:04:20 +0530 Subject: [PATCH 073/139] feat: allow passing pageTheme in createThemeOptions --- packages/theme/src/baseTheme.ts | 3 +-- packages/theme/src/themes.ts | 3 +++ packages/theme/src/types.ts | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/theme/src/baseTheme.ts b/packages/theme/src/baseTheme.ts index 1f0e01586e..640476c487 100644 --- a/packages/theme/src/baseTheme.ts +++ b/packages/theme/src/baseTheme.ts @@ -24,8 +24,6 @@ import { SimpleThemeOptions, } from './types'; -import { pageTheme } from './pageTheme'; - const DEFAULT_FONT_FAMILY = '"Helvetica Neue", Helvetica, Roboto, Arial, sans-serif'; @@ -36,6 +34,7 @@ export function createThemeOptions( palette, fontFamily = DEFAULT_FONT_FAMILY, defaultPageTheme, + pageTheme, } = options; if (!pageTheme[defaultPageTheme]) { diff --git a/packages/theme/src/themes.ts b/packages/theme/src/themes.ts index 3838ed01cc..2c06f387b8 100644 --- a/packages/theme/src/themes.ts +++ b/packages/theme/src/themes.ts @@ -15,6 +15,7 @@ */ import { createTheme } from './baseTheme'; +import { pageTheme } from './pageTheme'; import { yellow } from '@material-ui/core/colors'; export const lightTheme = createTheme({ @@ -74,6 +75,7 @@ export const lightTheme = createTheme({ }, }, defaultPageTheme: 'home', + pageTheme, }); export const darkTheme = createTheme({ @@ -133,4 +135,5 @@ export const darkTheme = createTheme({ }, }, defaultPageTheme: 'home', + pageTheme, }); diff --git a/packages/theme/src/types.ts b/packages/theme/src/types.ts index e551208f3f..696ccf586f 100644 --- a/packages/theme/src/types.ts +++ b/packages/theme/src/types.ts @@ -94,6 +94,7 @@ export interface BackstageThemeOptions extends ThemeOptions { export type SimpleThemeOptions = { palette: BackstagePaletteOptions; defaultPageTheme: string; + pageTheme: Record; fontFamily?: string; }; From fecd2d4001dfdf798db911d7cb940e542fe63cd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 14 Oct 2020 11:23:49 +0200 Subject: [PATCH 074/139] fix(catalog-backend): move batch write to EntitiesCatalog where it belongs --- .../catalog/DatabaseEntitiesCatalog.test.ts | 43 +++- .../src/catalog/DatabaseEntitiesCatalog.ts | 167 +++++++++++++- plugins/catalog-backend/src/catalog/types.ts | 11 + .../ingestion/HigherOrderOperations.test.ts | 21 +- .../src/ingestion/HigherOrderOperations.ts | 204 +++--------------- .../src/service/CatalogBuilder.ts | 2 +- .../src/service/router.test.ts | 1 + 7 files changed, 248 insertions(+), 201 deletions(-) diff --git a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts index 8ef685e6b0..3d1f887e1f 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts @@ -14,8 +14,9 @@ * limitations under the License. */ +import { getVoidLogger } from '@backstage/backend-common'; import type { Entity } from '@backstage/catalog-model'; -import type { Database } from '../database'; +import { Database, DatabaseManager } from '../database'; import { DatabaseEntitiesCatalog } from './DatabaseEntitiesCatalog'; describe('DatabaseEntitiesCatalog', () => { @@ -58,7 +59,7 @@ describe('DatabaseEntitiesCatalog', () => { db.entities.mockResolvedValue([]); db.addEntities.mockResolvedValue([{ entity }]); - const catalog = new DatabaseEntitiesCatalog(db); + const catalog = new DatabaseEntitiesCatalog(db, getVoidLogger()); const result = await catalog.addOrUpdateEntity(entity); expect(db.entityByName).toHaveBeenCalledTimes(1); @@ -97,7 +98,7 @@ describe('DatabaseEntitiesCatalog', () => { }); db.updateEntity.mockResolvedValue({ entity }); - const catalog = new DatabaseEntitiesCatalog(db); + const catalog = new DatabaseEntitiesCatalog(db, getVoidLogger()); const result = await catalog.addOrUpdateEntity(entity); expect(db.entities).toHaveBeenCalledTimes(0); @@ -149,7 +150,7 @@ describe('DatabaseEntitiesCatalog', () => { db.entityByName.mockResolvedValue({ entity: existing }); db.updateEntity.mockResolvedValue({ entity: existing }); - const catalog = new DatabaseEntitiesCatalog(db); + const catalog = new DatabaseEntitiesCatalog(db, getVoidLogger()); const result = await catalog.addOrUpdateEntity(added); expect(db.entityByName).toHaveBeenCalledTimes(1); @@ -180,4 +181,38 @@ describe('DatabaseEntitiesCatalog', () => { expect(result).toEqual(existing); }); }); + + describe('batchAddOrUpdateEntities', () => { + it('both adds and updates', async () => { + const catalog = new DatabaseEntitiesCatalog( + await DatabaseManager.createTestDatabase(), + getVoidLogger(), + ); + const entities: Entity[] = []; + for (let i = 0; i < 500; ++i) { + entities.push({ + apiVersion: 'a', + kind: 'k', + metadata: { name: `n${i}` }, + }); + } + + await catalog.batchAddOrUpdateEntities(entities); + const afterFirst = await catalog.entities(); + expect(afterFirst.length).toBe(500); + + entities[40].metadata.op = 'changed'; + entities.push({ + apiVersion: 'a', + kind: 'k', + metadata: { name: `n500`, op: 'added' }, + }); + + await catalog.batchAddOrUpdateEntities(entities); + const afterSecond = await catalog.entities(); + expect(afterSecond.length).toBe(501); + expect(afterSecond.find(e => e.metadata.op === 'changed')).toBeDefined(); + expect(afterSecond.find(e => e.metadata.op === 'added')).toBeDefined(); + }); + }); }); diff --git a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts index 8619355c91..0a213dd186 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts @@ -14,18 +14,46 @@ * limitations under the License. */ -import { NotFoundError } from '@backstage/backend-common'; +import { ConflictError, NotFoundError } from '@backstage/backend-common'; import type { Entity } from '@backstage/catalog-model'; import { + entityHasChanges, generateUpdatedEntity, getEntityName, LOCATION_ANNOTATION, + serializeEntityRef, } from '@backstage/catalog-model'; +import { chunk, groupBy } from 'lodash'; +import limiterFactory from 'p-limit'; +import { Logger } from 'winston'; import type { Database, DbEntityResponse, EntityFilters } from '../database'; +import { durationText } from '../util/timing'; import type { EntitiesCatalog } from './types'; +type BatchContext = { + kind: string; + namespace: string; + locationId?: string; +}; + +// Some locations return tens or hundreds of thousands of entities. To make +// those payloads more manageable, we break work apart in batches of this +// many entities and write them to storage per batch. +const BATCH_SIZE = 100; + +// When writing large batches, there's an increasing chance of contention in +// the form of conflicts where we compete with other writes. Each batch gets +// this many attempts at being written before giving up. +const BATCH_ATTEMPTS = 3; + +// The number of batches that may be ongoing at the same time. +const BATCH_CONCURRENCY = 3; + export class DatabaseEntitiesCatalog implements EntitiesCatalog { - constructor(private readonly database: Database) {} + constructor( + private readonly database: Database, + private readonly logger: Logger, + ) {} async entities(filters?: EntityFilters): Promise { const items = await this.database.transaction(tx => @@ -102,4 +130,139 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog { return undefined; }); } + + /** + * Writes a number of entities efficiently to storage. + * + * @param entities Some entities + * @param locationId The location that they all belong to + */ + async batchAddOrUpdateEntities(entities: Entity[], locationId?: string) { + // Group the entities by unique kind+namespace combinations + const entitiesByKindAndNamespace = groupBy(entities, entity => { + const name = getEntityName(entity); + return `${name.kind}:${name.namespace}`.toLowerCase(); + }); + + const limiter = limiterFactory(BATCH_CONCURRENCY); + const tasks: Promise[] = []; + + for (const groupEntities of Object.values(entitiesByKindAndNamespace)) { + const { kind, namespace } = getEntityName(groupEntities[0]); + + // Go through the new entities in reasonable chunk sizes (sometimes, + // sources produce tens of thousands of entities, and those are too large + // batch sizes to reasonably send to the database) + for (const batch of chunk(groupEntities, BATCH_SIZE)) { + tasks.push( + limiter(async () => { + const first = serializeEntityRef(batch[0]); + const last = serializeEntityRef(batch[batch.length - 1]); + this.logger.debug( + `Considering batch ${first}-${last} (${batch.length} entries)`, + ); + + // Retry the batch write a few times to deal with contention + const context = { kind, namespace, locationId }; + for (let attempt = 1; attempt <= BATCH_ATTEMPTS; ++attempt) { + try { + const { toAdd, toUpdate } = await this.analyzeBatch( + batch, + context, + ); + if (toAdd.length) await this.batchAdd(toAdd, context); + if (toUpdate.length) await this.batchUpdate(toUpdate, context); + break; + } catch (e) { + if (e instanceof ConflictError && attempt < BATCH_ATTEMPTS) { + this.logger.warn( + `Failed to write batch at attempt ${attempt}/${BATCH_ATTEMPTS}, ${e}`, + ); + } else { + throw e; + } + } + } + }), + ); + } + } + + await Promise.all(tasks); + } + + // Given a batch of entities that were just read from a location, take them + // into consideration by comparing against the existing catalog entities and + // produce the list of entities to be added, and the list of entities to be + // updated + private async analyzeBatch( + newEntities: Entity[], + { kind, namespace }: BatchContext, + ): Promise<{ + toAdd: Entity[]; + toUpdate: Entity[]; + }> { + const markTimestamp = process.hrtime(); + + const names = newEntities.map(e => e.metadata.name); + const oldEntities = await this.entities({ + kind: kind, + 'metadata.namespace': namespace, + 'metadata.name': names, + }); + + const oldEntitiesByName = new Map( + oldEntities.map(e => [e.metadata.name, e]), + ); + + const toAdd: Entity[] = []; + const toUpdate: Entity[] = []; + + for (const newEntity of newEntities) { + const oldEntity = oldEntitiesByName.get(newEntity.metadata.name); + if (!oldEntity) { + toAdd.push(newEntity); + } else if (entityHasChanges(oldEntity, newEntity)) { + // TODO(freben): This currently uses addOrUpdateEntity under the hood, + // but should probably calculate the end result entity right here + // instead and call a dedicated batch update database method instead + toUpdate.push(newEntity); + } + } + + this.logger.debug( + `Found ${toAdd.length} entities to add, ${ + toUpdate.length + } entities to update in ${durationText(markTimestamp)}`, + ); + + return { toAdd, toUpdate }; + } + + // Efficiently adds the given entities to storage, under the assumption that + // they do not conflict with any existing entities + private async batchAdd(entities: Entity[], { locationId }: BatchContext) { + const markTimestamp = process.hrtime(); + + await this.addEntities(entities, locationId); + + this.logger.debug( + `Added ${entities.length} entities in ${durationText(markTimestamp)}`, + ); + } + + // Efficiently updates the given entities into storage, under the assumption + // that there already exist entities with the same names + private async batchUpdate(entities: Entity[], { locationId }: BatchContext) { + const markTimestamp = process.hrtime(); + + // TODO(freben): Still not batched + for (const entity of entities) { + await this.addOrUpdateEntity(entity, locationId); + } + + this.logger.debug( + `Updated ${entities.length} entities in ${durationText(markTimestamp)}`, + ); + } } diff --git a/plugins/catalog-backend/src/catalog/types.ts b/plugins/catalog-backend/src/catalog/types.ts index 810d51a6c1..ca73eddce9 100644 --- a/plugins/catalog-backend/src/catalog/types.ts +++ b/plugins/catalog-backend/src/catalog/types.ts @@ -26,6 +26,17 @@ export type EntitiesCatalog = { addOrUpdateEntity(entity: Entity, locationId?: string): Promise; addEntities(entities: Entity[], locationId?: string): Promise; removeEntityByUid(uid: string): Promise; + + /** + * Writes a number of entities efficiently to storage. + * + * @param entities Some entities + * @param locationId The location that they all belong to + */ + batchAddOrUpdateEntities( + entities: Entity[], + locationId?: string, + ): Promise; }; // diff --git a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.test.ts b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.test.ts index 909139d678..7fe164d05f 100644 --- a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.test.ts +++ b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.test.ts @@ -15,12 +15,7 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import { - Entity, - ENTITY_DEFAULT_NAMESPACE, - Location, - LocationSpec, -} from '@backstage/catalog-model'; +import { Entity, Location, LocationSpec } from '@backstage/catalog-model'; import { EntitiesCatalog, LocationsCatalog } from '../catalog'; import { LocationUpdateStatus } from '../catalog/types'; import { DatabaseLocationUpdateLogStatus } from '../database/types'; @@ -39,6 +34,7 @@ describe('HigherOrderOperations', () => { addOrUpdateEntity: jest.fn(), addEntities: jest.fn(), removeEntityByUid: jest.fn(), + batchAddOrUpdateEntities: jest.fn(), }; locationsCatalog = { addLocation: jest.fn(), @@ -190,8 +186,7 @@ describe('HigherOrderOperations', () => { entities: [{ entity: desc, location }], errors: [], }); - entitiesCatalog.entities.mockResolvedValue([]); - entitiesCatalog.addEntities.mockResolvedValue(undefined); + entitiesCatalog.batchAddOrUpdateEntities.mockResolvedValue(undefined); await expect( higherOrderOperation.refreshAllLocations(), @@ -203,14 +198,8 @@ describe('HigherOrderOperations', () => { type: 'some', target: 'thing', }); - expect(entitiesCatalog.entities).toHaveBeenCalledTimes(1); - expect(entitiesCatalog.entities).toHaveBeenNthCalledWith(1, { - kind: 'Component', - 'metadata.namespace': ENTITY_DEFAULT_NAMESPACE, - 'metadata.name': ['c1'], - }); - expect(entitiesCatalog.addEntities).toHaveBeenCalledTimes(1); - expect(entitiesCatalog.addEntities).toHaveBeenNthCalledWith( + expect(entitiesCatalog.batchAddOrUpdateEntities).toHaveBeenCalledTimes(1); + expect(entitiesCatalog.batchAddOrUpdateEntities).toHaveBeenNthCalledWith( 1, [expect.objectContaining({ metadata: { name: 'c1' } })], '123', diff --git a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts index fa73c58348..6df6d9b4b7 100644 --- a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts +++ b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts @@ -14,17 +14,8 @@ * limitations under the License. */ -import { ConflictError, InputError } from '@backstage/backend-common'; -import { - Entity, - entityHasChanges, - getEntityName, - Location, - LocationSpec, - serializeEntityRef, -} from '@backstage/catalog-model'; -import { chunk, groupBy } from 'lodash'; -import limiterFactory from 'p-limit'; +import { InputError } from '@backstage/backend-common'; +import { Entity, Location, LocationSpec } from '@backstage/catalog-model'; import { v4 as uuidv4 } from 'uuid'; import { Logger } from 'winston'; import { EntitiesCatalog, LocationsCatalog } from '../catalog'; @@ -35,25 +26,6 @@ import { LocationReader, } from './types'; -type BatchContext = { - kind: string; - namespace: string; - location: Location; -}; - -// Some locations return tens or hundreds of thousands of entities. To make -// those payloads more manageable, we break work apart in batches of this -// many entities and write them to storage per batch. -const BATCH_SIZE = 100; - -// When writing large batches, there's an increasing chance of contention in -// the form of conflicts where we compete with other writes. Each batch gets -// this many attempts at being written before giving up. -const BATCH_ATTEMPTS = 3; - -// The number of batches that may be ongoing at the same time. -const BATCH_CONCURRENCY = 3; - /** * Placeholder for operations that span several catalogs and/or stretches out * in time. @@ -192,10 +164,30 @@ export class HigherOrderOperations implements HigherOrderOperation { startTimestamp = process.hrtime(); - await this.batchAddOrUpdateEntities( - readerOutput.entities.map(e => e.entity), - location, - ); + try { + await this.entitiesCatalog.batchAddOrUpdateEntities( + readerOutput.entities.map(e => e.entity), + location.id, + ); + } catch (e) { + for (const entity of readerOutput.entities) { + await this.locationsCatalog.logUpdateFailure( + location.id, + e, + entity.entity.metadata.name, + ); + } + throw e; + } + + this.logger.info(`Posting update success markers`); + + for (const entity of readerOutput.entities) { + await this.locationsCatalog.logUpdateSuccess( + location.id, + entity.entity.metadata.name, + ); + } this.logger.info( `Wrote ${readerOutput.entities.length} entities from location ${ @@ -203,148 +195,4 @@ export class HigherOrderOperations implements HigherOrderOperation { }:${location.target} in ${durationText(startTimestamp)}`, ); } - - /** - * Writes a number of entities efficiently to storage. - * - * @param entities Some entities - * @param location The location that they all belong to - */ - async batchAddOrUpdateEntities(entities: Entity[], location: Location) { - // Group the entities by unique kind+namespace combinations - const entitiesByKindAndNamespace = groupBy(entities, entity => { - const name = getEntityName(entity); - return `${name.kind}:${name.namespace}`.toLowerCase(); - }); - - const limiter = limiterFactory(BATCH_CONCURRENCY); - const tasks: Promise[] = []; - - for (const groupEntities of Object.values(entitiesByKindAndNamespace)) { - const { kind, namespace } = getEntityName(groupEntities[0]); - - // Go through the new entities in reasonable chunk sizes (sometimes, - // sources produce tens of thousands of entities, and those are too large - // batch sizes to reasonably send to the database) - for (const batch of chunk(groupEntities, BATCH_SIZE)) { - tasks.push( - limiter(async () => { - const first = serializeEntityRef(batch[0]); - const last = serializeEntityRef(batch[batch.length - 1]); - this.logger.debug( - `Considering batch ${first}-${last} (${batch.length} entries)`, - ); - - // Retry the batch write a few times to deal with contention - const context = { kind, namespace, location }; - for (let attempt = 1; attempt <= BATCH_ATTEMPTS; ++attempt) { - try { - const { toAdd, toUpdate } = await this.analyzeBatch( - batch, - context, - ); - if (toAdd.length) await this.batchAdd(toAdd, context); - if (toUpdate.length) await this.batchUpdate(toUpdate, context); - break; - } catch (e) { - if (e instanceof ConflictError && attempt < BATCH_ATTEMPTS) { - this.logger.warn( - `Failed to write batch at attempt ${attempt}/${BATCH_ATTEMPTS}, ${e}`, - ); - } else { - throw e; - } - } - } - }), - ); - } - } - - await Promise.all(tasks); - } - - // Given a batch of entities that were just read from a location, take them - // into consideration by comparing against the existing catalog entities and - // produce the list of entities to be added, and the list of entities to be - // updated - private async analyzeBatch( - newEntities: Entity[], - { kind, namespace }: BatchContext, - ): Promise<{ - toAdd: Entity[]; - toUpdate: Entity[]; - }> { - const markTimestamp = process.hrtime(); - - const oldEntities = await this.entitiesCatalog.entities({ - kind: kind, - 'metadata.namespace': namespace, - 'metadata.name': newEntities.map(e => e.metadata.name), - }); - - const oldEntitiesByName = new Map( - oldEntities.map(e => [e.metadata.name, e]), - ); - - const toAdd: Entity[] = []; - const toUpdate: Entity[] = []; - - for (const newEntity of newEntities) { - const oldEntity = oldEntitiesByName.get(newEntity.metadata.name); - if (!oldEntity) { - toAdd.push(newEntity); - } else if (entityHasChanges(oldEntity, newEntity)) { - toUpdate.push(newEntity); - } - } - - this.logger.debug( - `Found ${toAdd.length} entities to add, ${ - toUpdate.length - } entities to update in ${durationText(markTimestamp)}`, - ); - - return { toAdd, toUpdate }; - } - - // Efficiently adds the given entities to storage, under the assumption that - // they do not conflict with any existing entities - private async batchAdd(entities: Entity[], { location }: BatchContext) { - const markTimestamp = process.hrtime(); - - await this.entitiesCatalog.addEntities(entities, location.id); - - // TODO(freben): Still not batched - for (const entity of entities) { - await this.locationsCatalog.logUpdateSuccess( - location.id, - entity.metadata.name, - ); - } - - this.logger.debug( - `Added ${entities.length} entities in ${durationText(markTimestamp)}`, - ); - } - - // Efficiently updates the given entities into storage, under the assumption - // that there already exist entities with the same names - private async batchUpdate(entities: Entity[], { location }: BatchContext) { - const markTimestamp = process.hrtime(); - - // TODO(freben): Still not batched - for (const entity of entities) { - await this.entitiesCatalog.addOrUpdateEntity(entity); - - await this.locationsCatalog.logUpdateSuccess( - location.id, - entity.metadata.name, - ); - } - - this.logger.debug( - `Updated ${entities.length} entities in ${durationText(markTimestamp)}`, - ); - } } diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index e02005b817..e1d76bc3cd 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -370,7 +370,7 @@ export class CatalogBuilder { { logger }, ); - const entitiesCatalog = new DatabaseEntitiesCatalog(db); + const entitiesCatalog = new DatabaseEntitiesCatalog(db, this.env.logger); const locationsCatalog = new DatabaseLocationsCatalog(db); const higherOrderOperation = new HigherOrderOperations( entitiesCatalog, diff --git a/plugins/catalog-backend/src/service/router.test.ts b/plugins/catalog-backend/src/service/router.test.ts index f7ab4878f8..414f8a93de 100644 --- a/plugins/catalog-backend/src/service/router.test.ts +++ b/plugins/catalog-backend/src/service/router.test.ts @@ -35,6 +35,7 @@ describe('createRouter', () => { addOrUpdateEntity: jest.fn(), addEntities: jest.fn(), removeEntityByUid: jest.fn(), + batchAddOrUpdateEntities: jest.fn(), }; locationsCatalog = { addLocation: jest.fn(), From 0d67d11eb349f810a0069588a2496a47a04a1718 Mon Sep 17 00:00:00 2001 From: "althaf.hameez" Date: Thu, 15 Oct 2020 16:54:36 +0800 Subject: [PATCH 075/139] fix --- .../default-app/packages/app/cypress/integration/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/create-app/templates/default-app/packages/app/cypress/integration/app.js b/packages/create-app/templates/default-app/packages/app/cypress/integration/app.js index dde26b7b39..2d63193fbf 100644 --- a/packages/create-app/templates/default-app/packages/app/cypress/integration/app.js +++ b/packages/create-app/templates/default-app/packages/app/cypress/integration/app.js @@ -1,6 +1,6 @@ describe('App', () => { it('should render the catalog', () => { cy.visit('/'); - cy.contains('Backstage Service Catalog'); + cy.contains('Spotify Service Catalog'); }); }); From a6c0d9803ecde293b82a0464375a7dad6669ee52 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Thu, 15 Oct 2020 11:08:16 +0200 Subject: [PATCH 076/139] Type with interface instead of class. --- plugins/newrelic/src/api/index.ts | 6 +++++- plugins/newrelic/src/plugin.ts | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/plugins/newrelic/src/api/index.ts b/plugins/newrelic/src/api/index.ts index 692cb19e6f..eb94acdbd1 100644 --- a/plugins/newrelic/src/api/index.ts +++ b/plugins/newrelic/src/api/index.ts @@ -68,7 +68,11 @@ type Options = { proxyPathBase?: string; }; -export class NewRelicApi { +export interface NewRelicApi { + getApplications(): Promise; +} + +export class NewRelicClient implements NewRelicApi { private readonly discoveryApi: DiscoveryApi; private readonly proxyPathBase: string; diff --git a/plugins/newrelic/src/plugin.ts b/plugins/newrelic/src/plugin.ts index f247158fbd..5f5ba88617 100644 --- a/plugins/newrelic/src/plugin.ts +++ b/plugins/newrelic/src/plugin.ts @@ -20,7 +20,7 @@ import { createRouteRef, discoveryApiRef, } from '@backstage/core'; -import { NewRelicApi, newRelicApiRef } from './api'; +import { NewRelicClient, newRelicApiRef } from './api'; import NewRelicComponent from './components/NewRelicComponent'; export const rootRouteRef = createRouteRef({ @@ -34,7 +34,7 @@ export const plugin = createPlugin({ createApiFactory({ api: newRelicApiRef, deps: { discoveryApi: discoveryApiRef }, - factory: ({ discoveryApi }) => new NewRelicApi({ discoveryApi }), + factory: ({ discoveryApi }) => new NewRelicClient({ discoveryApi }), }), ], register({ router }) { From 7ff2f1c0f0c63b8be5712a56569b83743b79f980 Mon Sep 17 00:00:00 2001 From: Martina Iglesias Date: Thu, 15 Oct 2020 11:30:39 +0200 Subject: [PATCH 077/139] [charts] Add certificate issuer (#2877) --- contrib/chart/backstage/README.md | 27 +++++++++++++++---- .../chart/backstage/templates/ingress.yaml | 26 +++++++++++++----- contrib/chart/backstage/templates/issuer.yaml | 19 +++++++++++++ contrib/chart/backstage/values.yaml | 4 +++ 4 files changed, 65 insertions(+), 11 deletions(-) create mode 100644 contrib/chart/backstage/templates/issuer.yaml diff --git a/contrib/chart/backstage/README.md b/contrib/chart/backstage/README.md index 4f247252b1..9d1c93765b 100644 --- a/contrib/chart/backstage/README.md +++ b/contrib/chart/backstage/README.md @@ -62,13 +62,30 @@ backstage-ingress * 123.1.2.3 80 17m > **NOTE**: this is not a production ready deployment. -## Caveats - -The current implementation does not generate certificates for the ingress which means the browser will alert that the -site is insecure and using self-signed certificates. - ## Customization +### Issue certificates + +These charts can install or reuse a `clusterIssuer` to generate certificates for the backstage `ingress`. To do that: + +1. [Install][install-cert-manager] or make sure [cert-manager][cert-manager] is installed in the cluster. +2. Enable the issuer in the charts. This will first check if there is a `letsencrypt` issuer already deployed in your + cluster and deploy one if it doesn't exist. + +To enable it you need to provide a valid email address in the chart's values: + +``` +issuer: + email: me@example.com + clusterIssuer: "letsencrypt-prod" +``` + +By default, the charts use `letsencrypt-staging` so in the above example we instruct helm to use the production issuer +instead. + +[cert-manager]: https://cert-manager.io/docs/ +[install-cert-manager]: https://cert-manager.io/docs/installation/kubernetes/#installing-with-helm + ### Custom PostgreSQL instance Configuring a connection to an existing PostgreSQL instance is possible through the chart's values. diff --git a/contrib/chart/backstage/templates/ingress.yaml b/contrib/chart/backstage/templates/ingress.yaml index 034862ef75..7231afda13 100644 --- a/contrib/chart/backstage/templates/ingress.yaml +++ b/contrib/chart/backstage/templates/ingress.yaml @@ -5,16 +5,25 @@ apiVersion: networking.k8s.io/v1beta1 kind: Ingress metadata: name: {{ include "backstage.fullname" . }}-ingress - {{- with .Values.ingress.annotations }} annotations: + {{- if .Values.issuer.email }} + cert-manager.io/cluster-issuer: {{ .Values.issuer.clusterIssuer }} + {{- end }} + kubernetes.io/ingress.class: nginx nginx.ingress.kubernetes.io/ssl-redirect: "false" nginx.ingress.kubernetes.io/configuration-snippet: | if ($scheme = https) { add_header Strict-Transport-Security "max-age=0;"; } - {{- toYaml . | nindent 4 }} - {{- end }} + {{- toYaml .Values.ingress.annotations | nindent 4 }} spec: + tls: + - secretName: {{ include "backstage.fullname" . }}-tls + hosts: + - {{ $frontendUrl.host }} + - {{ $backendUrl.host }} + - {{ $lighthouseUrl.host }} + rules: - host: {{ $frontendUrl.host }} http: @@ -56,17 +65,22 @@ apiVersion: networking.k8s.io/v1beta1 kind: Ingress metadata: name: {{ include "backstage.fullname" . }}-ingress-lighthouse - {{- with .Values.ingress.annotations }} annotations: + {{- if .Values.issuer.email }} + cert-manager.io/cluster-issuer: {{ .Values.issuer.clusterIssuer }} + {{- end }} nginx.ingress.kubernetes.io/rewrite-target: /$2 nginx.ingress.kubernetes.io/ssl-redirect: "false" nginx.ingress.kubernetes.io/configuration-snippet: | if ($scheme = https) { add_header Strict-Transport-Security "max-age=0;"; } - {{- toYaml . | nindent 4 }} - {{- end }} + {{- toYaml .Values.ingress.annotations | nindent 4 }} spec: + tls: + - secretName: {{ include "backstage.fullname" . }}-tls + hosts: + - {{ $lighthouseUrl.host }} rules: - host: {{ $frontendUrl.host }} http: diff --git a/contrib/chart/backstage/templates/issuer.yaml b/contrib/chart/backstage/templates/issuer.yaml new file mode 100644 index 0000000000..d129c8c701 --- /dev/null +++ b/contrib/chart/backstage/templates/issuer.yaml @@ -0,0 +1,19 @@ +{{- if (and (.Capabilities.APIVersions.Has "cert-manager.io/v1alpha2") .Values.issuer.email ) -}} +{{/* Only install issuer if it doesn't already exist in the cluster */}} +{{- if not ( lookup "cert-manager.io/v1alpha2" "ClusterIssuer" "" .Values.issuer.clusterIssuer ) }} +apiVersion: cert-manager.io/v1alpha2 +kind: ClusterIssuer +metadata: + name: {{ .Values.issuer.clusterIssuer }} +spec: + acme: + server: https://acme-v02.api.letsencrypt.org/directory + email: {{ required "expected a valid .Values.issuer.email to enable ClusterIssuer" .Values.issuer.email }} + privateKeySecretRef: + name: {{ required "expected .Values.issuer.cluster-issuer to not be empty (letsencrypt-prod | letsencrypt-staging)" .Values.issuer.clusterIssuer }} + solvers: + - http01: + ingress: + class: nginx +{{- end -}} +{{- end -}} diff --git a/contrib/chart/backstage/values.yaml b/contrib/chart/backstage/values.yaml index eec9c8fbe9..d51f1016d9 100644 --- a/contrib/chart/backstage/values.yaml +++ b/contrib/chart/backstage/values.yaml @@ -61,6 +61,10 @@ ingress: annotations: kubernetes.io/ingress.class: nginx +issuer: + email: + clusterIssuer: 'letsencrypt-staging' + global: postgresql: postgresqlUsername: backend-user From 94e8a38953397f3b69bef54880d6d20375c05e01 Mon Sep 17 00:00:00 2001 From: "althaf.hameez" Date: Thu, 15 Oct 2020 18:08:22 +0800 Subject: [PATCH 078/139] Acme corporation fix --- .../default-app/packages/app/cypress/integration/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/create-app/templates/default-app/packages/app/cypress/integration/app.js b/packages/create-app/templates/default-app/packages/app/cypress/integration/app.js index 2d63193fbf..227e218fe6 100644 --- a/packages/create-app/templates/default-app/packages/app/cypress/integration/app.js +++ b/packages/create-app/templates/default-app/packages/app/cypress/integration/app.js @@ -1,6 +1,6 @@ describe('App', () => { it('should render the catalog', () => { cy.visit('/'); - cy.contains('Spotify Service Catalog'); + cy.contains('Acme Corporation Service Catalog'); }); }); From 37015caeb9679197dbdb4aa887168ab8e9399fef Mon Sep 17 00:00:00 2001 From: "althaf.hameez" Date: Thu, 15 Oct 2020 18:25:48 +0800 Subject: [PATCH 079/139] Revert to backstage --- packages/e2e-test/src/commands/run.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/e2e-test/src/commands/run.ts b/packages/e2e-test/src/commands/run.ts index 7775fb29b2..cfe0ad9685 100644 --- a/packages/e2e-test/src/commands/run.ts +++ b/packages/e2e-test/src/commands/run.ts @@ -301,7 +301,7 @@ async function testAppServe(pluginName: string, appDir: string) { try { const browser = new Browser(); - await waitForPageWithText(browser, '/', 'Spotify Service Catalog'); + await waitForPageWithText(browser, '/', 'Backstage Service Catalog'); await waitForPageWithText( browser, `/${pluginName}`, From 1af3dbdb5ebaa8c283d4b2cb6eecd04aeb1949ba Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Thu, 15 Oct 2020 12:37:50 +0200 Subject: [PATCH 080/139] Provide more context. Handle more edge cases. --- plugins/newrelic/src/api/index.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/plugins/newrelic/src/api/index.ts b/plugins/newrelic/src/api/index.ts index eb94acdbd1..d76a87875c 100644 --- a/plugins/newrelic/src/api/index.ts +++ b/plugins/newrelic/src/api/index.ts @@ -84,10 +84,20 @@ export class NewRelicClient implements NewRelicApi { async getApplications(): Promise { const url = await this.getApiUrl('apm', 'applications.json'); const response = await fetch(url); - const responseJson = await response.json(); + let responseJson; + + try { + responseJson = await response.json(); + } catch (e) { + responseJson = { applications: [] }; + } if (response.status !== 200) { - throw new Error(responseJson?.error?.title || response.statusText); + throw new Error( + `Error communicating with New Relic: ${ + responseJson?.error?.title || response.statusText + }`, + ); } return responseJson; From a6c47b3d180e12dbf05d58c3097c025954165d95 Mon Sep 17 00:00:00 2001 From: "althaf.hameez" Date: Thu, 15 Oct 2020 18:42:26 +0800 Subject: [PATCH 081/139] Acme --- packages/e2e-test/src/commands/run.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/e2e-test/src/commands/run.ts b/packages/e2e-test/src/commands/run.ts index cfe0ad9685..e7e23f4825 100644 --- a/packages/e2e-test/src/commands/run.ts +++ b/packages/e2e-test/src/commands/run.ts @@ -301,7 +301,7 @@ async function testAppServe(pluginName: string, appDir: string) { try { const browser = new Browser(); - await waitForPageWithText(browser, '/', 'Backstage Service Catalog'); + await waitForPageWithText(browser, '/', 'Acme Corporation Service Catalog'); await waitForPageWithText( browser, `/${pluginName}`, From 4e5d8b28ac54b0bdba1dc443ffeeb57c911f02ff Mon Sep 17 00:00:00 2001 From: "althaf.hameez" Date: Thu, 15 Oct 2020 18:53:46 +0800 Subject: [PATCH 082/139] Fix prettier --- packages/e2e-test/src/commands/run.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/e2e-test/src/commands/run.ts b/packages/e2e-test/src/commands/run.ts index e7e23f4825..c05aed0b83 100644 --- a/packages/e2e-test/src/commands/run.ts +++ b/packages/e2e-test/src/commands/run.ts @@ -301,7 +301,11 @@ async function testAppServe(pluginName: string, appDir: string) { try { const browser = new Browser(); - await waitForPageWithText(browser, '/', 'Acme Corporation Service Catalog'); + await waitForPageWithText( + browser, + '/', + 'Acme Corporation Service Catalog', + ); await waitForPageWithText( browser, `/${pluginName}`, From 6943ead4c644958245974304306fec8918e7a995 Mon Sep 17 00:00:00 2001 From: Markus Heinemann Date: Thu, 15 Oct 2020 13:04:12 +0000 Subject: [PATCH 083/139] feat: add base url config property to github scaffholders --- app-config.yaml | 1 + packages/backend/src/plugins/scaffolder.ts | 6 +++++- .../default-app/packages/backend/src/plugins/scaffolder.ts | 3 ++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index 8ec43c6007..b527069c15 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -147,6 +147,7 @@ catalog: scaffolder: github: + baseUrl: https://github.com token: $env: GITHUB_TOKEN visibility: public # or 'internal' or 'private' diff --git a/packages/backend/src/plugins/scaffolder.ts b/packages/backend/src/plugins/scaffolder.ts index 90667bfb16..7ba90ed8b5 100644 --- a/packages/backend/src/plugins/scaffolder.ts +++ b/packages/backend/src/plugins/scaffolder.ts @@ -68,7 +68,11 @@ export default async function createPlugin({ ) as RepoVisibilityOptions; const githubToken = githubConfig.getString('token'); - const githubClient = new Octokit({ auth: githubToken }); + const githubBaseUrl = githubConfig.getString('baseUrl'); + const githubClient = new Octokit({ + auth: githubToken, + baseUrl: githubBaseUrl, + }); const githubPublisher = new GithubPublisher({ client: githubClient, token: githubToken, diff --git a/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts b/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts index ed16d766f1..9f59412036 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts @@ -48,7 +48,8 @@ export default async function createPlugin({ ) as RepoVisibilityOptions; const githubToken = githubConfig.getString('token'); - const githubClient = new Octokit({ auth: githubToken }); + const githubBaseUrl = githubConfig.getString('baseUrl'); + const githubClient = new Octokit({ auth: githubToken, baseUrl: githubBaseUrl }); const githubPublisher = new GithubPublisher({ client: githubClient, token: githubToken, From 1c8c437562e7945b6fe887e4e3a9f35301d30389 Mon Sep 17 00:00:00 2001 From: Markus Heinemann Date: Thu, 15 Oct 2020 13:18:14 +0000 Subject: [PATCH 084/139] Add changeset --- .changeset/selfish-bats-perform.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/selfish-bats-perform.md diff --git a/.changeset/selfish-bats-perform.md b/.changeset/selfish-bats-perform.md new file mode 100644 index 0000000000..e4d05c088c --- /dev/null +++ b/.changeset/selfish-bats-perform.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-scaffolder-backend': patch +--- + +The new `scaffolder.github.baseUrl` config property allows to specify a custom base url for GitHub enterprise instances From 1797d4924c198ec37dabb92edcc61c8f4b9cb886 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Thu, 15 Oct 2020 15:38:34 +0200 Subject: [PATCH 085/139] Update create-app to use expected techdocs api origin url. (#2911) --- packages/create-app/templates/default-app/app-config.yaml.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/create-app/templates/default-app/app-config.yaml.hbs b/packages/create-app/templates/default-app/app-config.yaml.hbs index 7175ea0e4f..347bbaa29c 100644 --- a/packages/create-app/templates/default-app/app-config.yaml.hbs +++ b/packages/create-app/templates/default-app/app-config.yaml.hbs @@ -42,7 +42,7 @@ proxy: techdocs: storageUrl: http://localhost:7000/api/techdocs/static/docs - requestUrl: http://localhost:7000/api/techdocs/docs + requestUrl: http://localhost:7000/api/techdocs generators: techdocs: 'docker' From a5cb46bac95b4e181e6a86a14608c7d47b83a29b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 15 Oct 2020 15:48:03 +0200 Subject: [PATCH 086/139] chore(catalog-backend): rename locationprocessor to catalogprocessor --- .changeset/raghu-is-great.md | 10 +++++ .../src/ingestion/LocationReaders.ts | 38 +++++++++---------- .../AnnotateLocationEntityProcessor.ts | 4 +- .../processors/AzureApiReaderProcessor.ts | 10 ++--- .../processors/BitbucketApiReaderProcessor.ts | 10 ++--- .../processors/CodeOwnersProcessor.ts | 9 ++--- .../processors/EntityPolicyProcessor.ts | 4 +- .../processors/FileReaderProcessor.ts | 6 +-- .../processors/GithubOrgReaderProcessor.ts | 6 +-- .../processors/GithubReaderProcessor.ts | 6 +-- .../processors/GitlabApiReaderProcessor.ts | 10 ++--- .../processors/GitlabReaderProcessor.ts | 6 +-- .../processors/LdapOrgReaderProcessor.ts | 6 +-- .../processors/LocationEntityProcessor.ts | 6 +-- .../processors/PlaceholderProcessor.ts | 4 +- .../processors/StaticLocationProcessor.ts | 6 +-- .../processors/UrlReaderProcessor.test.ts | 24 ++++++------ .../processors/UrlReaderProcessor.ts | 8 ++-- .../processors/YamlProcessor.test.ts | 18 ++++----- .../src/ingestion/processors/YamlProcessor.ts | 6 +-- .../src/ingestion/processors/results.ts | 14 +++---- .../src/ingestion/processors/types.ts | 32 ++++++++-------- .../src/service/CatalogBuilder.test.ts | 8 ++-- .../src/service/CatalogBuilder.ts | 36 +++++++++--------- 24 files changed, 147 insertions(+), 140 deletions(-) create mode 100644 .changeset/raghu-is-great.md diff --git a/.changeset/raghu-is-great.md b/.changeset/raghu-is-great.md new file mode 100644 index 0000000000..805e49f3e8 --- /dev/null +++ b/.changeset/raghu-is-great.md @@ -0,0 +1,10 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +Renamed the `LocationProcessor` class to `CatalogProcessor`. + +Likewise, renamed `LocationProcessorResult`, `LocationProcessorLocationResult`, +`LocationProcessorDataResult`, `LocationProcessorEntityResult`, +`LocationProcessorErrorResult`, and `LocationProcessorEmit` to their `Catalog*` +counterparts. diff --git a/plugins/catalog-backend/src/ingestion/LocationReaders.ts b/plugins/catalog-backend/src/ingestion/LocationReaders.ts index 0f6ad18f97..c36b3d2b05 100644 --- a/plugins/catalog-backend/src/ingestion/LocationReaders.ts +++ b/plugins/catalog-backend/src/ingestion/LocationReaders.ts @@ -25,13 +25,13 @@ import { Logger } from 'winston'; import { CatalogRulesEnforcer } from './CatalogRules'; import * as result from './processors/results'; import { - LocationProcessor, - LocationProcessorDataResult, - LocationProcessorEmit, - LocationProcessorEntityResult, - LocationProcessorErrorResult, - LocationProcessorLocationResult, - LocationProcessorResult, + CatalogProcessor, + CatalogProcessorDataResult, + CatalogProcessorEmit, + CatalogProcessorEntityResult, + CatalogProcessorErrorResult, + CatalogProcessorLocationResult, + CatalogProcessorResult, } from './processors/types'; import { LocationReader, ReadLocationResult } from './types'; @@ -42,7 +42,7 @@ type Options = { reader: UrlReader; logger: Logger; config: Config; - processors: LocationProcessor[]; + processors: CatalogProcessor[]; rulesEnforcer: CatalogRulesEnforcer; }; @@ -60,11 +60,11 @@ export class LocationReaders implements LocationReader { const { rulesEnforcer, logger } = this.options; const output: ReadLocationResult = { entities: [], errors: [] }; - let items: LocationProcessorResult[] = [result.location(location, false)]; + let items: CatalogProcessorResult[] = [result.location(location, false)]; for (let depth = 0; depth < MAX_DEPTH; ++depth) { - const newItems: LocationProcessorResult[] = []; - const emit: LocationProcessorEmit = i => newItems.push(i); + const newItems: CatalogProcessorResult[] = []; + const emit: CatalogProcessorEmit = i => newItems.push(i); for (const item of items) { if (item.type === 'location') { @@ -109,8 +109,8 @@ export class LocationReaders implements LocationReader { } private async handleLocation( - item: LocationProcessorLocationResult, - emit: LocationProcessorEmit, + item: CatalogProcessorLocationResult, + emit: CatalogProcessorEmit, ) { const { processors, logger } = this.options; @@ -136,8 +136,8 @@ export class LocationReaders implements LocationReader { } private async handleData( - item: LocationProcessorDataResult, - emit: LocationProcessorEmit, + item: CatalogProcessorDataResult, + emit: CatalogProcessorEmit, ) { const { processors, logger } = this.options; @@ -160,8 +160,8 @@ export class LocationReaders implements LocationReader { } private async handleEntity( - item: LocationProcessorEntityResult, - emit: LocationProcessorEmit, + item: CatalogProcessorEntityResult, + emit: CatalogProcessorEmit, ): Promise { const { processors, logger } = this.options; @@ -189,8 +189,8 @@ export class LocationReaders implements LocationReader { } private async handleError( - item: LocationProcessorErrorResult, - emit: LocationProcessorEmit, + item: CatalogProcessorErrorResult, + emit: CatalogProcessorEmit, ) { const { processors, logger } = this.options; diff --git a/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.ts index ec7e59281e..fea70b6c5e 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.ts @@ -16,9 +16,9 @@ import { Entity, LocationSpec } from '@backstage/catalog-model'; import lodash from 'lodash'; -import { LocationProcessor } from './types'; +import { CatalogProcessor } from './types'; -export class AnnotateLocationEntityProcessor implements LocationProcessor { +export class AnnotateLocationEntityProcessor implements CatalogProcessor { async processEntity(entity: Entity, location: LocationSpec): Promise { return lodash.merge( { diff --git a/plugins/catalog-backend/src/ingestion/processors/AzureApiReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/AzureApiReaderProcessor.ts index 2a15c18c84..b6e65a7d7a 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AzureApiReaderProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/AzureApiReaderProcessor.ts @@ -15,17 +15,17 @@ */ import { LocationSpec } from '@backstage/catalog-model'; -import fetch, { RequestInit, HeadersInit } from 'node-fetch'; -import * as result from './results'; -import { LocationProcessor, LocationProcessorEmit } from './types'; import { Config } from '@backstage/config'; +import fetch, { HeadersInit, RequestInit } from 'node-fetch'; +import * as result from './results'; +import { CatalogProcessor, CatalogProcessorEmit } from './types'; // *********************************************************************** // * NOTE: This has been replaced by packages/backend-common/src/reading * // * Don't implement new functionality here as this file will be removed * // *********************************************************************** -export class AzureApiReaderProcessor implements LocationProcessor { +export class AzureApiReaderProcessor implements CatalogProcessor { private privateToken: string; constructor(config: Config) { @@ -54,7 +54,7 @@ export class AzureApiReaderProcessor implements LocationProcessor { async readLocation( location: LocationSpec, optional: boolean, - emit: LocationProcessorEmit, + emit: CatalogProcessorEmit, ): Promise { if (location.type !== 'azure/api') { return false; diff --git a/plugins/catalog-backend/src/ingestion/processors/BitbucketApiReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/BitbucketApiReaderProcessor.ts index b3720a5b76..4efcec2884 100644 --- a/plugins/catalog-backend/src/ingestion/processors/BitbucketApiReaderProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/BitbucketApiReaderProcessor.ts @@ -15,17 +15,17 @@ */ import { LocationSpec } from '@backstage/catalog-model'; -import fetch, { RequestInit, HeadersInit } from 'node-fetch'; -import * as result from './results'; -import { LocationProcessor, LocationProcessorEmit } from './types'; import { Config } from '@backstage/config'; +import fetch, { HeadersInit, RequestInit } from 'node-fetch'; +import * as result from './results'; +import { CatalogProcessor, CatalogProcessorEmit } from './types'; // *********************************************************************** // * NOTE: This has been replaced by packages/backend-common/src/reading * // * Don't implement new functionality here as this file will be removed * // *********************************************************************** -export class BitbucketApiReaderProcessor implements LocationProcessor { +export class BitbucketApiReaderProcessor implements CatalogProcessor { private username: string; private password: string; @@ -58,7 +58,7 @@ export class BitbucketApiReaderProcessor implements LocationProcessor { async readLocation( location: LocationSpec, optional: boolean, - emit: LocationProcessorEmit, + emit: CatalogProcessorEmit, ): Promise { if (location.type !== 'bitbucket/api') { return false; diff --git a/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.ts index e624af91ee..1802e01d99 100644 --- a/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.ts @@ -16,14 +16,13 @@ import { UrlReader } from '@backstage/backend-common'; import { Entity, LocationSpec } from '@backstage/catalog-model'; -import { LocationProcessor } from './types'; import * as codeowners from 'codeowners-utils'; import { CodeOwnersEntry } from 'codeowners-utils'; -import parseGitUri from 'git-url-parse'; -import { filter, head, get, pipe, reverse } from 'lodash/fp'; - // NOTE: This can be removed when ES2021 is implemented import 'core-js/features/promise'; +import parseGitUri from 'git-url-parse'; +import { filter, get, head, pipe, reverse } from 'lodash/fp'; +import { CatalogProcessor } from './types'; const ALLOWED_LOCATION_TYPES = [ 'azure/api', @@ -38,7 +37,7 @@ type Options = { reader: UrlReader; }; -export class CodeOwnersProcessor implements LocationProcessor { +export class CodeOwnersProcessor implements CatalogProcessor { constructor(private readonly options: Options) {} async processEntity(entity: Entity, location: LocationSpec): Promise { diff --git a/plugins/catalog-backend/src/ingestion/processors/EntityPolicyProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/EntityPolicyProcessor.ts index 67da9a6705..87543d91f4 100644 --- a/plugins/catalog-backend/src/ingestion/processors/EntityPolicyProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/EntityPolicyProcessor.ts @@ -15,9 +15,9 @@ */ import { Entity, EntityPolicy } from '@backstage/catalog-model'; -import { LocationProcessor } from './types'; +import { CatalogProcessor } from './types'; -export class EntityPolicyProcessor implements LocationProcessor { +export class EntityPolicyProcessor implements CatalogProcessor { private readonly policy: EntityPolicy; constructor(policy: EntityPolicy) { diff --git a/plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.ts index b95e2ddb58..83fc01185b 100644 --- a/plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.ts @@ -17,13 +17,13 @@ import { LocationSpec } from '@backstage/catalog-model'; import fs from 'fs-extra'; import * as result from './results'; -import { LocationProcessor, LocationProcessorEmit } from './types'; +import { CatalogProcessor, CatalogProcessorEmit } from './types'; -export class FileReaderProcessor implements LocationProcessor { +export class FileReaderProcessor implements CatalogProcessor { async readLocation( location: LocationSpec, optional: boolean, - emit: LocationProcessorEmit, + emit: CatalogProcessorEmit, ): Promise { if (location.type !== 'file') { return false; diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts index b8a6f6c43f..bef57a6e5c 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts @@ -19,14 +19,14 @@ import { Config } from '@backstage/config'; import { graphql } from '@octokit/graphql'; import { Logger } from 'winston'; import * as results from './results'; -import { LocationProcessor, LocationProcessorEmit } from './types'; +import { CatalogProcessor, CatalogProcessorEmit } from './types'; import { getOrganizationTeams, getOrganizationUsers } from './util/github'; import { buildOrgHierarchy } from './util/org'; /** * Extracts teams and users out of a GitHub org. */ -export class GithubOrgReaderProcessor implements LocationProcessor { +export class GithubOrgReaderProcessor implements CatalogProcessor { private readonly providers: ProviderConfig[]; private readonly logger: Logger; @@ -45,7 +45,7 @@ export class GithubOrgReaderProcessor implements LocationProcessor { async readLocation( location: LocationSpec, _optional: boolean, - emit: LocationProcessorEmit, + emit: CatalogProcessorEmit, ): Promise { if (location.type !== 'github-org') { return false; diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/GithubReaderProcessor.ts index 89d1f69860..2f03900cdb 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubReaderProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/GithubReaderProcessor.ts @@ -20,7 +20,7 @@ import parseGitUri from 'git-url-parse'; import fetch, { HeadersInit, RequestInit } from 'node-fetch'; import { Logger } from 'winston'; import * as result from './results'; -import { LocationProcessor, LocationProcessorEmit } from './types'; +import { CatalogProcessor, CatalogProcessorEmit } from './types'; // *********************************************************************** // * NOTE: This has been replaced by packages/backend-common/src/reading * @@ -210,7 +210,7 @@ export function readConfig(config: Config, logger: Logger): ProviderConfig[] { * A processor that adds the ability to read files from GitHub v3 APIs, such as * the one exposed by GitHub itself. */ -export class GithubReaderProcessor implements LocationProcessor { +export class GithubReaderProcessor implements CatalogProcessor { private providers: ProviderConfig[]; static fromConfig(config: Config, logger: Logger) { @@ -224,7 +224,7 @@ export class GithubReaderProcessor implements LocationProcessor { async readLocation( location: LocationSpec, optional: boolean, - emit: LocationProcessorEmit, + emit: CatalogProcessorEmit, ): Promise { // The github/api type is for backward compatibility if (location.type !== 'github' && location.type !== 'github/api') { diff --git a/plugins/catalog-backend/src/ingestion/processors/GitlabApiReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/GitlabApiReaderProcessor.ts index aeba7a7ebe..e47b7d1aad 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GitlabApiReaderProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/GitlabApiReaderProcessor.ts @@ -15,17 +15,17 @@ */ import { LocationSpec } from '@backstage/catalog-model'; -import fetch, { RequestInit, HeadersInit } from 'node-fetch'; -import * as result from './results'; -import { LocationProcessor, LocationProcessorEmit } from './types'; import { Config } from '@backstage/config'; +import fetch, { HeadersInit, RequestInit } from 'node-fetch'; +import * as result from './results'; +import { CatalogProcessor, CatalogProcessorEmit } from './types'; // *********************************************************************** // * NOTE: This has been replaced by packages/backend-common/src/reading * // * Don't implement new functionality here as this file will be removed * // *********************************************************************** -export class GitlabApiReaderProcessor implements LocationProcessor { +export class GitlabApiReaderProcessor implements CatalogProcessor { private privateToken: string; constructor(config: Config) { @@ -50,7 +50,7 @@ export class GitlabApiReaderProcessor implements LocationProcessor { async readLocation( location: LocationSpec, optional: boolean, - emit: LocationProcessorEmit, + emit: CatalogProcessorEmit, ): Promise { if (location.type !== 'gitlab/api') { return false; diff --git a/plugins/catalog-backend/src/ingestion/processors/GitlabReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/GitlabReaderProcessor.ts index b51bf85a8e..2c58a6d43e 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GitlabReaderProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/GitlabReaderProcessor.ts @@ -17,18 +17,18 @@ import { LocationSpec } from '@backstage/catalog-model'; import fetch from 'node-fetch'; import * as result from './results'; -import { LocationProcessor, LocationProcessorEmit } from './types'; +import { CatalogProcessor, CatalogProcessorEmit } from './types'; // *********************************************************************** // * NOTE: This has been replaced by packages/backend-common/src/reading * // * Don't implement new functionality here as this file will be removed * // *********************************************************************** -export class GitlabReaderProcessor implements LocationProcessor { +export class GitlabReaderProcessor implements CatalogProcessor { async readLocation( location: LocationSpec, optional: boolean, - emit: LocationProcessorEmit, + emit: CatalogProcessorEmit, ): Promise { if (location.type !== 'gitlab') { return false; diff --git a/plugins/catalog-backend/src/ingestion/processors/LdapOrgReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/LdapOrgReaderProcessor.ts index a32c96480f..d863142169 100644 --- a/plugins/catalog-backend/src/ingestion/processors/LdapOrgReaderProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/LdapOrgReaderProcessor.ts @@ -24,12 +24,12 @@ import { readLdapOrg, } from './ldap'; import * as results from './results'; -import { LocationProcessor, LocationProcessorEmit } from './types'; +import { CatalogProcessor, CatalogProcessorEmit } from './types'; /** * Extracts teams and users out of an LDAP server. */ -export class LdapOrgReaderProcessor implements LocationProcessor { +export class LdapOrgReaderProcessor implements CatalogProcessor { private readonly providers: LdapProviderConfig[]; private readonly logger: Logger; @@ -49,7 +49,7 @@ export class LdapOrgReaderProcessor implements LocationProcessor { async readLocation( location: LocationSpec, _optional: boolean, - emit: LocationProcessorEmit, + emit: CatalogProcessorEmit, ): Promise { if (location.type !== 'ldap-org') { return false; diff --git a/plugins/catalog-backend/src/ingestion/processors/LocationEntityProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/LocationEntityProcessor.ts index a5c9718574..1917f58fbd 100644 --- a/plugins/catalog-backend/src/ingestion/processors/LocationEntityProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/LocationEntityProcessor.ts @@ -16,13 +16,13 @@ import { Entity, LocationEntity, LocationSpec } from '@backstage/catalog-model'; import * as result from './results'; -import { LocationProcessor, LocationProcessorEmit } from './types'; +import { CatalogProcessor, CatalogProcessorEmit } from './types'; -export class LocationRefProcessor implements LocationProcessor { +export class LocationRefProcessor implements CatalogProcessor { async processEntity( entity: Entity, _location: LocationSpec, - emit: LocationProcessorEmit, + emit: CatalogProcessorEmit, ): Promise { if (entity.kind === 'Location') { const location = entity as LocationEntity; diff --git a/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.ts index 630c78d600..3941a2bedc 100644 --- a/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.ts @@ -18,7 +18,7 @@ import { UrlReader } from '@backstage/backend-common'; import { Entity, LocationSpec } from '@backstage/catalog-model'; import { JsonValue } from '@backstage/config'; import yaml from 'yaml'; -import { LocationProcessor } from './types'; +import { CatalogProcessor } from './types'; export type ResolverRead = (url: string) => Promise; @@ -42,7 +42,7 @@ type Options = { * Traverses raw entity JSON looking for occurrences of $-prefixed placeholders * that it then fills in with actual data. */ -export class PlaceholderProcessor implements LocationProcessor { +export class PlaceholderProcessor implements CatalogProcessor { constructor(private readonly options: Options) {} async processEntity(entity: Entity, location: LocationSpec): Promise { diff --git a/plugins/catalog-backend/src/ingestion/processors/StaticLocationProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/StaticLocationProcessor.ts index 6a2d1096cc..f34b672183 100644 --- a/plugins/catalog-backend/src/ingestion/processors/StaticLocationProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/StaticLocationProcessor.ts @@ -15,9 +15,9 @@ */ import { LocationSpec } from '@backstage/catalog-model'; -import * as result from './results'; import { Config } from '@backstage/config'; -import { LocationProcessorEmit } from './types'; +import * as result from './results'; +import { CatalogProcessorEmit } from './types'; export class StaticLocationProcessor implements StaticLocationProcessor { static fromConfig(config: Config): StaticLocationProcessor { @@ -38,7 +38,7 @@ export class StaticLocationProcessor implements StaticLocationProcessor { async readLocation( location: LocationSpec, _optional: boolean, - emit: LocationProcessorEmit, + emit: CatalogProcessorEmit, ): Promise { if (location.type !== 'bootstrap') { return false; diff --git a/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts index 009872cda3..a2b9678ba0 100644 --- a/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts @@ -14,16 +14,16 @@ * limitations under the License. */ -import { UrlReaderProcessor } from './UrlReaderProcessor'; -import { - LocationProcessorDataResult, - LocationProcessorResult, - LocationProcessorErrorResult, -} from './types'; -import { setupServer } from 'msw/node'; -import { rest } from 'msw'; import { getVoidLogger, UrlReaders } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { + CatalogProcessorDataResult, + CatalogProcessorErrorResult, + CatalogProcessorResult, +} from './types'; +import { UrlReaderProcessor } from './UrlReaderProcessor'; describe('UrlReaderProcessor', () => { const mockApiOrigin = 'http://localhost:23000'; @@ -48,9 +48,9 @@ describe('UrlReaderProcessor', () => { ), ); - const generated = (await new Promise(emit => + const generated = (await new Promise(emit => processor.readLocation(spec, false, emit), - )) as LocationProcessorDataResult; + )) as CatalogProcessorDataResult; expect(generated.type).toBe('data'); expect(generated.location).toBe(spec); @@ -72,9 +72,9 @@ describe('UrlReaderProcessor', () => { }), ); - const generated = (await new Promise(emit => + const generated = (await new Promise(emit => processor.readLocation(spec, false, emit), - )) as LocationProcessorErrorResult; + )) as CatalogProcessorErrorResult; expect(generated.type).toBe('error'); expect(generated.location).toBe(spec); diff --git a/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.ts index 2bc05bf1cb..0169abbbe3 100644 --- a/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import { Logger } from 'winston'; import { UrlReader } from '@backstage/backend-common'; import { LocationSpec } from '@backstage/catalog-model'; +import { Logger } from 'winston'; import * as result from './results'; -import { LocationProcessor, LocationProcessorEmit } from './types'; +import { CatalogProcessor, CatalogProcessorEmit } from './types'; // TODO(Rugvip): Added for backwards compatibility when moving to UrlReader, this // can be removed in a bit @@ -35,13 +35,13 @@ type Options = { logger: Logger; }; -export class UrlReaderProcessor implements LocationProcessor { +export class UrlReaderProcessor implements CatalogProcessor { constructor(private readonly options: Options) {} async readLocation( location: LocationSpec, optional: boolean, - emit: LocationProcessorEmit, + emit: CatalogProcessorEmit, ): Promise { if (deprecatedTypes.includes(location.type)) { // TODO(Rugvip): Let's not enable this warning yet, as we want to move over the example YAMLs diff --git a/plugins/catalog-backend/src/ingestion/processors/YamlProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/YamlProcessor.test.ts index 9f1ede3f9e..f5d2dfea38 100644 --- a/plugins/catalog-backend/src/ingestion/processors/YamlProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/YamlProcessor.test.ts @@ -14,14 +14,14 @@ * limitations under the License. */ -import { YamlProcessor } from './YamlProcessor'; import { Entity } from '@backstage/catalog-model'; -import yaml from 'yaml'; import { TextEncoder } from 'util'; +import yaml from 'yaml'; import { - LocationProcessorEntityResult, - LocationProcessorErrorResult, + CatalogProcessorEntityResult, + CatalogProcessorErrorResult, } from './types'; +import { YamlProcessor } from './YamlProcessor'; describe('YamlProcessor', () => { const processor = new YamlProcessor(); @@ -82,7 +82,7 @@ describe('YamlProcessor', () => { expect(await processor.parseData(buffer, locationSpec, emit)).toBe(true); - const e = emit.mock.calls[0][0] as LocationProcessorEntityResult; + const e = emit.mock.calls[0][0] as CatalogProcessorEntityResult; expect(e.type).toBe('entity'); expect(e.location).toBe(locationSpec); expect(e.entity).toEqual(entity); @@ -114,12 +114,12 @@ describe('YamlProcessor', () => { expect(await processor.parseData(buffer, locationSpec, emit)).toBe(true); - const eComponent = emit.mock.calls[0][0] as LocationProcessorEntityResult; + const eComponent = emit.mock.calls[0][0] as CatalogProcessorEntityResult; expect(eComponent.type).toBe('entity'); expect(eComponent.location).toBe(locationSpec); expect(eComponent.entity).toEqual(entityComponent); - const eApi = emit.mock.calls[1][0] as LocationProcessorEntityResult; + const eApi = emit.mock.calls[1][0] as CatalogProcessorEntityResult; expect(eApi.type).toBe('entity'); expect(eApi.location).toBe(locationSpec); expect(eApi.entity).toEqual(entityApi); @@ -131,7 +131,7 @@ describe('YamlProcessor', () => { expect(await processor.parseData(buffer, locationSpec, emit)).toBe(true); - const e = emit.mock.calls[0][0] as LocationProcessorErrorResult; + const e = emit.mock.calls[0][0] as CatalogProcessorErrorResult; expect(e.error.message).toMatch(/^YAML error, /); expect(e.type).toBe('error'); expect(e.location).toBe(locationSpec); @@ -143,7 +143,7 @@ describe('YamlProcessor', () => { expect(await processor.parseData(buffer, locationSpec, emit)).toBe(true); - const e = emit.mock.calls[0][0] as LocationProcessorErrorResult; + const e = emit.mock.calls[0][0] as CatalogProcessorErrorResult; expect(e.error.message).toMatch(/^Expected object at root, got /); expect(e.type).toBe('error'); expect(e.location).toBe(locationSpec); diff --git a/plugins/catalog-backend/src/ingestion/processors/YamlProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/YamlProcessor.ts index 860f9ff752..46df4e6a2b 100644 --- a/plugins/catalog-backend/src/ingestion/processors/YamlProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/YamlProcessor.ts @@ -18,18 +18,18 @@ import { Entity, LocationSpec } from '@backstage/catalog-model'; import lodash from 'lodash'; import yaml from 'yaml'; import * as result from './results'; -import { LocationProcessor, LocationProcessorEmit } from './types'; +import { CatalogProcessor, CatalogProcessorEmit } from './types'; /** * Handles incoming raw data buffers, and if they have a yaml extension, * attempts to parse them into structured data and emitting them as un- * validated entities. */ -export class YamlProcessor implements LocationProcessor { +export class YamlProcessor implements CatalogProcessor { async parseData( data: Buffer, location: LocationSpec, - emit: LocationProcessorEmit, + emit: CatalogProcessorEmit, ): Promise { if (!location.target.match(/\.ya?ml/)) { return false; diff --git a/plugins/catalog-backend/src/ingestion/processors/results.ts b/plugins/catalog-backend/src/ingestion/processors/results.ts index 9c6a7fc2b3..2718d871dd 100644 --- a/plugins/catalog-backend/src/ingestion/processors/results.ts +++ b/plugins/catalog-backend/src/ingestion/processors/results.ts @@ -16,12 +16,12 @@ import { InputError, NotFoundError } from '@backstage/backend-common'; import { Entity, LocationSpec } from '@backstage/catalog-model'; -import { LocationProcessorResult } from './types'; +import { CatalogProcessorResult } from './types'; export function notFoundError( atLocation: LocationSpec, message: string, -): LocationProcessorResult { +): CatalogProcessorResult { return { type: 'error', location: atLocation, @@ -32,7 +32,7 @@ export function notFoundError( export function inputError( atLocation: LocationSpec, message: string, -): LocationProcessorResult { +): CatalogProcessorResult { return { type: 'error', location: atLocation, @@ -43,27 +43,27 @@ export function inputError( export function generalError( atLocation: LocationSpec, message: string, -): LocationProcessorResult { +): CatalogProcessorResult { return { type: 'error', location: atLocation, error: new Error(message) }; } export function data( atLocation: LocationSpec, newData: Buffer, -): LocationProcessorResult { +): CatalogProcessorResult { return { type: 'data', location: atLocation, data: newData }; } export function location( newLocation: LocationSpec, optional: boolean, -): LocationProcessorResult { +): CatalogProcessorResult { return { type: 'location', location: newLocation, optional }; } export function entity( atLocation: LocationSpec, newEntity: Entity, -): LocationProcessorResult { +): CatalogProcessorResult { return { type: 'entity', location: atLocation, entity: newEntity }; } diff --git a/plugins/catalog-backend/src/ingestion/processors/types.ts b/plugins/catalog-backend/src/ingestion/processors/types.ts index cd2c423423..0e6b1491a6 100644 --- a/plugins/catalog-backend/src/ingestion/processors/types.ts +++ b/plugins/catalog-backend/src/ingestion/processors/types.ts @@ -16,7 +16,7 @@ import { Entity, LocationSpec } from '@backstage/catalog-model'; -export type LocationProcessor = { +export type CatalogProcessor = { /** * Reads the contents of a location. * @@ -28,7 +28,7 @@ export type LocationProcessor = { readLocation?( location: LocationSpec, optional: boolean, - emit: LocationProcessorEmit, + emit: CatalogProcessorEmit, ): Promise; /** @@ -42,7 +42,7 @@ export type LocationProcessor = { parseData?( data: Buffer, location: LocationSpec, - emit: LocationProcessorEmit, + emit: CatalogProcessorEmit, ): Promise; /** @@ -57,7 +57,7 @@ export type LocationProcessor = { processEntity?( entity: Entity, location: LocationSpec, - emit: LocationProcessorEmit, + emit: CatalogProcessorEmit, ): Promise; /** @@ -71,40 +71,38 @@ export type LocationProcessor = { handleError?( error: Error, location: LocationSpec, - emit: LocationProcessorEmit, + emit: CatalogProcessorEmit, ): Promise; }; -export type LocationProcessorEmit = ( - generated: LocationProcessorResult, -) => void; +export type CatalogProcessorEmit = (generated: CatalogProcessorResult) => void; -export type LocationProcessorLocationResult = { +export type CatalogProcessorLocationResult = { type: 'location'; location: LocationSpec; optional: boolean; }; -export type LocationProcessorDataResult = { +export type CatalogProcessorDataResult = { type: 'data'; data: Buffer; location: LocationSpec; }; -export type LocationProcessorEntityResult = { +export type CatalogProcessorEntityResult = { type: 'entity'; entity: Entity; location: LocationSpec; }; -export type LocationProcessorErrorResult = { +export type CatalogProcessorErrorResult = { type: 'error'; error: Error; location: LocationSpec; }; -export type LocationProcessorResult = - | LocationProcessorLocationResult - | LocationProcessorDataResult - | LocationProcessorEntityResult - | LocationProcessorErrorResult; +export type CatalogProcessorResult = + | CatalogProcessorLocationResult + | CatalogProcessorDataResult + | CatalogProcessorEntityResult + | CatalogProcessorErrorResult; diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.test.ts b/plugins/catalog-backend/src/service/CatalogBuilder.test.ts index e2467654d8..66e7dba682 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.test.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.test.ts @@ -18,9 +18,9 @@ import { getVoidLogger, UrlReader } from '@backstage/backend-common'; import { Entity, LocationSpec } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import { DatabaseManager } from '../database'; -import { LocationProcessorEmit } from '../ingestion'; -import { CatalogBuilder, CatalogEnvironment } from './CatalogBuilder'; +import { CatalogProcessorEmit } from '../ingestion'; import * as result from '../ingestion/processors/results'; +import { CatalogBuilder, CatalogEnvironment } from './CatalogBuilder'; describe('CatalogBuilder', () => { const db = DatabaseManager.createTestDatabaseConnection(); @@ -54,7 +54,7 @@ describe('CatalogBuilder', () => { async readLocation( location: LocationSpec, _optional: boolean, - emit: LocationProcessorEmit, + emit: CatalogProcessorEmit, ) { expect(location.type).toBe('test'); emit(result.data(location, await reader.read('ignored'))); @@ -67,7 +67,7 @@ describe('CatalogBuilder', () => { async parseData( data: Buffer, location: LocationSpec, - emit: LocationProcessorEmit, + emit: CatalogProcessorEmit, ) { expect(data.toString()).toEqual('junk'); emit( diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index e1d76bc3cd..aa4f97258d 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -46,6 +46,7 @@ import { AnnotateLocationEntityProcessor, AzureApiReaderProcessor, BitbucketApiReaderProcessor, + CatalogProcessor, CodeOwnersProcessor, EntityPolicyProcessor, FileReaderProcessor, @@ -55,7 +56,6 @@ import { GitlabReaderProcessor, HigherOrderOperation, HigherOrderOperations, - LocationProcessor, LocationReaders, LocationRefProcessor, PlaceholderProcessor, @@ -121,13 +121,13 @@ export class CatalogBuilder { private entityPoliciesReplace: boolean; private entityKinds: EntityPolicy[]; private entityKindsReplace: boolean; - private readerProcessors: LocationProcessor[]; + private readerProcessors: CatalogProcessor[]; private readerProcessorsReplace: boolean; - private parserProcessors: LocationProcessor[]; + private parserProcessors: CatalogProcessor[]; private parserProcessorsReplace: boolean; - private preProcessors: LocationProcessor[]; + private preProcessors: CatalogProcessor[]; private preProcessorsReplace: boolean; - private postProcessors: LocationProcessor[]; + private postProcessors: CatalogProcessor[]; private postProcessorsReplace: boolean; private placeholderResolvers: Record; private fieldFormatValidators: Partial; @@ -219,7 +219,7 @@ export class CatalogBuilder { * * @param processors One or more processors */ - addReaderProcessor(...processors: LocationProcessor[]): CatalogBuilder { + addReaderProcessor(...processors: CatalogProcessor[]): CatalogBuilder { this.readerProcessors.push(...processors); return this; } @@ -234,7 +234,7 @@ export class CatalogBuilder { * * @param processors One or more processors */ - replaceReaderProcessors(processors: LocationProcessor[]): CatalogBuilder { + replaceReaderProcessors(processors: CatalogProcessor[]): CatalogBuilder { this.readerProcessors = [...processors]; this.readerProcessorsReplace = true; return this; @@ -247,7 +247,7 @@ export class CatalogBuilder { * * @param processors One or more processors */ - addParserProcessor(...processors: LocationProcessor[]): CatalogBuilder { + addParserProcessor(...processors: CatalogProcessor[]): CatalogBuilder { this.parserProcessors.push(...processors); return this; } @@ -262,7 +262,7 @@ export class CatalogBuilder { * * @param processors One or more processors */ - replaceParserProcessors(processors: LocationProcessor[]): CatalogBuilder { + replaceParserProcessors(processors: CatalogProcessor[]): CatalogBuilder { this.parserProcessors = [...processors]; this.parserProcessorsReplace = true; return this; @@ -274,7 +274,7 @@ export class CatalogBuilder { * * @param processors One or more processors */ - addPreProcessor(...processors: LocationProcessor[]): CatalogBuilder { + addPreProcessor(...processors: CatalogProcessor[]): CatalogBuilder { this.preProcessors.push(...processors); return this; } @@ -288,7 +288,7 @@ export class CatalogBuilder { * * @param processors One or more processors */ - replacePreProcessors(processors: LocationProcessor[]): CatalogBuilder { + replacePreProcessors(processors: CatalogProcessor[]): CatalogBuilder { this.preProcessors = [...processors]; this.preProcessorsReplace = true; return this; @@ -300,7 +300,7 @@ export class CatalogBuilder { * * @param processors One or more processors */ - addPostProcessor(...processors: LocationProcessor[]): CatalogBuilder { + addPostProcessor(...processors: CatalogProcessor[]): CatalogBuilder { this.postProcessors.push(...processors); return this; } @@ -314,7 +314,7 @@ export class CatalogBuilder { * * @param processors One or more processors */ - replacePostProcessors(processors: LocationProcessor[]): CatalogBuilder { + replacePostProcessors(processors: CatalogProcessor[]): CatalogBuilder { this.postProcessors = [...processors]; this.postProcessorsReplace = true; return this; @@ -418,7 +418,7 @@ export class CatalogBuilder { ]); } - private buildProcessors(entityPolicy: EntityPolicy): LocationProcessor[] { + private buildProcessors(entityPolicy: EntityPolicy): CatalogProcessor[] { const { config, reader } = this.env; const placeholderResolvers = lodash.merge( @@ -441,7 +441,7 @@ export class CatalogBuilder { ]; } - private buildReaderProcessors(): LocationProcessor[] { + private buildReaderProcessors(): CatalogProcessor[] { const { config, logger, reader } = this.env; if (this.readerProcessorsReplace) { @@ -491,7 +491,7 @@ export class CatalogBuilder { ]; } - private buildParserProcessors(): LocationProcessor[] { + private buildParserProcessors(): CatalogProcessor[] { if (this.parserProcessorsReplace) { return this.parserProcessors; } @@ -499,7 +499,7 @@ export class CatalogBuilder { return [new YamlProcessor(), ...this.parserProcessors]; } - private buildPreProcessors(): LocationProcessor[] { + private buildPreProcessors(): CatalogProcessor[] { const { reader } = this.env; if (this.preProcessorsReplace) { @@ -509,7 +509,7 @@ export class CatalogBuilder { return [new CodeOwnersProcessor({ reader }), ...this.preProcessors]; } - private buildPostProcessors(): LocationProcessor[] { + private buildPostProcessors(): CatalogProcessor[] { if (this.postProcessorsReplace) { return this.postProcessors; } From 973500dd8ddfa2c3f638f9cfbf54ab87426138b6 Mon Sep 17 00:00:00 2001 From: Marvin9 Date: Thu, 15 Oct 2020 19:34:57 +0530 Subject: [PATCH 087/139] chore: make pageTheme optional & provide default value --- packages/theme/src/baseTheme.ts | 3 ++- packages/theme/src/types.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/theme/src/baseTheme.ts b/packages/theme/src/baseTheme.ts index 640476c487..b0797c6506 100644 --- a/packages/theme/src/baseTheme.ts +++ b/packages/theme/src/baseTheme.ts @@ -23,6 +23,7 @@ import { BackstageThemeOptions, SimpleThemeOptions, } from './types'; +import { pageTheme as defaultPageThemes } from './pageTheme'; const DEFAULT_FONT_FAMILY = '"Helvetica Neue", Helvetica, Roboto, Arial, sans-serif'; @@ -34,7 +35,7 @@ export function createThemeOptions( palette, fontFamily = DEFAULT_FONT_FAMILY, defaultPageTheme, - pageTheme, + pageTheme = defaultPageThemes, } = options; if (!pageTheme[defaultPageTheme]) { diff --git a/packages/theme/src/types.ts b/packages/theme/src/types.ts index 696ccf586f..1c6ccc0754 100644 --- a/packages/theme/src/types.ts +++ b/packages/theme/src/types.ts @@ -94,7 +94,7 @@ export interface BackstageThemeOptions extends ThemeOptions { export type SimpleThemeOptions = { palette: BackstagePaletteOptions; defaultPageTheme: string; - pageTheme: Record; + pageTheme?: Record; fontFamily?: string; }; From 9af34d30f17027d0471551c8f86dad587c72e670 Mon Sep 17 00:00:00 2001 From: David Tuite Date: Thu, 15 Oct 2020 15:53:29 +0100 Subject: [PATCH 088/139] Upgrade GitHub Insights plugin to 0.2.6 Fixes issue with entities backed to private repos not rendering when the GitHub insights plugin is installed. --- packages/app/package.json | 2 +- yarn.lock | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/app/package.json b/packages/app/package.json index baa273fd08..65ba6140b2 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -34,8 +34,8 @@ "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@octokit/rest": "^18.0.0", + "@roadiehq/backstage-plugin-github-insights": "^0.2.6", "@roadiehq/backstage-plugin-github-pull-requests": "^0.5.1", - "@roadiehq/backstage-plugin-github-insights": "^0.2.2", "@roadiehq/backstage-plugin-travis-ci": "^0.2.3", "dayjs": "^1.9.1", "history": "^5.0.0", diff --git a/yarn.lock b/yarn.lock index c47977b4f4..574c0f65a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3650,10 +3650,10 @@ resolved "https://registry.npmjs.org/@rjsf/material-ui/-/material-ui-2.3.0.tgz#a051eb4db2ad778e39933a31ba806f415dd3ba90" integrity sha512-v/xZ4Xk18ZgBARcCe99IDqdO97GU0UC784gG8PhGGFDdy5Rbdy7ixTjHkGYttkr43PB7SX6ttohNhkKSW4nATQ== -"@roadiehq/backstage-plugin-github-insights@^0.2.2": - version "0.2.2" - resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-github-insights/-/backstage-plugin-github-insights-0.2.2.tgz#1647aec22f1f8bc40268f075ae307abf660ae75c" - integrity sha512-m4j1kUmoj+ZCRyaisXVBkLW9kaI/OGMq8xzthm4adOb12mS0s2p2P9AMa4T+bRyLgHpQ0vB75qhw4p6sHJAvWA== +"@roadiehq/backstage-plugin-github-insights@^0.2.6": + version "0.2.6" + resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-github-insights/-/backstage-plugin-github-insights-0.2.6.tgz#7c854cc6b107ff30b0f03b1a5e1ca5e04723dd99" + integrity sha512-d3CmA2B0iOB2jKFK3z4q1ehl9nCfh2yrCb6byOvsIyV2xlm3cnt3Uq5hLlQekTYLWRvBWw2ULQVC8+wk2Uq6gg== dependencies: "@backstage/catalog-model" "^0.1.1-alpha.24" "@backstage/core" "^0.1.1-alpha.24" @@ -3661,6 +3661,8 @@ "@material-ui/core" "^4.11.0" "@material-ui/icons" "^4.9.1" "@material-ui/lab" "4.0.0-alpha.45" + "@octokit/rest" "^18.0.0" + "@octokit/types" "^5.0.1" history "^5.0.0" react "^16.13.1" react-dom "^16.13.1" From 5c46f0d337cfb937dc4e47286a427cc88b96ffcd Mon Sep 17 00:00:00 2001 From: Markus Heinemann Date: Thu, 15 Oct 2020 20:35:43 +0000 Subject: [PATCH 089/139] Rename config property for custom github scaffolder host --- app-config.yaml | 2 +- packages/backend/src/plugins/scaffolder.ts | 5 +++-- .../default-app/packages/backend/src/plugins/scaffolder.ts | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index b527069c15..768e60a346 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -147,7 +147,7 @@ catalog: scaffolder: github: - baseUrl: https://github.com + host: https://github.com token: $env: GITHUB_TOKEN visibility: public # or 'internal' or 'private' diff --git a/packages/backend/src/plugins/scaffolder.ts b/packages/backend/src/plugins/scaffolder.ts index 7ba90ed8b5..24bd4e942f 100644 --- a/packages/backend/src/plugins/scaffolder.ts +++ b/packages/backend/src/plugins/scaffolder.ts @@ -68,10 +68,11 @@ export default async function createPlugin({ ) as RepoVisibilityOptions; const githubToken = githubConfig.getString('token'); - const githubBaseUrl = githubConfig.getString('baseUrl'); + const githubHost = + githubConfig.getOptionalString('host') ?? 'https://github.com'; const githubClient = new Octokit({ auth: githubToken, - baseUrl: githubBaseUrl, + baseUrl: githubHost, }); const githubPublisher = new GithubPublisher({ client: githubClient, diff --git a/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts b/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts index 9f59412036..240309fbd1 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts @@ -48,8 +48,8 @@ export default async function createPlugin({ ) as RepoVisibilityOptions; const githubToken = githubConfig.getString('token'); - const githubBaseUrl = githubConfig.getString('baseUrl'); - const githubClient = new Octokit({ auth: githubToken, baseUrl: githubBaseUrl }); + const githubHost = githubConfig.getOptionalString('host'); + const githubClient = new Octokit({ auth: githubToken, baseUrl: githubHost }); const githubPublisher = new GithubPublisher({ client: githubClient, token: githubToken, From ee88f6d21023f9949248daf565057f22bb3e21ee Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Thu, 15 Oct 2020 22:44:15 +0200 Subject: [PATCH 090/139] Fix example in README. --- plugins/newrelic/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/newrelic/README.md b/plugins/newrelic/README.md index e787d9c91f..e14acedef2 100644 --- a/plugins/newrelic/README.md +++ b/plugins/newrelic/README.md @@ -29,7 +29,7 @@ While working locally, you may wish to hard-code your API key in your ```yaml # app-config.local.yaml proxy: - 'newrelic/apm/api': + '/newrelic/apm/api': headers: X-Api-Key: NRRA-YourActualApiKey ``` From d553996bf27dba5fb7fb85728a92acb04442f628 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 Oct 2020 04:28:40 +0000 Subject: [PATCH 091/139] chore(deps): bump dayjs from 1.9.1 to 1.9.3 Bumps [dayjs](https://github.com/iamkun/dayjs) from 1.9.1 to 1.9.3. - [Release notes](https://github.com/iamkun/dayjs/releases) - [Changelog](https://github.com/iamkun/dayjs/blob/dev/CHANGELOG.md) - [Commits](https://github.com/iamkun/dayjs/compare/v1.9.1...v1.9.3) Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index adc0e7a8f1..4d28072207 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9594,9 +9594,9 @@ dateformat@^3.0.0: integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== dayjs@^1.9.1: - version "1.9.1" - resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.9.1.tgz#201a755f7db5103ed6de63ba93a984141c754541" - integrity sha512-01NCTBg8cuMJG1OQc6PR7T66+AFYiPwgDvdJmvJBn29NGzIG+DIFxPLNjHzwz3cpFIvG+NcwIjP9hSaPVoOaDg== + version "1.9.3" + resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.9.3.tgz#b7f94b22ad2a136a4ca02a01ab68ae893fe1a268" + integrity sha512-V+1SyIvkS+HmNbN1G7A9+ERbFTV9KTXu6Oor98v2xHmzzpp52OIJhQuJSTywWuBY5pyAEmlwbCi1Me87n/SLOw== de-indent@^1.0.2: version "1.0.2" From 275b7f001ab2d47c1c0c485a2166d765451686db Mon Sep 17 00:00:00 2001 From: Althaf Hameez Date: Fri, 16 Oct 2020 14:49:25 +0800 Subject: [PATCH 092/139] Change default organization to be set to My Company instead of Spotify / Acme Organization (#2918) --- app-config.yaml | 6 +++--- .../create-app/templates/default-app/app-config.yaml.hbs | 2 +- .../default-app/packages/app/cypress/integration/app.js | 2 +- packages/e2e-test/src/commands/run.ts | 6 +----- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index 3de43ad7e9..542110f774 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -41,7 +41,7 @@ proxy: $env: NEW_RELIC_REST_API_KEY organization: - name: Spotify + name: My Company techdocs: storageUrl: http://localhost:7000/api/techdocs/static/docs @@ -50,10 +50,10 @@ techdocs: techdocs: 'docker' sentry: - organization: spotify + organization: my-company rollbar: - organization: spotify + organization: my-company accountToken: $env: ROLLBAR_ACCOUNT_TOKEN diff --git a/packages/create-app/templates/default-app/app-config.yaml.hbs b/packages/create-app/templates/default-app/app-config.yaml.hbs index 347bbaa29c..b92cdf22bd 100644 --- a/packages/create-app/templates/default-app/app-config.yaml.hbs +++ b/packages/create-app/templates/default-app/app-config.yaml.hbs @@ -3,7 +3,7 @@ app: baseUrl: http://localhost:7000 organization: - name: Acme Corporation + name: My Company backend: baseUrl: http://localhost:7000 diff --git a/packages/create-app/templates/default-app/packages/app/cypress/integration/app.js b/packages/create-app/templates/default-app/packages/app/cypress/integration/app.js index 227e218fe6..efcd5b8d93 100644 --- a/packages/create-app/templates/default-app/packages/app/cypress/integration/app.js +++ b/packages/create-app/templates/default-app/packages/app/cypress/integration/app.js @@ -1,6 +1,6 @@ describe('App', () => { it('should render the catalog', () => { cy.visit('/'); - cy.contains('Acme Corporation Service Catalog'); + cy.contains('My Company Service Catalog'); }); }); diff --git a/packages/e2e-test/src/commands/run.ts b/packages/e2e-test/src/commands/run.ts index c05aed0b83..5caed14227 100644 --- a/packages/e2e-test/src/commands/run.ts +++ b/packages/e2e-test/src/commands/run.ts @@ -301,11 +301,7 @@ async function testAppServe(pluginName: string, appDir: string) { try { const browser = new Browser(); - await waitForPageWithText( - browser, - '/', - 'Acme Corporation Service Catalog', - ); + await waitForPageWithText(browser, '/', 'My Company Service Catalog'); await waitForPageWithText( browser, `/${pluginName}`, From c91054ac17c4dbe8dcd25ae6b312c5c6b3406024 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 16 Oct 2020 13:29:45 +0200 Subject: [PATCH 093/139] chore(deps): bump yup --- packages/catalog-model/package.json | 4 +- .../policies/SchemaValidEntityPolicy.ts | 38 ++++++++++--------- .../src/kinds/ComponentEntityV1alpha1.test.ts | 20 ++++++++++ .../src/kinds/ComponentEntityV1alpha1.ts | 2 +- .../src/kinds/GroupEntityV1alpha1.test.ts | 27 ++++++++++--- .../src/kinds/GroupEntityV1alpha1.ts | 14 ++++--- .../src/kinds/LocationEntityV1alpha1.test.ts | 5 +++ .../src/kinds/LocationEntityV1alpha1.ts | 2 +- .../src/kinds/UserEntityV1alpha1.ts | 6 ++- .../catalog-model/src/location/validation.ts | 6 ++- packages/config-loader/package.json | 4 +- packages/config-loader/src/lib/secrets.ts | 2 +- plugins/catalog-backend/package.json | 4 +- plugins/proxy-backend/package.json | 4 +- yarn.lock | 10 ++--- 15 files changed, 98 insertions(+), 50 deletions(-) diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index 8fc4484d33..5ba2b6efdf 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -22,11 +22,11 @@ "dependencies": { "@backstage/config": "^0.1.1-alpha.25", "@types/json-schema": "^7.0.5", - "@types/yup": "^0.28.2", + "@types/yup": "^0.29.8", "json-schema": "^0.2.5", "lodash": "^4.17.15", "uuid": "^8.0.0", - "yup": "^0.29.1" + "yup": "^0.29.3" }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.25", diff --git a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts index a6d376b4ac..4ff73e9a49 100644 --- a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts @@ -18,24 +18,26 @@ import * as yup from 'yup'; import { EntityPolicy } from '../../types'; import { Entity } from '../Entity'; -const DEFAULT_ENTITY_SCHEMA = yup.object({ - apiVersion: yup.string().required(), - kind: yup.string().required(), - metadata: yup - .object({ - uid: yup.string().notRequired().min(1), - etag: yup.string().notRequired().min(1), - generation: yup.number().notRequired().integer().min(1), - name: yup.string().required(), - namespace: yup.string().notRequired(), - description: yup.string().notRequired(), - labels: yup.object>().notRequired(), - annotations: yup.object>().notRequired(), - tags: yup.array().notRequired(), - }) - .required(), - spec: yup.object({}).notRequired(), -}); +const DEFAULT_ENTITY_SCHEMA = yup + .object({ + apiVersion: yup.string().required(), + kind: yup.string().required(), + metadata: yup + .object({ + uid: yup.string().notRequired().min(1), + etag: yup.string().notRequired().min(1), + generation: yup.number().notRequired().integer().min(1), + name: yup.string().required(), + namespace: yup.string().notRequired(), + description: yup.string().notRequired(), + labels: yup.object>().notRequired(), + annotations: yup.object>().notRequired(), + tags: yup.array().notRequired(), + }) + .required(), + spec: yup.object({}).notRequired(), + }) + .required(); /** * Ensures that the entity spec is valid according to a schema. diff --git a/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.test.ts index 57d6d47388..11434937cd 100644 --- a/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.test.ts +++ b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.test.ts @@ -101,4 +101,24 @@ describe('ComponentV1alpha1Policy', () => { (entity as any).spec.owner = ''; await expect(policy.enforce(entity)).rejects.toThrow(/owner/); }); + + it('accepts missing implementsApis', async () => { + delete (entity as any).spec.implementsApis; + await expect(policy.enforce(entity)).resolves.toBe(entity); + }); + + it('rejects empty implementsApis', async () => { + (entity as any).spec.implementsApis = ['']; + await expect(policy.enforce(entity)).rejects.toThrow(/implementsApis/); + }); + + it('rejects undefined implementsApis', async () => { + (entity as any).spec.implementsApis = [undefined]; + await expect(policy.enforce(entity)).rejects.toThrow(/implementsApis/); + }); + + it('accepts no implementsApis', async () => { + (entity as any).spec.implementsApis = []; + await expect(policy.enforce(entity)).resolves.toBe(entity); + }); }); diff --git a/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts index 9efc491ca9..662ce28b52 100644 --- a/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts @@ -29,7 +29,7 @@ const schema = yup.object>({ type: yup.string().required().min(1), lifecycle: yup.string().required().min(1), owner: yup.string().required().min(1), - implementsApis: yup.array(yup.string()).notRequired(), + implementsApis: yup.array(yup.string().required()).notRequired(), }) .required(), }); diff --git a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts index dc0ecd3fa3..9137727c6e 100644 --- a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts +++ b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts @@ -90,9 +90,14 @@ describe('GroupV1alpha1Policy', () => { await expect(policy.enforce(entity)).rejects.toThrow(/ancestor/); }); - it('accepts empty ancestors', async () => { + it('rejects empty ancestors', async () => { (entity as any).spec.ancestors = ['']; - await expect(policy.enforce(entity)).resolves.toBe(entity); + await expect(policy.enforce(entity)).rejects.toThrow(/ancestor/); + }); + + it('rejects undefined ancestors', async () => { + (entity as any).spec.ancestors = [undefined]; + await expect(policy.enforce(entity)).rejects.toThrow(/ancestor/); }); it('accepts no ancestors', async () => { @@ -105,9 +110,14 @@ describe('GroupV1alpha1Policy', () => { await expect(policy.enforce(entity)).rejects.toThrow(/children/); }); - it('accepts empty children', async () => { + it('rejects empty children', async () => { (entity as any).spec.children = ['']; - await expect(policy.enforce(entity)).resolves.toBe(entity); + await expect(policy.enforce(entity)).rejects.toThrow(/children/); + }); + + it('rejects undefined children', async () => { + (entity as any).spec.children = [undefined]; + await expect(policy.enforce(entity)).rejects.toThrow(/children/); }); it('accepts no children', async () => { @@ -120,9 +130,14 @@ describe('GroupV1alpha1Policy', () => { await expect(policy.enforce(entity)).rejects.toThrow(/descendants/); }); - it('accepts empty descendants', async () => { + it('rejects empty descendants', async () => { (entity as any).spec.descendants = ['']; - await expect(policy.enforce(entity)).resolves.toBe(entity); + await expect(policy.enforce(entity)).rejects.toThrow(/descendants/); + }); + + it('rejects undefined descendants', async () => { + (entity as any).spec.descendants = [undefined]; + await expect(policy.enforce(entity)).rejects.toThrow(/descendants/); }); it('accepts no descendants', async () => { diff --git a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts index 073332d707..f0e91ddce8 100644 --- a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts @@ -30,21 +30,23 @@ const schema = yup.object>({ parent: yup.string().notRequired().min(1), // Use these manual tests because yup .required() requires at least // one element and there is no simple workaround -_- - ancestors: yup.array(yup.string()).test({ + // the cast is there to convince typescript that the array itself is + // required without using .required() + ancestors: yup.array(yup.string().required()).test({ name: 'isDefined', message: 'ancestors must be defined', test: v => Boolean(v), - }), - children: yup.array(yup.string()).test({ + }) as yup.ArraySchema, + children: yup.array(yup.string().required()).test({ name: 'isDefined', message: 'children must be defined', test: v => Boolean(v), - }), - descendants: yup.array(yup.string()).test({ + }) as yup.ArraySchema, + descendants: yup.array(yup.string().required()).test({ name: 'isDefined', message: 'descendants must be defined', test: v => Boolean(v), - }), + }) as yup.ArraySchema, }) .required(), }); diff --git a/packages/catalog-model/src/kinds/LocationEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/LocationEntityV1alpha1.test.ts index a259eaeca8..865ceda53e 100644 --- a/packages/catalog-model/src/kinds/LocationEntityV1alpha1.test.ts +++ b/packages/catalog-model/src/kinds/LocationEntityV1alpha1.test.ts @@ -93,6 +93,11 @@ describe('LocationV1alpha1Policy', () => { await expect(policy.enforce(entity)).resolves.toBe(entity); }); + it('accepts empty targets', async () => { + (entity as any).spec.targets = []; + await expect(policy.enforce(entity)).resolves.toBe(entity); + }); + it('rejects wrong targets', async () => { (entity as any).spec.targets = 7; await expect(policy.enforce(entity)).rejects.toThrow(/targets/); diff --git a/packages/catalog-model/src/kinds/LocationEntityV1alpha1.ts b/packages/catalog-model/src/kinds/LocationEntityV1alpha1.ts index 6d1f1f5515..dc2485f11b 100644 --- a/packages/catalog-model/src/kinds/LocationEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/LocationEntityV1alpha1.ts @@ -28,7 +28,7 @@ const schema = yup.object>({ .object({ type: yup.string().required().min(1), target: yup.string().notRequired().min(1), - targets: yup.array(yup.string()).notRequired(), + targets: yup.array(yup.string().required()).notRequired(), }) .required(), }); diff --git a/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts b/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts index ff4edcc8a2..6086b3776e 100644 --- a/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts @@ -35,11 +35,13 @@ const schema = yup.object>({ .notRequired(), // Use this manual test because yup .required() requires at least one // element and there is no simple workaround -_- - memberOf: yup.array(yup.string()).test({ + // the cast is there to convince typescript that the array itself is + // required without using .required() + memberOf: yup.array(yup.string().required()).test({ name: 'isDefined', message: 'memberOf must be defined', test: v => Boolean(v), - }), + }) as yup.ArraySchema, }) .required(), }); diff --git a/packages/catalog-model/src/location/validation.ts b/packages/catalog-model/src/location/validation.ts index 5fad47bdd0..b1b91edd83 100644 --- a/packages/catalog-model/src/location/validation.ts +++ b/packages/catalog-model/src/location/validation.ts @@ -22,7 +22,8 @@ export const locationSpecSchema = yup type: yup.string().required(), target: yup.string().required(), }) - .noUnknown(); + .noUnknown() + .required(); export const locationSchema = yup .object({ @@ -30,4 +31,5 @@ export const locationSchema = yup type: yup.string().required(), target: yup.string().required(), }) - .noUnknown(); + .noUnknown() + .required(); diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index 0a95dc8c86..eead016c8f 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -33,13 +33,13 @@ "@backstage/config": "^0.1.1-alpha.25", "fs-extra": "^9.0.0", "yaml": "^1.9.2", - "yup": "^0.29.1" + "yup": "^0.29.3" }, "devDependencies": { "@types/jest": "^26.0.7", "@types/mock-fs": "^4.10.0", "@types/node": "^12.0.0", - "@types/yup": "^0.28.2", + "@types/yup": "^0.29.8", "mock-fs": "^4.13.0" }, "files": [ diff --git a/packages/config-loader/src/lib/secrets.ts b/packages/config-loader/src/lib/secrets.ts index 4c3812d527..97f0de2940 100644 --- a/packages/config-loader/src/lib/secrets.ts +++ b/packages/config-loader/src/lib/secrets.ts @@ -58,7 +58,7 @@ const secretLoaderSchemas = { }; // The top-level secret schema, which figures out what type of secret it is. -const secretSchema = yup.lazy(value => { +const secretSchema = yup.lazy(value => { if (typeof value !== 'object' || value === null) { return yup.object().required().label('secret'); } diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 718bb2964c..34f9c7b360 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -43,7 +43,7 @@ "winston": "^3.2.1", "yaml": "^1.9.2", "yn": "^4.0.0", - "yup": "^0.29.1" + "yup": "^0.29.3" }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.25", @@ -53,7 +53,7 @@ "@types/lodash": "^4.14.151", "@types/supertest": "^2.0.8", "@types/uuid": "^8.0.0", - "@types/yup": "^0.28.2", + "@types/yup": "^0.29.8", "jest-fetch-mock": "^3.0.3", "msw": "^0.20.5", "supertest": "^4.0.2" diff --git a/plugins/proxy-backend/package.json b/plugins/proxy-backend/package.json index e96faaec0f..c7125d8b18 100644 --- a/plugins/proxy-backend/package.json +++ b/plugins/proxy-backend/package.json @@ -31,7 +31,7 @@ "winston": "^3.2.1", "yaml": "^1.9.2", "yn": "^4.0.0", - "yup": "^0.29.1" + "yup": "^0.29.3" }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.25", @@ -39,7 +39,7 @@ "@types/node-fetch": "^2.5.7", "@types/supertest": "^2.0.8", "@types/uuid": "^8.0.0", - "@types/yup": "^0.28.2", + "@types/yup": "^0.29.8", "jest-fetch-mock": "^3.0.3", "supertest": "^4.0.2" }, diff --git a/yarn.lock b/yarn.lock index 4d28072207..5007bd0d58 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5820,10 +5820,10 @@ dependencies: "@types/yargs-parser" "*" -"@types/yup@^0.28.2": - version "0.28.3" - resolved "https://registry.npmjs.org/@types/yup/-/yup-0.28.3.tgz#387c35f9a6a36b8d3561f6601eb4e72518b92899" - integrity sha512-0Sir2LxOmupF8HBUvpJoZghLmOqKfZsBk1GYlMwSIccLDDUoN04LHvo0KzDp9qxt1IKf9Fudpj35SrJ8VqetkQ== +"@types/yup@^0.29.8": + version "0.29.8" + resolved "https://registry.npmjs.org/@types/yup/-/yup-0.29.8.tgz#83db15735987db9fe5a38772a0fb9500e3c5bf39" + integrity sha512-MBSp62AjB1KrSOI3gX9GekddXU5YYQAVA93+aSl78biBqoSzxg876aQY2KJK5Gnfbpqq7O2cadVX5kPAtBqIXw== "@types/zen-observable@^0.8.0": version "0.8.0" @@ -24123,7 +24123,7 @@ yn@^4.0.0: resolved "https://registry.npmjs.org/yn/-/yn-4.0.0.tgz#611480051ea43b510da1dfdbe177ed159f00a979" integrity sha512-huWiiCS4TxKc4SfgmTwW1K7JmXPPAmuXWYy4j9qjQo4+27Kni8mGhAAi1cloRWmBe2EqcLgt3IGqQoRL/MtPgg== -yup@^0.29.1: +yup@^0.29.3: version "0.29.3" resolved "https://registry.npmjs.org/yup/-/yup-0.29.3.tgz#69a30fd3f1c19f5d9e31b1cf1c2b851ce8045fea" integrity sha512-RNUGiZ/sQ37CkhzKFoedkeMfJM0vNQyaz+wRZJzxdKE7VfDeVKH8bb4rr7XhRLbHJz5hSjoDNwMEIaKhuMZ8gQ== From 02c60b5f8b9b49511a543dfa80c4003068e1f093 Mon Sep 17 00:00:00 2001 From: Abhishek Jakhar Date: Fri, 16 Oct 2020 09:21:52 +0530 Subject: [PATCH 094/139] fix(tech-radar): add overflowX scroll to the company radar section --- .changeset/slow-experts-peel.md | 5 ++ .../tech-radar/src/components/RadarPage.tsx | 53 +++++++++++-------- 2 files changed, 36 insertions(+), 22 deletions(-) create mode 100644 .changeset/slow-experts-peel.md diff --git a/.changeset/slow-experts-peel.md b/.changeset/slow-experts-peel.md new file mode 100644 index 0000000000..ed9c7af21e --- /dev/null +++ b/.changeset/slow-experts-peel.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-tech-radar': patch +--- + +fix the horizontal scrolling issue in the RadarPage component diff --git a/plugins/tech-radar/src/components/RadarPage.tsx b/plugins/tech-radar/src/components/RadarPage.tsx index 84cfef3213..779d1fa088 100644 --- a/plugins/tech-radar/src/components/RadarPage.tsx +++ b/plugins/tech-radar/src/components/RadarPage.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import { Grid } from '@material-ui/core'; +import { Grid, makeStyles } from '@material-ui/core'; import { Content, ContentHeader, @@ -27,6 +27,12 @@ import { import RadarComponent from '../components/RadarComponent'; import { TechRadarComponentProps } from '../api'; +const useStyles = makeStyles(() => ({ + overflowXScroll: { + overflowX: 'scroll', + }, +})); + export type TechRadarPageProps = TechRadarComponentProps & { title?: string; subtitle?: string; @@ -38,28 +44,31 @@ export const RadarPage = ({ subtitle, pageTitle, ...props -}: TechRadarPageProps): JSX.Element => ( - -
- - -
- - - - This is used for visualizing the official guidelines of different - areas of software development such as languages, frameworks, - infrastructure and processes. - - - - - +}: TechRadarPageProps): JSX.Element => { + const classes = useStyles(); + return ( + +
+ + +
+ + + + This is used for visualizing the official guidelines of different + areas of software development such as languages, frameworks, + infrastructure and processes. + + + + + + -
-
-
-); + + + ); +}; RadarPage.defaultProps = { title: 'Tech Radar', From 0d53c752ace139be551a42a2b14728243e456a96 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 16 Oct 2020 13:59:00 +0200 Subject: [PATCH 095/139] cli: cleanup and fix removePlugin tests --- .../remove-plugin/removePlugin.test.ts | 33 +++++++++++-------- .../commands/remove-plugin/removePlugin.ts | 1 - 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/packages/cli/src/commands/remove-plugin/removePlugin.test.ts b/packages/cli/src/commands/remove-plugin/removePlugin.test.ts index 3ffa8d63cf..a5e2af5974 100644 --- a/packages/cli/src/commands/remove-plugin/removePlugin.test.ts +++ b/packages/cli/src/commands/remove-plugin/removePlugin.test.ts @@ -32,7 +32,6 @@ import { removePluginFromCodeOwners, } from './removePlugin'; -// Some constant variables const BACKSTAGE = `@backstage`; const testPluginName = 'yarn-test-package'; const testPluginPackage = `${BACKSTAGE}/plugin-${testPluginName}`; @@ -68,8 +67,8 @@ const createTestPluginFile = async ( .split('-') .map(name => capitalize(name)) .join(''); - const exportStatement = `export { default as ${pluginNameCapitalized}} from @backstage/plugin-${testPluginName}`; - addExportStatement(testFilePath, exportStatement); + const exportStatement = `export { plugin as ${pluginNameCapitalized}} from @backstage/plugin-${testPluginName}`; + await addExportStatement(testFilePath, exportStatement); }; const mkTestPluginDir = (testDirPath: string) => { @@ -78,15 +77,21 @@ const mkTestPluginDir = (testDirPath: string) => { fse.createFileSync(path.join(testDirPath, `testFile${i}.ts`)); }; -beforeAll(() => { - // Create temporary directory for all tests - createTemporaryPluginFolder(tempDir); -}); - describe('removePlugin', () => { + beforeAll(() => { + // Create temporary directory for all tests + createTemporaryPluginFolder(tempDir); + }); + + afterAll(() => { + // Remove temporary directory + fse.removeSync(tempDir); + }); + describe('Remove Plugin Dependencies', () => { const appPath = paths.resolveTargetRoot('packages', 'app'); const githubDir = paths.resolveTargetRoot('.github'); + it('removes plugin references from /packages/app/package.json', async () => { // Set up test const packageFilePath = path.join(appPath, 'package.json'); @@ -105,7 +110,8 @@ describe('removePlugin', () => { fse.removeSync(testFilePath); } }); - it('removes plugin exports from /packages/app/src/packacge.json', async () => { + + it('removes plugin exports from /packages/app/src/package.json', async () => { const testFilePath = path.join(tempDir, 'test.ts'); const pluginsFilePaths = path.join(appPath, 'src', 'plugins.ts'); createTestPluginFile(testFilePath, pluginsFilePaths); @@ -122,6 +128,7 @@ describe('removePlugin', () => { fse.removeSync(testFilePath); } }); + it('removes codeOwners references', async () => { const testFilePath = path.join(tempDir, 'test'); const codeownersPath = path.join(githubDir, 'CODEOWNERS'); @@ -144,12 +151,14 @@ describe('removePlugin', () => { } }); }); + describe('Remove files', () => { const testDirPath = path.join( paths.resolveTargetRoot(), 'plugins', testPluginName, ); + describe('Removes Plugin Directory', () => { it('removes plugin directory from /plugins', async () => { try { @@ -162,6 +171,7 @@ describe('removePlugin', () => { } }); }); + describe('Removes System Link', () => { it('removes system link from @backstage', async () => { const scopedDir = paths.resolveTargetRoot('node_modules', '@backstage'); @@ -183,8 +193,3 @@ describe('removePlugin', () => { }); }); }); - -afterAll(() => { - // Remove temporary directory - fse.removeSync(tempDir); -}); diff --git a/packages/cli/src/commands/remove-plugin/removePlugin.ts b/packages/cli/src/commands/remove-plugin/removePlugin.ts index 1395384022..643bd599d7 100644 --- a/packages/cli/src/commands/remove-plugin/removePlugin.ts +++ b/packages/cli/src/commands/remove-plugin/removePlugin.ts @@ -20,7 +20,6 @@ import inquirer, { Answers, Question } from 'inquirer'; import { getCodeownersFilePath } from '../../lib/codeowners'; import { paths } from '../../lib/paths'; import { Task } from '../../lib/tasks'; -// import os from 'os'; const BACKSTAGE = '@backstage'; From d47e2287b50a781014c5577b90ea38ad245ca110 Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Fri, 16 Oct 2020 09:54:28 -0400 Subject: [PATCH 096/139] compare -> default; deprecate dailyCost as metric; cleanup --- app-config.yaml | 4 +- .../ExampleCostInsightsClient.ts | 5 +- plugins/cost-insights/README.md | 7 +- .../cost-insights/src/api/CostInsightsApi.ts | 5 +- .../CostInsightsNavigation.test.tsx | 1 + .../CostInsightsPage/CostInsightsPage.tsx | 25 +++--- .../CostOverviewCard/CostOverviewCard.tsx | 37 +++++---- .../CostOverviewCard/CostOverviewChart.tsx | 83 +++++++++---------- .../MetricSelect/MetricSelect.test.tsx | 26 ++---- .../components/MetricSelect/MetricSelect.tsx | 54 ++++++------ plugins/cost-insights/src/hooks/useConfig.tsx | 49 ++++------- .../cost-insights/src/hooks/useFilters.tsx | 7 +- .../src/types/ChangeStatistic.ts | 10 ++- plugins/cost-insights/src/types/Cost.ts | 4 - plugins/cost-insights/src/types/Metric.ts | 6 +- plugins/cost-insights/src/types/MetricData.ts | 25 ++++++ plugins/cost-insights/src/types/index.ts | 1 + plugins/cost-insights/src/utils/config.ts | 32 +++++++ 18 files changed, 196 insertions(+), 185 deletions(-) create mode 100644 plugins/cost-insights/src/types/MetricData.ts create mode 100644 plugins/cost-insights/src/utils/config.ts diff --git a/app-config.yaml b/app-config.yaml index d1414582ed..966f067ff5 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -236,11 +236,9 @@ costInsights: name: Big Query icon: search metrics: - dailyCost: - name: Pied Piper's Daily Cost - compare: DAU DAU: name: Daily Active Users + default: true MSC: name: Monthly Subscribers homepage: diff --git a/packages/app/src/plugins/cost-insights/ExampleCostInsightsClient.ts b/packages/app/src/plugins/cost-insights/ExampleCostInsightsClient.ts index fc2249fadc..0b275baf74 100644 --- a/packages/app/src/plugins/cost-insights/ExampleCostInsightsClient.ts +++ b/packages/app/src/plugins/cost-insights/ExampleCostInsightsClient.ts @@ -117,10 +117,7 @@ export class ExampleCostInsightsClient implements CostInsightsApi { return projects; } - async getMetricData( - metric: string | null, - intervals: string, - ): Promise { + async getMetricData(metric: string, intervals: string): Promise { const aggregation = aggregationFor( durationOf(intervals), 100_000, diff --git a/plugins/cost-insights/README.md b/plugins/cost-insights/README.md index 26f9d9371a..3028d3a09c 100644 --- a/plugins/cost-insights/README.md +++ b/plugins/cost-insights/README.md @@ -79,9 +79,9 @@ costInsights: ### Metrics (Optional) -In the `Cost Overview` panel, users can choose from a dropdown of business metrics to see costs as they relate to a metric, such as daily active users. Metrics must be defined as keys on the `metrics` field. A user-friendly name is **required**. Metrics will be provided to the `getDailyCost` and `getProjectCosts` API methods via the `metric` parameter. +In the `Cost Overview` panel, users can choose from a dropdown of business metrics to see costs as they relate to a metric, such as daily active users. Metrics must be defined as keys on the `metrics` field. A user-friendly name is **required**. Metrics will be provided to the `getMetricData` API method via the `metric` parameter. -**Note:** Cost Insights displays daily cost without a metric by default. The dropdown text for this default can be overridden by assigning it a value on the `dailyCost` field. +An optional `default` field can be set to `true` to set the default comparison metric to daily cost in the Cost Overview panel. ```yaml ## ./app-config.yaml @@ -95,10 +95,9 @@ costInsights: name: Some Other Cloud Product icon: data metrics: - dailyCost: - name: Earth Rotation metricA: name: Metric A ## required + default: true metricB: name: Metric B metricC: diff --git a/plugins/cost-insights/src/api/CostInsightsApi.ts b/plugins/cost-insights/src/api/CostInsightsApi.ts index 52b3f335d9..3136fb4c27 100644 --- a/plugins/cost-insights/src/api/CostInsightsApi.ts +++ b/plugins/cost-insights/src/api/CostInsightsApi.ts @@ -87,7 +87,7 @@ export type CostInsightsApi = { * @param intervals An ISO 8601 repeating interval string, such as R2/P1M/2020-09-01 * https://en.wikipedia.org/wiki/ISO_8601#Repeating_intervals */ - getMetricData(metric: string | null, intervals: string): Promise; + getMetricData(metric: string, intervals: string): Promise; /** * Get cost aggregations for a particular cloud product and interval timeframe. This includes @@ -104,10 +104,9 @@ export type CostInsightsApi = { * @param product The product from the cost-insights configuration in app-config.yaml * @param group * @param duration A time duration, such as P1M. See the Duration type for a detailed explanation - * of how the durations are interpreted in Cost Insights. + * of how the durations are interpreted in Cost Insights. * @param project (optional) The project id from getGroupProjects or query parameters */ - getProductInsights( product: string, group: string, diff --git a/plugins/cost-insights/src/components/CostInsightsNavigation/CostInsightsNavigation.test.tsx b/plugins/cost-insights/src/components/CostInsightsNavigation/CostInsightsNavigation.test.tsx index 3ec2828029..7ca4958c8e 100644 --- a/plugins/cost-insights/src/components/CostInsightsNavigation/CostInsightsNavigation.test.tsx +++ b/plugins/cost-insights/src/components/CostInsightsNavigation/CostInsightsNavigation.test.tsx @@ -40,6 +40,7 @@ const mockMetrics: Metric[] = [ { kind: 'some-metric', name: 'Some Metric', + default: false, }, ]; diff --git a/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPage.tsx b/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPage.tsx index 9bc396537b..20f965f1c7 100644 --- a/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPage.tsx +++ b/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPage.tsx @@ -101,34 +101,26 @@ const CostInsightsPage = () => { try { if (pageFilters.group) { dispatchLoadingInsights(true); + const intervals = intervalsOf(pageFilters.duration); const [ fetchedProjects, fetchedAlerts, fetchedMetricData, - fetchedCosts, + fetchedDailyCost, ] = await Promise.all([ client.getGroupProjects(pageFilters.group), client.getAlerts(pageFilters.group), pageFilters.metric - ? client.getMetricData( - pageFilters.metric, - intervalsOf(pageFilters.duration), - ) + ? client.getMetricData(pageFilters.metric, intervals) : null, pageFilters.project - ? client.getProjectDailyCost( - pageFilters.project, - intervalsOf(pageFilters.duration), - ) - : client.getGroupDailyCost( - pageFilters.group, - intervalsOf(pageFilters.duration), - ), + ? client.getProjectDailyCost(pageFilters.project, intervals) + : client.getGroupDailyCost(pageFilters.group, intervals), ]); setProjects(fetchedProjects); setAlerts(fetchedAlerts); setMetricData(fetchedMetricData); - setDailyCost(fetchedCosts); + setDailyCost(fetchedDailyCost); } else { dispatchLoadingNone(loadingActions); } @@ -267,7 +259,10 @@ const CostInsightsPage = () => { {!!dailyCost.aggregation.length && ( - + )} diff --git a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx index daffe335c5..a08e7fc789 100644 --- a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx +++ b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx @@ -35,10 +35,14 @@ import { } from '../../types'; export type CostOverviewCardProps = { - data: [Cost, MetricData | null]; + dailyCostData: Cost; + metricData: MetricData | null; }; -const CostOverviewCard = ({ data }: CostOverviewCardProps) => { +const CostOverviewCard = ({ + dailyCostData, + metricData, +}: CostOverviewCardProps) => { const theme = useTheme(); const config = useConfig(); const { ScrollAnchor } = useScroll(DefaultNavigation.CostOverviewCard); @@ -46,10 +50,12 @@ const CostOverviewCard = ({ data }: CostOverviewCardProps) => { mapFiltersToProps, ); - // There should always be a daily cost metric but a comparison metric is optional. - const dailyCost = findAlways(config.metrics, m => m.kind === null); - const metric = config.metrics.find(m => m.kind === filters.metric); - const comparedChange = data[1] ? getComparedChange(data[0], data[1]) : null; + const metric = filters.metric + ? findAlways(config.metrics, m => m.kind === filters.metric) + : null; + const comparedChange = metricData + ? getComparedChange(dailyCostData, metricData) + : null; return ( @@ -62,21 +68,18 @@ const CostOverviewCard = ({ data }: CostOverviewCardProps) => { - - {formatPercent(data[0].change.ratio)} + + {formatPercent(dailyCostData.change.ratio)} - {metric && metric.kind && data[1] && comparedChange && ( + {metric && metricData && comparedChange && ( <> - {formatPercent(data[1].change.ratio)} + {formatPercent(metricData.change.ratio)} { )} diff --git a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewChart.tsx b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewChart.tsx index 40cefbb589..9612589542 100644 --- a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewChart.tsx +++ b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewChart.tsx @@ -16,7 +16,6 @@ import React from 'react'; import { useTheme } from '@material-ui/core'; import { - AxisDomain, ComposedChart, XAxis, YAxis, @@ -30,6 +29,7 @@ import { import { ChartData, Cost, + Maybe, Metric, MetricData, CostInsightsTheme, @@ -40,71 +40,69 @@ import { } from '../../utils/graphs'; import CostOverviewTooltip from './CostOverviewTooltip'; import { TooltipItemProps } from '../Tooltip'; -import { NULL_METRIC } from '../../hooks/useConfig'; import { useCostOverviewStyles as useStyles } from '../../utils/styles'; import { groupByDate, toDataMax, trendFrom } from '../../utils/charts'; import { aggregationSort } from '../../utils/sort'; type CostOverviewChartProps = { - data: [Cost, MetricData | null]; - name: string; - compare?: Metric; + metric: Maybe; + metricData: Maybe; + dailyCostData: Cost; responsive?: boolean; }; const CostOverviewChart = ({ - data, - name, - compare, + dailyCostData, + metric, + metricData, responsive = true, }: CostOverviewChartProps) => { const theme = useTheme(); const styles = useStyles(theme); - const { dailyCost, metric } = { + const data = { dailyCost: { - id: NULL_METRIC, - name: name, - format: 'number', - data: data[0], + dataKey: 'dailyCost', + name: `Daily Cost`, + format: 'currency', + data: dailyCostData, }, metric: { - id: compare?.kind ?? 'Unknown', - name: compare?.name ?? 'Unknown', - format: data[1]?.format ?? 'number', - data: data[1], + dataKey: metric?.kind ?? 'Unknown', + name: metric?.name ?? 'Unknown', + format: metricData?.format ?? 'number', + data: metricData, }, }; - const metricsByDate = metric.data - ? metric.data.aggregation.reduce(groupByDate, {}) + const metricsByDate = data.metric.data + ? data.metric.data.aggregation.reduce(groupByDate, {}) : {}; - const chartData: ChartData[] = dailyCost - .data!.aggregation.slice() + const chartData: ChartData[] = data.dailyCost.data.aggregation + .slice() .sort(aggregationSort) .map(entry => ({ date: Date.parse(entry.date), - trend: trendFrom(dailyCost.data!.trendline, Date.parse(entry.date)), + trend: trendFrom(data.dailyCost.data.trendline, Date.parse(entry.date)), dailyCost: entry.amount, - ...(metric && metric.data - ? { [metric.id]: metricsByDate[`${entry.date}`] } + ...(metric && data.metric.data + ? { [data.metric.dataKey]: metricsByDate[`${entry.date}`] } : {}), })); - const metricDataMax: AxisDomain = metric - ? toDataMax(metric.id, chartData) - : 'dataMax'; - function tooltipFormatter(payload: TooltipPayload): TooltipItemProps { return { - label: payload.dataKey === dailyCost.id ? dailyCost.name : metric.name, + label: + payload.dataKey === data.dailyCost.dataKey + ? data.dailyCost.name + : data.metric.name, value: - payload.dataKey === dailyCost.id - ? formatGraphValue(payload.value as number, dailyCost.format) - : formatGraphValue(payload.value as number, metric.format), + payload.dataKey === data.dailyCost.dataKey + ? formatGraphValue(payload.value as number, data.dailyCost.format) + : formatGraphValue(payload.value as number, data.metric.format), fill: - payload.dataKey === dailyCost.id + payload.dataKey === data.dailyCost.dataKey ? theme.palette.blue : theme.palette.magenta, }; @@ -131,23 +129,23 @@ const CostOverviewChart = ({ tick={{ fill: styles.axis.fill }} tickFormatter={formatGraphValue} width={styles.yAxis.width} - yAxisId={dailyCost.id} + yAxisId={data.dailyCost.dataKey} /> {metric && ( 0, metricDataMax]} + domain={[() => 0, toDataMax(data.metric.dataKey, chartData)]} width={styles.yAxis.width} - yAxisId={metric.id} + yAxisId={data.metric.dataKey} /> )} {metric && ( )} } diff --git a/plugins/cost-insights/src/components/MetricSelect/MetricSelect.test.tsx b/plugins/cost-insights/src/components/MetricSelect/MetricSelect.test.tsx index 67d1ec3ad3..e78fd77b1a 100644 --- a/plugins/cost-insights/src/components/MetricSelect/MetricSelect.test.tsx +++ b/plugins/cost-insights/src/components/MetricSelect/MetricSelect.test.tsx @@ -23,7 +23,7 @@ describe('', () => { it('should display a metric', async () => { const mockProps: MetricSelectProps = { metric: 'test', - metrics: [{ kind: 'test', name: 'some-name' }], + metrics: [{ kind: 'test', name: 'some-name', default: false }], onSelect: jest.fn(), }; const { getByText } = await renderInTestApp( @@ -32,25 +32,12 @@ describe('', () => { expect(getByText(/some-name/)).toBeInTheDocument(); }); - it('should display a null metric', async () => { - const mockProps: MetricSelectProps = { - metric: null, - metrics: [{ kind: null, name: 'billie-nullish' }], - onSelect: jest.fn(), - }; - const { getByText } = await renderInTestApp( - , - ); - expect(getByText(/billie-nullish/)).toBeInTheDocument(); - }); - it('should display all metrics', async () => { const mockProps: MetricSelectProps = { metric: null, metrics: [ - { kind: null, name: 'billie-nullish' }, - { kind: 'MAU1M', name: 'Cost Per Million MAU' }, - { kind: 'my-cool-metric', name: 'metric-mcmetric-face' }, + { kind: 'DAU', name: 'Daily Active Users', default: true }, + { kind: 'MSC', name: 'Monthly Subscribers', default: false }, ], onSelect: jest.fn(), }; @@ -61,11 +48,10 @@ describe('', () => { UserEvent.click(button); - await waitFor(() => getAllByText(/billie-nullish/)); + await waitFor(() => getAllByText(/None/)); // The active metric should display in the popver list and in the input - expect(getAllByText(/billie-nullish/).length).toBe(2); - expect(getByText(/Cost Per Million MAU/)).toBeInTheDocument(); - expect(getByText(/metric-mcmetric-face/)).toBeInTheDocument(); + expect(getByText(/Daily Active Users/)).toBeInTheDocument(); + expect(getByText(/Monthly Subscribers/)).toBeInTheDocument(); }); }); diff --git a/plugins/cost-insights/src/components/MetricSelect/MetricSelect.tsx b/plugins/cost-insights/src/components/MetricSelect/MetricSelect.tsx index 1d3af911d4..474d8622ea 100644 --- a/plugins/cost-insights/src/components/MetricSelect/MetricSelect.tsx +++ b/plugins/cost-insights/src/components/MetricSelect/MetricSelect.tsx @@ -15,10 +15,9 @@ */ import React from 'react'; -import { Select, MenuItem } from '@material-ui/core'; -import { Maybe, Metric, findAlways } from '../../types'; +import { InputLabel, FormControl, Select, MenuItem } from '@material-ui/core'; +import { Maybe, Metric } from '../../types'; import { useSelectStyles as useStyles } from '../../utils/styles'; -import { NULL_METRIC } from '../../hooks/useConfig'; export type MetricSelectProps = { metric: Maybe; @@ -29,38 +28,37 @@ export type MetricSelectProps = { const MetricSelect = ({ metric, metrics, onSelect }: MetricSelectProps) => { const classes = useStyles(); - const handleOnChange = (e: React.ChangeEvent<{ value: unknown }>) => { - if (e.target.value === NULL_METRIC) { + function onChange(e: React.ChangeEvent<{ value: unknown }>) { + if (e.target.value === 'none') { onSelect(null); } else { onSelect(e.target.value as string); } - }; - - const renderValue = (value: unknown) => { - const kind = (value === NULL_METRIC ? null : value) as Maybe; - const { name } = findAlways(metrics, m => m.kind === kind); - return {name}; - }; + } return ( - + + None - ))} - + {metrics.map((m: Metric) => ( + + {m.name} + + ))} + + ); }; diff --git a/plugins/cost-insights/src/hooks/useConfig.tsx b/plugins/cost-insights/src/hooks/useConfig.tsx index 830cafc4f1..6cbdd1a2ef 100644 --- a/plugins/cost-insights/src/hooks/useConfig.tsx +++ b/plugins/cost-insights/src/hooks/useConfig.tsx @@ -25,12 +25,10 @@ import { useApi, configApiRef } from '@backstage/core'; import { Config as BackstageConfig } from '@backstage/config'; import { Currency, defaultCurrencies, Product, Icon, Metric } from '../types'; import { getIcon } from '../utils/navigation'; - -export const NULL_METRIC = 'dailyCost'; -export const NULL_METRIC_NAME = 'Daily Cost'; +import { validateMetrics } from '../utils/config'; /* - * Config schema 2020-09-28 + * Config schema 2020-10-15 * * costInsights: * engineerCost: 200000 @@ -42,11 +40,9 @@ export const NULL_METRIC_NAME = 'Daily Cost'; * name: Product B * icon: data * metrics: - * dailyCost: - * name: Daily Cost - * compare: metricA * metricA: * name: Metric A + * default: true * metricB: * name: Metric B */ @@ -64,7 +60,7 @@ export const ConfigContext = createContext( ); const defaultState: ConfigContextProps = { - metrics: [{ kind: null, name: NULL_METRIC_NAME }], + metrics: [], products: [], icons: [], engineerCost: 0, @@ -90,9 +86,9 @@ export const ConfigProvider = ({ children }: { children: ReactNode }) => { const metrics = c.getOptionalConfig('costInsights.metrics'); if (metrics) { return metrics.keys().map(key => ({ - kind: key === NULL_METRIC ? null : key, + kind: key, name: metrics.getString(`${key}.name`), - compare: metrics.getOptionalString(`${key}.compare`), + default: metrics.getOptionalBoolean(`${key}.default`) ?? false, })); } @@ -119,23 +115,15 @@ export const ConfigProvider = ({ children }: { children: ReactNode }) => { const engineerCost = getEngineerCost(); const icons = getIcons(); - if (metrics.find((m: Metric) => m.kind === null)) { - setConfig(prevState => ({ - ...prevState, - metrics, - products, - engineerCost, - icons, - })); - } else { - setConfig(prevState => ({ - ...prevState, - metrics: [...prevState.metrics, ...metrics], - products, - engineerCost, - icons, - })); - } + validateMetrics(metrics); + + setConfig(prevState => ({ + ...prevState, + metrics, + products, + engineerCost, + icons, + })); setLoading(false); } @@ -154,12 +142,7 @@ export const ConfigProvider = ({ children }: { children: ReactNode }) => { export function useConfig(): ConfigContextProps { const config = useContext(ConfigContext); - - if (!config) { - assertNever(); - } - - return config; + return config ? config : assertNever(); } function assertNever(): never { diff --git a/plugins/cost-insights/src/hooks/useFilters.tsx b/plugins/cost-insights/src/hooks/useFilters.tsx index 0ad4c24267..7dcf8ddb26 100644 --- a/plugins/cost-insights/src/hooks/useFilters.tsx +++ b/plugins/cost-insights/src/hooks/useFilters.tsx @@ -75,9 +75,8 @@ export const FilterProvider = ({ children }: FilterProviderProps) => { const queryParams = useQueryParams(); const qsRef = useRef(''); const groups = useGroups(); - const { products } = useConfig(); - const defaultProductFilters = products.map(product => ({ + const defaultProductFilters = config.products.map(product => ({ productType: product.kind, duration: Duration.P1M, })); @@ -103,8 +102,8 @@ export const FilterProvider = ({ children }: FilterProviderProps) => { // TODO: Figure out why pageFilters doesn't get updated by the above when groups are loaded. useEffect(() => { const initialState = getInitialPageState(groups, queryParams.pageFilters); - const compared = config.metrics.find(m => m.kind === null)?.compare; - setPageFilters({ ...initialState, metric: compared || null }); + const defaultMetric = config.metrics.find(m => m.default); + setPageFilters({ ...initialState, metric: defaultMetric?.kind ?? null }); }, [groups]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { diff --git a/plugins/cost-insights/src/types/ChangeStatistic.ts b/plugins/cost-insights/src/types/ChangeStatistic.ts index cf7310ef97..cdd2e02da6 100644 --- a/plugins/cost-insights/src/types/ChangeStatistic.ts +++ b/plugins/cost-insights/src/types/ChangeStatistic.ts @@ -15,6 +15,7 @@ */ import { Cost } from './Cost'; +import { MetricData } from './MetricData'; import { aggregationSort } from '../utils/sort'; export interface ChangeStatistic { @@ -51,9 +52,12 @@ export function growthOf(amount: number, ratio: number) { } // Used by for displaying engineer totals -export function getComparedChange(a: Cost, b: Cost): ChangeStatistic { - const ratio = a.change.ratio - b.change.ratio; - const amount = a.aggregation.slice().sort(aggregationSort)[0].amount; +export function getComparedChange( + dailyCost: Cost, + metricData: MetricData, +): ChangeStatistic { + const ratio = dailyCost.change.ratio - metricData.change.ratio; + const amount = dailyCost.aggregation.slice().sort(aggregationSort)[0].amount; return { ratio: ratio, amount: amount * ratio, diff --git a/plugins/cost-insights/src/types/Cost.ts b/plugins/cost-insights/src/types/Cost.ts index 8a9ce3c69b..84a8bfae51 100644 --- a/plugins/cost-insights/src/types/Cost.ts +++ b/plugins/cost-insights/src/types/Cost.ts @@ -24,7 +24,3 @@ export interface Cost { change: ChangeStatistic; trendline: Trendline; } - -export interface MetricData extends Cost { - format: 'number' | 'currency'; -} diff --git a/plugins/cost-insights/src/types/Metric.ts b/plugins/cost-insights/src/types/Metric.ts index 4fa9cd189b..b0a0c9cc76 100644 --- a/plugins/cost-insights/src/types/Metric.ts +++ b/plugins/cost-insights/src/types/Metric.ts @@ -14,10 +14,8 @@ * limitations under the License. */ -import { Maybe } from '../types'; - export type Metric = { - kind: Maybe; + kind: string; name: string; - compare?: string; + default: boolean; }; diff --git a/plugins/cost-insights/src/types/MetricData.ts b/plugins/cost-insights/src/types/MetricData.ts new file mode 100644 index 0000000000..bd3bdde735 --- /dev/null +++ b/plugins/cost-insights/src/types/MetricData.ts @@ -0,0 +1,25 @@ +/* + * 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 { DateAggregation } from './DateAggregation'; +import { ChangeStatistic } from './ChangeStatistic'; + +export interface MetricData { + id: string; + format: 'number' | 'currency'; + aggregation: DateAggregation[]; + change: ChangeStatistic; +} diff --git a/plugins/cost-insights/src/types/index.ts b/plugins/cost-insights/src/types/index.ts index 8523c7db7b..398110ec80 100644 --- a/plugins/cost-insights/src/types/index.ts +++ b/plugins/cost-insights/src/types/index.ts @@ -27,6 +27,7 @@ export * from './Filters'; export * from './Group'; export * from './Loading'; export * from './Maybe'; +export * from './MetricData'; export * from './Metric'; export * from './Product'; export * from './Project'; diff --git a/plugins/cost-insights/src/utils/config.ts b/plugins/cost-insights/src/utils/config.ts new file mode 100644 index 0000000000..c2c2c066d9 --- /dev/null +++ b/plugins/cost-insights/src/utils/config.ts @@ -0,0 +1,32 @@ +/* + * 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 { Metric } from '../types'; + +export function validateMetrics(metrics: Metric[]) { + let defaultMetric = null; + for (const metric of metrics) { + if (metric.default) { + if (defaultMetric) { + throw new Error( + `Cannot set default for multiple metrics: Received: ${defaultMetric.kind} and ${metric.kind}`, + ); + } else { + defaultMetric = metric; + } + } + } +} From 71de0d8aee494c81c8cfe4ab623fd6fcf12a5f74 Mon Sep 17 00:00:00 2001 From: David Tuite Date: Fri, 16 Oct 2020 13:39:15 +0100 Subject: [PATCH 097/139] Add Google Analytics Specity a Google Analytics Tracking ID in the `app-config.yaml` to enable Google Analytics tracking. ``` app: title: baseUrl: googleAnalyticsTrackingId: UA-000000-0 ``` If the `googleAnalyticsTrackingId` is not supplied, no tracking script is added to the application and no tracking can occur. Includes Helm chart changes to allow overriding the GA Tracking ID. --- app-config.yaml | 1 + .../templates/backstage-app-config.yaml | 1 + contrib/chart/backstage/values.yaml | 1 + packages/app/public/index.html | 17 +++++++++++++++++ packages/cli/src/lib/bundler/config.ts | 3 +++ 5 files changed, 23 insertions(+) diff --git a/app-config.yaml b/app-config.yaml index 12fe211a4b..c48265df9e 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -1,6 +1,7 @@ app: title: Backstage Example App baseUrl: http://localhost:7000 + googleAnalyticsTrackingId: # UA-000000-0 backend: baseUrl: http://localhost:7000 diff --git a/contrib/chart/backstage/templates/backstage-app-config.yaml b/contrib/chart/backstage/templates/backstage-app-config.yaml index 061fc3285b..f06e47feab 100644 --- a/contrib/chart/backstage/templates/backstage-app-config.yaml +++ b/contrib/chart/backstage/templates/backstage-app-config.yaml @@ -13,6 +13,7 @@ metadata: data: APP_CONFIG_app_baseUrl: {{ .Values.appConfig.app.baseUrl | quote | quote }} APP_CONFIG_app_title: {{ .Values.appConfig.app.title | quote | quote }} + APP_CONFIG_app_googleAnalyticsTrackingId: {{ .Values.appConfig.app.googleAnalyticsTrackingId | quote | quote }} APP_CONFIG_backend_baseUrl: {{ .Values.appConfig.backend.baseUrl | quote | quote }} APP_CONFIG_backend_cors_origin: {{ .Values.appConfig.backend.cors.origin | quote | quote }} APP_CONFIG_techdocs_storageUrl: {{ .Values.appConfig.techdocs.storageUrl | quote | quote }} diff --git a/contrib/chart/backstage/values.yaml b/contrib/chart/backstage/values.yaml index d51f1016d9..f92e8aad47 100644 --- a/contrib/chart/backstage/values.yaml +++ b/contrib/chart/backstage/values.yaml @@ -86,6 +86,7 @@ appConfig: app: baseUrl: https://demo.example.com title: Backstage + googleAnalyticsTrackingId: backend: baseUrl: https://demo.example.com listen: diff --git a/packages/app/public/index.html b/packages/app/public/index.html index ea9208ca57..77e5c01e19 100644 --- a/packages/app/public/index.html +++ b/packages/app/public/index.html @@ -48,6 +48,23 @@ } <%= app.title %> + + <% if (app.googleAnalyticsTrackingId && typeof app.googleAnalyticsTrackingId + === 'string') { %> + + + <% } %> diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index 00a555eebe..34b9f498ad 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -111,6 +111,9 @@ export async function createConfig( app: { title: options.config.getString('app.title'), baseUrl: validBaseUrl.href, + googleAnalyticsTrackingId: options.config.getOptionalString( + 'app.googleAnalyticsTrackingId', + ), }, }, }), From 1e278d09ad60aad36c4ced7e8cdb6aab43c837ba Mon Sep 17 00:00:00 2001 From: David Tuite Date: Fri, 16 Oct 2020 14:05:59 +0100 Subject: [PATCH 098/139] Add dist-types to .prettierignore --- .prettierignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.prettierignore b/.prettierignore index 4b1acbb594..c4e675a5d7 100644 --- a/.prettierignore +++ b/.prettierignore @@ -6,3 +6,4 @@ coverage templates plugins/scaffolder-backend/sample-templates .vscode +dist-types From 2e1a0b8c99afa8557078afc8ab7102430513e6f7 Mon Sep 17 00:00:00 2001 From: Dominik Henneke Date: Fri, 16 Oct 2020 16:02:08 +0200 Subject: [PATCH 099/139] feat(api-docs): show a custom error page when no apis are implemented by a component (#2933) --- .../MissingImplementsApisEmptyState.tsx | 82 +++++++++++++++++++ .../MissingImplementsApisEmptyState/index.ts | 17 ++++ plugins/api-docs/src/catalog/Router.tsx | 7 +- 3 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 plugins/api-docs/src/catalog/MissingImplementsApisEmptyState/MissingImplementsApisEmptyState.tsx create mode 100644 plugins/api-docs/src/catalog/MissingImplementsApisEmptyState/index.ts diff --git a/plugins/api-docs/src/catalog/MissingImplementsApisEmptyState/MissingImplementsApisEmptyState.tsx b/plugins/api-docs/src/catalog/MissingImplementsApisEmptyState/MissingImplementsApisEmptyState.tsx new file mode 100644 index 0000000000..9160aca514 --- /dev/null +++ b/plugins/api-docs/src/catalog/MissingImplementsApisEmptyState/MissingImplementsApisEmptyState.tsx @@ -0,0 +1,82 @@ +/* + * 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 { Button, makeStyles, Typography } from '@material-ui/core'; +import { BackstageTheme } from '@backstage/theme'; +import { CodeSnippet, EmptyState } from '@backstage/core'; + +const COMPONENT_YAML = `# Example +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: example +spec: + type: service + lifecycle: production + owner: guest + implementsApis: + - example-api +`; + +const useStyles = makeStyles(theme => ({ + code: { + borderRadius: 6, + margin: `${theme.spacing(2)}px 0px`, + background: theme.palette.type === 'dark' ? '#444' : '#fff', + }, +})); + +export const MissingImplementsApisEmptyState = () => { + const classes = useStyles(); + return ( + + Components can implement APIs that are displayed on this page. You + need to fill the implementsApis field to enable this + tool. + + } + action={ + <> + + Link an API to your component as shown in the highlighted example + below: + +
+ +
+ + + } + /> + ); +}; diff --git a/plugins/api-docs/src/catalog/MissingImplementsApisEmptyState/index.ts b/plugins/api-docs/src/catalog/MissingImplementsApisEmptyState/index.ts new file mode 100644 index 0000000000..1b7d35c0a2 --- /dev/null +++ b/plugins/api-docs/src/catalog/MissingImplementsApisEmptyState/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 { MissingImplementsApisEmptyState } from './MissingImplementsApisEmptyState'; diff --git a/plugins/api-docs/src/catalog/Router.tsx b/plugins/api-docs/src/catalog/Router.tsx index 71640954b3..d3fe7f7553 100644 --- a/plugins/api-docs/src/catalog/Router.tsx +++ b/plugins/api-docs/src/catalog/Router.tsx @@ -17,20 +17,17 @@ import React from 'react'; import { Entity } from '@backstage/catalog-model'; import { Route, Routes } from 'react-router'; -import { WarningPanel } from '@backstage/core'; import { catalogRoute } from '../routes'; import { EntityPageApi } from './EntityPageApi'; +import { MissingImplementsApisEmptyState } from './MissingImplementsApisEmptyState'; const isPluginApplicableToEntity = (entity: Entity) => { return ((entity.spec?.implementsApis as string[]) || []).length > 0; }; export const Router = ({ entity }: { entity: Entity }) => - // TODO(shmidt-i): move warning to a separate standardized component !isPluginApplicableToEntity(entity) ? ( - - The entity doesn't implement any APIs. - + ) : ( Date: Fri, 16 Oct 2020 16:09:05 +0200 Subject: [PATCH 100/139] chore(cloudbuild): publish the cloudbuild plugin --- plugins/cloudbuild/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index 6c3ea2ff9a..d36b661664 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -4,7 +4,6 @@ "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", - "private": true, "publishConfig": { "access": "public", "main": "dist/index.esm.js", From a508287b9c36ac0d6664d7837b7aeaa94d42418e Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Fri, 16 Oct 2020 10:30:48 -0400 Subject: [PATCH 101/139] throw errors for multiple defaults --- plugins/cost-insights/src/utils/config.ts | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/plugins/cost-insights/src/utils/config.ts b/plugins/cost-insights/src/utils/config.ts index c2c2c066d9..4af7140d9c 100644 --- a/plugins/cost-insights/src/utils/config.ts +++ b/plugins/cost-insights/src/utils/config.ts @@ -17,16 +17,10 @@ import { Metric } from '../types'; export function validateMetrics(metrics: Metric[]) { - let defaultMetric = null; - for (const metric of metrics) { - if (metric.default) { - if (defaultMetric) { - throw new Error( - `Cannot set default for multiple metrics: Received: ${defaultMetric.kind} and ${metric.kind}`, - ); - } else { - defaultMetric = metric; - } - } + const defaults = metrics.filter(metric => metric.default); + if (defaults.length > 1) { + throw new Error( + `Only one default metric can be set at a time. Found ${defaults.length}`, + ); } } From e62956f96f3fbd4ec13f1829752fd009b807d1e4 Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Fri, 16 Oct 2020 11:02:11 -0400 Subject: [PATCH 102/139] getMetricData -> getDailyMetricData --- .../src/plugins/cost-insights/ExampleCostInsightsClient.ts | 5 ++++- plugins/cost-insights/src/api/CostInsightsApi.ts | 2 +- .../src/components/CostInsightsPage/CostInsightsPage.tsx | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/app/src/plugins/cost-insights/ExampleCostInsightsClient.ts b/packages/app/src/plugins/cost-insights/ExampleCostInsightsClient.ts index 0b275baf74..a6ef15f3b9 100644 --- a/packages/app/src/plugins/cost-insights/ExampleCostInsightsClient.ts +++ b/packages/app/src/plugins/cost-insights/ExampleCostInsightsClient.ts @@ -117,7 +117,10 @@ export class ExampleCostInsightsClient implements CostInsightsApi { return projects; } - async getMetricData(metric: string, intervals: string): Promise { + async getDailyMetricData( + metric: string, + intervals: string, + ): Promise { const aggregation = aggregationFor( durationOf(intervals), 100_000, diff --git a/plugins/cost-insights/src/api/CostInsightsApi.ts b/plugins/cost-insights/src/api/CostInsightsApi.ts index 3136fb4c27..028aead853 100644 --- a/plugins/cost-insights/src/api/CostInsightsApi.ts +++ b/plugins/cost-insights/src/api/CostInsightsApi.ts @@ -87,7 +87,7 @@ export type CostInsightsApi = { * @param intervals An ISO 8601 repeating interval string, such as R2/P1M/2020-09-01 * https://en.wikipedia.org/wiki/ISO_8601#Repeating_intervals */ - getMetricData(metric: string, intervals: string): Promise; + getDailyMetricData(metric: string, intervals: string): Promise; /** * Get cost aggregations for a particular cloud product and interval timeframe. This includes diff --git a/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPage.tsx b/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPage.tsx index 20f965f1c7..77d9f5a05e 100644 --- a/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPage.tsx +++ b/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPage.tsx @@ -111,7 +111,7 @@ const CostInsightsPage = () => { client.getGroupProjects(pageFilters.group), client.getAlerts(pageFilters.group), pageFilters.metric - ? client.getMetricData(pageFilters.metric, intervals) + ? client.getDailyMetricData(pageFilters.metric, intervals) : null, pageFilters.project ? client.getProjectDailyCost(pageFilters.project, intervals) From 6dba5f6b60ace9113abef852ae2cf24ddfbee368 Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Fri, 16 Oct 2020 11:22:39 -0400 Subject: [PATCH 103/139] update README --- plugins/cost-insights/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/cost-insights/README.md b/plugins/cost-insights/README.md index 3028d3a09c..8c2fd11761 100644 --- a/plugins/cost-insights/README.md +++ b/plugins/cost-insights/README.md @@ -79,7 +79,7 @@ costInsights: ### Metrics (Optional) -In the `Cost Overview` panel, users can choose from a dropdown of business metrics to see costs as they relate to a metric, such as daily active users. Metrics must be defined as keys on the `metrics` field. A user-friendly name is **required**. Metrics will be provided to the `getMetricData` API method via the `metric` parameter. +In the `Cost Overview` panel, users can choose from a dropdown of business metrics to see costs as they relate to a metric, such as daily active users. Metrics must be defined as keys on the `metrics` field. A user-friendly name is **required**. Metrics will be provided to the `getDailyMetricData` API method via the `metric` parameter. An optional `default` field can be set to `true` to set the default comparison metric to daily cost in the Cost Overview panel. From e7d4ac7ce3137a01336c15a12eab46ee66db5f3e Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Fri, 16 Oct 2020 11:23:04 -0400 Subject: [PATCH 104/139] add changeset --- .changeset/red-games-float.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .changeset/red-games-float.md diff --git a/.changeset/red-games-float.md b/.changeset/red-games-float.md new file mode 100644 index 0000000000..6eabba26a4 --- /dev/null +++ b/.changeset/red-games-float.md @@ -0,0 +1,10 @@ +--- +'@backstage/plugin-cost-insights': minor +--- + +- getProjectDailyCost and getGroupDailyCost no longer accept a metric as a parameter +- getDailyMetricData added to API for fetching daily metric data for given interval +- dailyCost removed as configurable metric +- default field added to metric configuration for displaying comparison metric data in top panel +- Metric.kind can no longer be null +- MetricData type added From fc1705050ef8d2312f49c675b7c20aea31f95629 Mon Sep 17 00:00:00 2001 From: Abhishek Jakhar Date: Sat, 17 Oct 2020 08:15:31 +0530 Subject: [PATCH 105/139] give aria-label attribute to Status Ok, Warning and Error --- .../core/src/components/Status/Status.tsx | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/core/src/components/Status/Status.tsx b/packages/core/src/components/Status/Status.tsx index 9a5ce5b218..6a2e753f1f 100644 --- a/packages/core/src/components/Status/Status.tsx +++ b/packages/core/src/components/Status/Status.tsx @@ -65,20 +65,34 @@ const useStyles = makeStyles(theme => ({ export const StatusOK: FC<{}> = props => { const classes = useStyles(props); - return ; + return ( + + ); }; export const StatusWarning: FC<{}> = props => { const classes = useStyles(props); return ( - + ); }; export const StatusError: FC<{}> = props => { const classes = useStyles(props); return ( - + ); }; From 754e31db5e0438038105d3d0636afe2fc3e89cbd Mon Sep 17 00:00:00 2001 From: Abhishek Jakhar Date: Sat, 17 Oct 2020 08:20:35 +0530 Subject: [PATCH 106/139] add changeset --- .changeset/swift-peas-argue.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/swift-peas-argue.md diff --git a/.changeset/swift-peas-argue.md b/.changeset/swift-peas-argue.md new file mode 100644 index 0000000000..d78d844d45 --- /dev/null +++ b/.changeset/swift-peas-argue.md @@ -0,0 +1,5 @@ +--- +'@backstage/core': patch +--- + +give aria-label attribute to Status Ok, Warning and Error From bb243dca25cf3057297dbfb6a3cbe52289f215c0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 13 Oct 2020 11:46:21 +0200 Subject: [PATCH 107/139] cli: add support for ts4 --- packages/cli/package.json | 4 ++-- packages/docgen/package.json | 2 +- yarn.lock | 16 ++++++++-------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 084254d7ff..ace0639cce 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -84,12 +84,12 @@ "rollup-pluginutils": "^2.8.2", "start-server-webpack-plugin": "^2.2.5", "style-loader": "^1.2.1", - "sucrase": "^3.14.1", + "sucrase": "^3.16.0", "tar": "^6.0.1", "terser-webpack-plugin": "^1.4.3", "ts-jest": "^26.0.0", "ts-loader": "^7.0.4", - "typescript": "^3.9.3", + "typescript": "^4.0.3", "url-loader": "^4.1.0", "webpack": "^4.41.6", "webpack-dev-server": "^3.11.0", diff --git a/packages/docgen/package.json b/packages/docgen/package.json index 61efc0bbd4..7c0bf846bd 100644 --- a/packages/docgen/package.json +++ b/packages/docgen/package.json @@ -31,7 +31,7 @@ "fs-extra": "^9.0.0", "github-slugger": "^1.3.0", "ts-node": "^8.6.2", - "typescript": "^3.9.3" + "typescript": "^4.0.3" }, "devDependencies": { "@types/fs-extra": "^9.0.1", diff --git a/yarn.lock b/yarn.lock index 4d28072207..9a5bd93ffb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21732,10 +21732,10 @@ subscriptions-transport-ws@^0.9.11, subscriptions-transport-ws@^0.9.16: symbol-observable "^1.0.4" ws "^5.2.0" -sucrase@^3.14.1: - version "3.15.0" - resolved "https://registry.npmjs.org/sucrase/-/sucrase-3.15.0.tgz#78596a78be7264a65b52ed8d873883413ef0220c" - integrity sha512-05TJOUfMgckH7wKqfk/1p4G6q16nIeW/GHQwD44vkT0mQMqqzgfHCwkX3whNmwyOo7nVF0jDLwVu/qOBTtsscw== +sucrase@^3.16.0: + version "3.16.0" + resolved "https://registry.npmjs.org/sucrase/-/sucrase-3.16.0.tgz#19b5b886ccca270dd5ca12ff060eeaf0b599735f" + integrity sha512-ovVuswxV5TayCPXfTk8bgBgk6uNRvsinIkEpq0J6zS1xXCx5N/LLGcbsKdRhqn/ToZylMX6+yXaR1LSn1I42Pg== dependencies: commander "^4.0.0" glob "7.1.6" @@ -22702,10 +22702,10 @@ typedarray@^0.0.6: resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@^3.9.3: - version "3.9.7" - resolved "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa" - integrity sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw== +typescript@^4.0.3: + version "4.0.3" + resolved "https://registry.npmjs.org/typescript/-/typescript-4.0.3.tgz#153bbd468ef07725c1df9c77e8b453f8d36abba5" + integrity sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg== ua-parser-js@^0.7.18: version "0.7.21" From 9f3ddfacdeed708755b98a0964c936eed6f23b09 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 17 Oct 2020 14:44:02 +0200 Subject: [PATCH 108/139] cli: split and bump eslint dependencies + fix issues --- .../src/database/config.test.ts | 2 +- .../src/database/connection.test.ts | 2 +- .../src/database/postgres.test.ts | 10 +- .../src/database/sqlite3.test.ts | 4 +- packages/backend-common/src/paths.ts | 2 - packages/cli/package.json | 11 +- packages/cli/src/lib/parallel.test.ts | 4 +- packages/techdocs-cli/package.json | 6 +- .../processors/CodeOwnersProcessor.test.ts | 18 +- .../src/components/BarChart/BarChart.test.tsx | 14 +- .../ProjectSelect/ProjectSelect.test.tsx | 7 +- .../src/components/AuditView/index.test.tsx | 4 +- .../src/hooks/useWebsiteForEntity.test.tsx | 2 +- .../RegisterComponentForm.test.tsx | 2 +- yarn.lock | 190 ++++++++++-------- 15 files changed, 157 insertions(+), 121 deletions(-) diff --git a/packages/backend-common/src/database/config.test.ts b/packages/backend-common/src/database/config.test.ts index c9a4107686..eb26013b4b 100644 --- a/packages/backend-common/src/database/config.test.ts +++ b/packages/backend-common/src/database/config.test.ts @@ -17,7 +17,7 @@ import { mergeDatabaseConfig } from './config'; describe('config', () => { - describe(mergeDatabaseConfig, () => { + describe('mergeDatabaseConfig', () => { it('does not mutate the input object', () => { const input = { original: 'key', diff --git a/packages/backend-common/src/database/connection.test.ts b/packages/backend-common/src/database/connection.test.ts index 2cc3a54e06..de0b0e40dc 100644 --- a/packages/backend-common/src/database/connection.test.ts +++ b/packages/backend-common/src/database/connection.test.ts @@ -26,7 +26,7 @@ describe('database connection', () => { }, ]); - describe(createDatabaseClient, () => { + describe('createDatabaseClient', () => { it('returns a postgres connection', () => { expect( createDatabaseClient( diff --git a/packages/backend-common/src/database/postgres.test.ts b/packages/backend-common/src/database/postgres.test.ts index 82161dc53c..8d139e4481 100644 --- a/packages/backend-common/src/database/postgres.test.ts +++ b/packages/backend-common/src/database/postgres.test.ts @@ -44,7 +44,7 @@ describe('postgres', () => { }, ]); - describe(buildPgDatabaseConfig, () => { + describe('buildPgDatabaseConfig', () => { it('builds a postgres config', () => { const mockConnection = createMockConnection(); @@ -125,7 +125,7 @@ describe('postgres', () => { }); }); - describe(getPgConnectionConfig, () => { + describe('getPgConnectionConfig', () => { it('returns the connection object back', () => { const mockConnection = createMockConnection(); const config = createConfig(mockConnection); @@ -163,7 +163,7 @@ describe('postgres', () => { }); }); - describe(createPgDatabaseClient, () => { + describe('createPgDatabaseClient', () => { it('creates a postgres knex instance', () => { expect( createPgDatabaseClient( @@ -188,8 +188,8 @@ describe('postgres', () => { }); }); - describe(parsePgConnectionString, () => { - it('parses a connection string uri ', () => { + describe('parsePgConnectionString', () => { + it('parses a connection string uri', () => { expect( parsePgConnectionString( 'postgresql://postgres:pass@foobar:5432/dbname?ssl=true', diff --git a/packages/backend-common/src/database/sqlite3.test.ts b/packages/backend-common/src/database/sqlite3.test.ts index dbff5ee354..a3ab331c2d 100644 --- a/packages/backend-common/src/database/sqlite3.test.ts +++ b/packages/backend-common/src/database/sqlite3.test.ts @@ -32,7 +32,7 @@ describe('sqlite3', () => { }, ]); - describe(buildSqliteDatabaseConfig, () => { + describe('buildSqliteDatabaseConfig', () => { it('buidls a string connection', () => { expect(buildSqliteDatabaseConfig(createConfig(':memory:'))).toEqual({ client: 'sqlite3', @@ -72,7 +72,7 @@ describe('sqlite3', () => { }); }); - describe(createSqliteDatabaseClient, () => { + describe('createSqliteDatabaseClient', () => { it('creates an in memory knex instance', () => { expect( createSqliteDatabaseClient( diff --git a/packages/backend-common/src/paths.ts b/packages/backend-common/src/paths.ts index 402c0e2252..262be366f6 100644 --- a/packages/backend-common/src/paths.ts +++ b/packages/backend-common/src/paths.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -/* eslint-disable @typescript-eslint/camelcase */ - import { resolve as resolvePath } from 'path'; /** diff --git a/packages/cli/package.json b/packages/cli/package.json index ace0639cce..74cbbfff6a 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -38,7 +38,9 @@ "@rollup/plugin-json": "^4.0.2", "@rollup/plugin-node-resolve": "^8.1.0", "@rollup/plugin-yaml": "^2.1.1", - "@spotify/eslint-config": "^7.0.1", + "@spotify/eslint-config-base": "^8.0.0", + "@spotify/eslint-config-react": "^8.0.0", + "@spotify/eslint-config-typescript": "^8.0.0", "@sucrase/webpack-loader": "^2.0.0", "@svgr/plugin-jsx": "5.4.x", "@svgr/plugin-svgo": "5.4.x", @@ -47,6 +49,8 @@ "@types/start-server-webpack-plugin": "^2.2.0", "@types/webpack-env": "^1.15.2", "@types/webpack-node-externals": "^2.5.0", + "@typescript-eslint/eslint-plugin": "^v3.10.1", + "@typescript-eslint/parser": "^v3.10.1", "bfj": "^7.0.2", "chalk": "^4.0.0", "chokidar": "^3.3.1", @@ -56,9 +60,14 @@ "diff": "^4.0.2", "esbuild": "^0.7.7", "eslint": "^7.1.0", + "eslint-config-prettier": "^6.0.0", "eslint-formatter-friendly": "^7.0.0", "eslint-plugin-import": "^2.20.2", + "eslint-plugin-jest": "^24.1.0", + "eslint-plugin-jsx-a11y": "^6.2.1", "eslint-plugin-monorepo": "^0.2.1", + "eslint-plugin-react": "^7.12.4", + "eslint-plugin-react-hooks": "^4.0.0", "fork-ts-checker-webpack-plugin": "^4.0.5", "fs-extra": "^9.0.0", "handlebars": "^4.7.3", diff --git a/packages/cli/src/lib/parallel.test.ts b/packages/cli/src/lib/parallel.test.ts index c8173603d9..8047774a6e 100644 --- a/packages/cli/src/lib/parallel.test.ts +++ b/packages/cli/src/lib/parallel.test.ts @@ -17,7 +17,7 @@ import { isParallelDefault, parseParallel } from './parallel'; describe('parallel', () => { - describe(parseParallel, () => { + describe('parseParallel', () => { it('coerces "false" string to boolean', () => { expect(parseParallel('false')).toBeFalsy(); }); @@ -44,7 +44,7 @@ describe('parallel', () => { }); }); - describe(isParallelDefault, () => { + describe('isParallelDefault', () => { it('returns true if default value', () => { expect(isParallelDefault(undefined)).toBeTruthy(); expect(isParallelDefault(true)).toBeTruthy(); diff --git a/packages/techdocs-cli/package.json b/packages/techdocs-cli/package.json index adce3dd101..30ca6e4538 100644 --- a/packages/techdocs-cli/package.json +++ b/packages/techdocs-cli/package.json @@ -28,11 +28,7 @@ "techdocs-cli": "bin/techdocs-cli" }, "devDependencies": { - "@spotify/eslint-config": "^7.0.0", - "@spotify/prettier-config": "^7.0.0", - "@types/serve-handler": "^6.1.0", - "eslint": "^7.1.0", - "eslint-plugin-import": "^2.22.0" + "@types/serve-handler": "^6.1.0" }, "files": [ "bin", diff --git a/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.test.ts index c1c83b7c6c..e1bd5bcb46 100644 --- a/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.test.ts @@ -27,7 +27,7 @@ import { resolveCodeOwner, } from './CodeOwnersProcessor'; -describe(CodeOwnersProcessor, () => { +describe('CodeOwnersProcessor', () => { const mockUrl = ({ basePath = '' } = {}): string => `https://github.com/spotify/backstage/blob/master/${basePath}catalog-info.yaml`; const mockLocation = ({ @@ -79,7 +79,7 @@ describe(CodeOwnersProcessor, () => { return data; }; - describe(buildUrl, () => { + describe('buildUrl', () => { it.each([['azure.com'], ['dev.azure.com']])( 'should throw not implemented error', source => { @@ -99,7 +99,7 @@ describe(CodeOwnersProcessor, () => { }); }); - describe(buildCodeOwnerUrl, () => { + describe('buildCodeOwnerUrl', () => { it('should build a location spec to the codeowners', () => { expect(buildCodeOwnerUrl(mockUrl(), '/docs/CODEOWNERS')).toEqual( 'https://github.com/spotify/backstage/blob/master/docs/CODEOWNERS', @@ -116,13 +116,13 @@ describe(CodeOwnersProcessor, () => { }); }); - describe(parseCodeOwners, () => { + describe('parseCodeOwners', () => { it('should parse the codeowners file', () => { expect(parseCodeOwners(mockCodeOwnersText())).toEqual(mockCodeOwners()); }); }); - describe(normalizeCodeOwner, () => { + describe('normalizeCodeOwner', () => { it('should remove org from org/team format', () => { expect(normalizeCodeOwner('@acme/foo')).toBe('foo'); }); @@ -139,13 +139,13 @@ describe(CodeOwnersProcessor, () => { ); }); - describe(findPrimaryCodeOwner, () => { + describe('findPrimaryCodeOwner', () => { it('should return the primary owner', () => { expect(findPrimaryCodeOwner(mockCodeOwners())).toBe('backstage-core'); }); }); - describe(findRawCodeOwners, () => { + describe('findRawCodeOwners', () => { it('should return found codeowner', async () => { const ownersText = mockCodeOwnersText(); const read = jest @@ -184,7 +184,7 @@ describe(CodeOwnersProcessor, () => { }); }); - describe(resolveCodeOwner, () => { + describe('resolveCodeOwner', () => { it('should return found codeowner', async () => { const read = jest .fn() @@ -207,7 +207,7 @@ describe(CodeOwnersProcessor, () => { }); }); - describe(CodeOwnersProcessor, () => { + describe('CodeOwnersProcessor', () => { const setupTest = ({ kind = 'Component', spec = {} } = {}) => { const entity = { kind, spec }; const read = jest diff --git a/plugins/cost-insights/src/components/BarChart/BarChart.test.tsx b/plugins/cost-insights/src/components/BarChart/BarChart.test.tsx index a39c5bafc2..737b86018c 100644 --- a/plugins/cost-insights/src/components/BarChart/BarChart.test.tsx +++ b/plugins/cost-insights/src/components/BarChart/BarChart.test.tsx @@ -79,12 +79,14 @@ describe('', () => { it('Should display only 6 resources by default, sorted by cost', async () => { const rendered = await renderWithProps({} as BarChartProps); - MockResources.sort(resourceSort).forEach((resource, index) => { - if (index < 6) { - expect(rendered.getByText(resource.name!)).toBeInTheDocument(); - } else { - expect(rendered.queryByText(resource.name!)).not.toBeInTheDocument(); - } + const sorted = MockResources.sort(resourceSort); + + expect(sorted.length).toBe(10); + sorted.slice(0, 6).forEach(resource => { + expect(rendered.getByText(resource.name!)).toBeInTheDocument(); + }); + sorted.slice(6).forEach(resource => { + expect(rendered.queryByText(resource.name!)).not.toBeInTheDocument(); }); }); diff --git a/plugins/cost-insights/src/components/ProjectSelect/ProjectSelect.test.tsx b/plugins/cost-insights/src/components/ProjectSelect/ProjectSelect.test.tsx index 92de819fe0..a74c79f3b8 100644 --- a/plugins/cost-insights/src/components/ProjectSelect/ProjectSelect.test.tsx +++ b/plugins/cost-insights/src/components/ProjectSelect/ProjectSelect.test.tsx @@ -59,10 +59,9 @@ describe('', () => { const button = getByRole(projectSelectContainer, 'button'); UserEvent.click(button); await waitFor(() => rendered.getByTestId('option-all')); - mockProjects.forEach( - project => - project.id && - expect(rendered.getByText(project.id)).toBeInTheDocument(), + + mockProjects.forEach(project => + expect(rendered.getByText(project.id)).toBeInTheDocument(), ); }); }); diff --git a/plugins/lighthouse/src/components/AuditView/index.test.tsx b/plugins/lighthouse/src/components/AuditView/index.test.tsx index 742be67fed..ca292a133d 100644 --- a/plugins/lighthouse/src/components/AuditView/index.test.tsx +++ b/plugins/lighthouse/src/components/AuditView/index.test.tsx @@ -163,7 +163,7 @@ describe('AuditView', () => { }); describe('when the request for the website by id is pending', () => { - it('it shows the loading', async () => { + it('shows the loading', async () => { mockFetch.mockImplementationOnce(() => new Promise(() => {})); const rendered = render( wrapInTestApp( @@ -177,7 +177,7 @@ describe('AuditView', () => { }); describe('when the request for the website by id fails', () => { - it('it shows an error', async () => { + it('shows an error', async () => { mockFetch.mockRejectOnce(new Error('failed to fetch')); const rendered = render( wrapInTestApp( diff --git a/plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx b/plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx index 868e26f04c..580162f182 100644 --- a/plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx +++ b/plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx @@ -75,7 +75,7 @@ describe('useWebsiteForEntity', () => { (mockLighthouseApi.getWebsiteByUrl as jest.Mock).mockResolvedValue(website); }); - it('returns the lighthouse information for the website url in annotations ', async () => { + it('returns the lighthouse information for the website url in annotations', async () => { const { result, waitForNextUpdate } = subject(); await waitForNextUpdate(); expect(result.current?.value).toBe(website); diff --git a/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.test.tsx b/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.test.tsx index 10c3d9fcd1..875b1f8660 100644 --- a/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.test.tsx +++ b/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.test.tsx @@ -30,7 +30,7 @@ describe('RegisterComponentForm', () => { expect(screen.getByText('Submit').closest('button')).toBeDisabled(); }); - it('should enable a submit button when the target url is set ', async () => { + it('should enable a submit button when the target url is set', async () => { render(); await act(async () => { diff --git a/yarn.lock b/yarn.lock index 9a5bd93ffb..168da27a6f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3802,10 +3802,10 @@ dependencies: "@sinonjs/commons" "^1.7.0" -"@spotify/eslint-config-base@^7.0.0": - version "7.0.0" - resolved "https://registry.npmjs.org/@spotify/eslint-config-base/-/eslint-config-base-7.0.0.tgz#36804ae09ec938f1aa5f9464ea993f3f151cfaa8" - integrity sha512-XRTrTRyRRYBxPYINKNHw4B8QWlA3p4I+id/Po2sMdejtXH3LZgDgIjdschUZSdQ6J6dVYDdaVykdizRM9I+G6Q== +"@spotify/eslint-config-base@^8.0.0": + version "8.0.0" + resolved "https://registry.npmjs.org/@spotify/eslint-config-base/-/eslint-config-base-8.0.0.tgz#c3d10d8a05ad9129d579952cb80063e4c4fa216e" + integrity sha512-07Fo+KoTMl4AaZJPD6deFnYmG634wzgeqfFcWdFlIrOLAasvWSH8Lp6ZE9cJBKuffOAy8kuEE1SwFo34+5X8bQ== "@spotify/eslint-config-oss@^1.0.1": version "1.0.2" @@ -3814,51 +3814,21 @@ dependencies: eslint-plugin-notice "^0.9.10" -"@spotify/eslint-config-react@^7.0.1": - version "7.0.1" - resolved "https://registry.npmjs.org/@spotify/eslint-config-react/-/eslint-config-react-7.0.1.tgz#2e70de9d7911ea9aeaa55a83a332220a28f6c431" - integrity sha512-HNDHvm19EaBXiDgsg52mjMgKWZTeA0hO2Q75ACNwb8UtjIKkANqyyuzyDGo8jiGMbWIm6wJjShlRtT95emNlqQ== +"@spotify/eslint-config-react@^8.0.0": + version "8.0.2" + resolved "https://registry.npmjs.org/@spotify/eslint-config-react/-/eslint-config-react-8.0.2.tgz#91af3c58b22d49a96795336ed66927e722cda430" + integrity sha512-V0gzO/jWmjhODLINAfeunHj+OBdj/cQeVDw+92ZUlkonAg6BZLjX6Vy8jeIUFjfgmVOtI9eKbcApxaurj1jN6g== -"@spotify/eslint-config-typescript@^7.0.0": - version "7.0.0" - resolved "https://registry.npmjs.org/@spotify/eslint-config-typescript/-/eslint-config-typescript-7.0.0.tgz#fc5227e3344f74b41ac3a530df24a95ac13254e4" - integrity sha512-28I/SAf68NKbWZ5IY0WYMa0D18PxWdC9DP9gRbOTlZufmsS8jEgqf3zBUWmP6XOf1nihpKWcqvbFUG5H7/JYXA== - -"@spotify/eslint-config@^7.0.0", "@spotify/eslint-config@^7.0.1": - version "7.0.1" - resolved "https://registry.npmjs.org/@spotify/eslint-config/-/eslint-config-7.0.1.tgz#07a21cfd7fce89cfc2c6dd5ea5d747e741201b66" - integrity sha512-8GI/TZGUhS4pr7oipT2MjrZFRgXcKzk9YImEusUdD2f5vlCniRFIBQNrvTMkyjfdQqvIVqJPLcdVPXeAgprsMw== - dependencies: - "@spotify/eslint-config-base" "^7.0.0" - "@spotify/eslint-config-react" "^7.0.1" - "@spotify/eslint-config-typescript" "^7.0.0" - "@spotify/web-scripts-utils" "^7.0.0" - "@typescript-eslint/eslint-plugin" "^2.14.0" - "@typescript-eslint/parser" "^2.14.0" - eslint-config-prettier "^6.0.0" - eslint-plugin-jest "^23.6.0" - eslint-plugin-jsx-a11y "^6.2.1" - eslint-plugin-react "^7.12.4" - eslint-plugin-react-hooks "^4.0.0" - -"@spotify/prettier-config@^7.0.0": - version "7.0.0" - resolved "https://registry.npmjs.org/@spotify/prettier-config/-/prettier-config-7.0.0.tgz#47750979d1282197295108b6958360660a955c16" - integrity sha512-lIMcx/2oDqTtW84iHKkRJe+8U6HK6GPwWH5sJp9UEHcDpdXomOQYvwcGXy2I2zwPQQ14gYYE6nEJuSnnYqsYRw== +"@spotify/eslint-config-typescript@^8.0.0": + version "8.1.1" + resolved "https://registry.npmjs.org/@spotify/eslint-config-typescript/-/eslint-config-typescript-8.1.1.tgz#a8c6f74ef9445fe740008ae3a26003a2eef78f93" + integrity sha512-0NkE6ZiSRf9IBWwCxzn/l7UdwcL/KZsbzjjOSweafyYueajEn+eIpKjOgDGUL/ebxSrOtjEp+YeFc4lSPkP0qg== "@spotify/prettier-config@^8.0.0": version "8.0.0" resolved "https://registry.npmjs.org/@spotify/prettier-config/-/prettier-config-8.0.0.tgz#8b6c2bd579ddc54887155a0721fe04e96c89f7f2" integrity sha512-so8w32ZV42CHWxOEXcBtbNO/hLXFrQNXVmhfzhUI6dVB9cq2xjRaiqu8GjFj8LvKbWpPj+S+KwTIS4aDVWqrFQ== -"@spotify/web-scripts-utils@^7.0.0": - version "7.0.0" - resolved "https://registry.npmjs.org/@spotify/web-scripts-utils/-/web-scripts-utils-7.0.0.tgz#8c6b8039fc645a36ac48629eb9ba06600f4d828a" - integrity sha512-McMy0j60lxOHjgDjegthZqEWN/PabphiM30A/mI/Y7xh9+JFnYWBTSm/wgn6EW2BsPpV631xSYYkk6B1Ph14Fw== - dependencies: - glob "^7.1.4" - read-pkg-up "^7.0.1" - "@storybook/addon-actions@^6.0.21": version "6.0.21" resolved "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-6.0.21.tgz#0de1d109d4b1eb99f644bbe84e74c25cfd2b1b6b" @@ -5830,49 +5800,113 @@ resolved "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d" integrity sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg== -"@typescript-eslint/eslint-plugin@^2.14.0": - version "2.24.0" - resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.24.0.tgz#a86cf618c965a462cddf3601f594544b134d6d68" - integrity sha512-wJRBeaMeT7RLQ27UQkDFOu25MqFOBus8PtOa9KaT5ZuxC1kAsd7JEHqWt4YXuY9eancX0GK9C68i5OROnlIzBA== +"@typescript-eslint/eslint-plugin@^v3.10.1": + version "3.10.1" + resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.10.1.tgz#7e061338a1383f59edc204c605899f93dc2e2c8f" + integrity sha512-PQg0emRtzZFWq6PxBcdxRH3QIQiyFO3WCVpRL3fgj5oQS3CDs3AeAKfv4DxNhzn8ITdNJGJ4D3Qw8eAJf3lXeQ== dependencies: - "@typescript-eslint/experimental-utils" "2.24.0" - eslint-utils "^1.4.3" + "@typescript-eslint/experimental-utils" "3.10.1" + debug "^4.1.1" functional-red-black-tree "^1.0.1" regexpp "^3.0.0" + semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/experimental-utils@2.24.0", "@typescript-eslint/experimental-utils@^2.5.0": - version "2.24.0" - resolved "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.24.0.tgz#a5cb2ed89fedf8b59638dc83484eb0c8c35e1143" - integrity sha512-DXrwuXTdVh3ycNCMYmWhUzn/gfqu9N0VzNnahjiDJvcyhfBy4gb59ncVZVxdp5XzBC77dCncu0daQgOkbvPwBw== +"@typescript-eslint/experimental-utils@3.10.1": + version "3.10.1" + resolved "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.10.1.tgz#e179ffc81a80ebcae2ea04e0332f8b251345a686" + integrity sha512-DewqIgscDzmAfd5nOGe4zm6Bl7PKtMG2Ad0KG8CUZAHlXfAKTF9Ol5PXhiMh39yRL2ChRH1cuuUGOcVyyrhQIw== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/typescript-estree" "2.24.0" + "@typescript-eslint/types" "3.10.1" + "@typescript-eslint/typescript-estree" "3.10.1" eslint-scope "^5.0.0" + eslint-utils "^2.0.0" -"@typescript-eslint/parser@^2.14.0": - version "2.24.0" - resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.24.0.tgz#2cf0eae6e6dd44d162486ad949c126b887f11eb8" - integrity sha512-H2Y7uacwSSg8IbVxdYExSI3T7uM1DzmOn2COGtCahCC3g8YtM1xYAPi2MAHyfPs61VKxP/J/UiSctcRgw4G8aw== +"@typescript-eslint/experimental-utils@^4.0.1": + version "4.4.1" + resolved "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.4.1.tgz#40613b9757fa0170de3e0043254dbb077cafac0c" + integrity sha512-Nt4EVlb1mqExW9cWhpV6pd1a3DkUbX9DeyYsdoeziKOpIJ04S2KMVDO+SEidsXRH/XHDpbzXykKcMTLdTXH6cQ== + dependencies: + "@types/json-schema" "^7.0.3" + "@typescript-eslint/scope-manager" "4.4.1" + "@typescript-eslint/types" "4.4.1" + "@typescript-eslint/typescript-estree" "4.4.1" + eslint-scope "^5.0.0" + eslint-utils "^2.0.0" + +"@typescript-eslint/parser@^v3.10.1": + version "3.10.1" + resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-3.10.1.tgz#1883858e83e8b442627e1ac6f408925211155467" + integrity sha512-Ug1RcWcrJP02hmtaXVS3axPPTTPnZjupqhgj+NnZ6BCkwSImWk/283347+x9wN+lqOdK9Eo3vsyiyDHgsmiEJw== dependencies: "@types/eslint-visitor-keys" "^1.0.0" - "@typescript-eslint/experimental-utils" "2.24.0" - "@typescript-eslint/typescript-estree" "2.24.0" + "@typescript-eslint/experimental-utils" "3.10.1" + "@typescript-eslint/types" "3.10.1" + "@typescript-eslint/typescript-estree" "3.10.1" eslint-visitor-keys "^1.1.0" -"@typescript-eslint/typescript-estree@2.24.0": - version "2.24.0" - resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.24.0.tgz#38bbc8bb479790d2f324797ffbcdb346d897c62a" - integrity sha512-RJ0yMe5owMSix55qX7Mi9V6z2FDuuDpN6eR5fzRJrp+8in9UF41IGNQHbg5aMK4/PjVaEQksLvz0IA8n+Mr/FA== +"@typescript-eslint/scope-manager@4.4.1": + version "4.4.1" + resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.4.1.tgz#d19447e60db2ce9c425898d62fa03b2cce8ea3f9" + integrity sha512-2oD/ZqD4Gj41UdFeWZxegH3cVEEH/Z6Bhr/XvwTtGv66737XkR4C9IqEkebCuqArqBJQSj4AgNHHiN1okzD/wQ== dependencies: + "@typescript-eslint/types" "4.4.1" + "@typescript-eslint/visitor-keys" "4.4.1" + +"@typescript-eslint/types@3.10.1": + version "3.10.1" + resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-3.10.1.tgz#1d7463fa7c32d8a23ab508a803ca2fe26e758727" + integrity sha512-+3+FCUJIahE9q0lDi1WleYzjCwJs5hIsbugIgnbB+dSCYUxl8L6PwmsyOPFZde2hc1DlTo/xnkOgiTLSyAbHiQ== + +"@typescript-eslint/types@4.4.1": + version "4.4.1" + resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.4.1.tgz#c507b35cf523bc7ba00aae5f75ee9b810cdabbc1" + integrity sha512-KNDfH2bCyax5db+KKIZT4rfA8rEk5N0EJ8P0T5AJjo5xrV26UAzaiqoJCxeaibqc0c/IvZxp7v2g3difn2Pn3w== + +"@typescript-eslint/typescript-estree@3.10.1": + version "3.10.1" + resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.10.1.tgz#fd0061cc38add4fad45136d654408569f365b853" + integrity sha512-QbcXOuq6WYvnB3XPsZpIwztBoquEYLXh2MtwVU+kO8jgYCiv4G5xrSP/1wg4tkvrEE+esZVquIPX/dxPlePk1w== + dependencies: + "@typescript-eslint/types" "3.10.1" + "@typescript-eslint/visitor-keys" "3.10.1" debug "^4.1.1" - eslint-visitor-keys "^1.1.0" glob "^7.1.6" is-glob "^4.0.1" lodash "^4.17.15" - semver "^6.3.0" + semver "^7.3.2" tsutils "^3.17.1" +"@typescript-eslint/typescript-estree@4.4.1": + version "4.4.1" + resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.4.1.tgz#598f6de488106c2587d47ca2462c60f6e2797cb8" + integrity sha512-wP/V7ScKzgSdtcY1a0pZYBoCxrCstLrgRQ2O9MmCUZDtmgxCO/TCqOTGRVwpP4/2hVfqMz/Vw1ZYrG8cVxvN3g== + dependencies: + "@typescript-eslint/types" "4.4.1" + "@typescript-eslint/visitor-keys" "4.4.1" + debug "^4.1.1" + globby "^11.0.1" + is-glob "^4.0.1" + lodash "^4.17.15" + semver "^7.3.2" + tsutils "^3.17.1" + +"@typescript-eslint/visitor-keys@3.10.1": + version "3.10.1" + resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-3.10.1.tgz#cd4274773e3eb63b2e870ac602274487ecd1e931" + integrity sha512-9JgC82AaQeglebjZMgYR5wgmfUdUc+EitGUUMW8u2nDckaeimzW+VsoLV6FoimPv2id3VQzfjwBxEMVz08ameQ== + dependencies: + eslint-visitor-keys "^1.1.0" + +"@typescript-eslint/visitor-keys@4.4.1": + version "4.4.1" + resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.4.1.tgz#1769dc7a9e2d7d2cfd3318b77ed8249187aed5c3" + integrity sha512-H2JMWhLaJNeaylSnMSQFEhT/S/FsJbebQALmoJxMPMxLtlVAMy2uJP/Z543n9IizhjRayLSqoInehCeNW9rWcw== + dependencies: + "@typescript-eslint/types" "4.4.1" + eslint-visitor-keys "^2.0.0" + "@webassemblyjs/ast@1.9.0": version "1.9.0" resolved "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964" @@ -10677,7 +10711,7 @@ eslint-plugin-graphql@^4.0.0: lodash.flatten "^4.4.0" lodash.without "^4.4.0" -eslint-plugin-import@^2.20.2, eslint-plugin-import@^2.22.0: +eslint-plugin-import@^2.20.2: version "2.22.0" resolved "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.22.0.tgz#92f7736fe1fde3e2de77623c838dd992ff5ffb7e" integrity sha512-66Fpf1Ln6aIS5Gr/55ts19eUuoDhAbZgnr6UxK5hbDx6l/QgQgx61AePq+BV4PP2uXQFClgMVzep5zZ94qqsxg== @@ -10696,12 +10730,12 @@ eslint-plugin-import@^2.20.2, eslint-plugin-import@^2.22.0: resolve "^1.17.0" tsconfig-paths "^3.9.0" -eslint-plugin-jest@^23.6.0: - version "23.8.2" - resolved "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-23.8.2.tgz#6f28b41c67ef635f803ebd9e168f6b73858eb8d4" - integrity sha512-xwbnvOsotSV27MtAe7s8uGWOori0nUsrXh2f1EnpmXua8sDfY6VZhHAhHg2sqK7HBNycRQExF074XSZ7DvfoFg== +eslint-plugin-jest@^24.1.0: + version "24.1.0" + resolved "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-24.1.0.tgz#6708037d7602e5288ce877fd0103f329dc978361" + integrity sha512-827YJ+E8B9PvXu/0eiVSNFfxxndbKv+qE/3GSMhdorCaeaOehtqHGX2YDW9B85TEOre9n/zscledkFW/KbnyGg== dependencies: - "@typescript-eslint/experimental-utils" "^2.5.0" + "@typescript-eslint/experimental-utils" "^4.0.1" eslint-plugin-jsx-a11y@^6.2.1: version "6.2.3" @@ -10779,13 +10813,6 @@ eslint-scope@^5.0.0, eslint-scope@^5.1.0: esrecurse "^4.1.0" estraverse "^4.1.1" -eslint-utils@^1.4.3: - version "1.4.3" - resolved "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f" - integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q== - dependencies: - eslint-visitor-keys "^1.1.0" - eslint-utils@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.0.0.tgz#7be1cc70f27a72a76cd14aa698bcabed6890e1cd" @@ -10798,6 +10825,11 @@ eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.2.0: resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.2.0.tgz#74415ac884874495f78ec2a97349525344c981fa" integrity sha512-WFb4ihckKil6hu3Dp798xdzSfddwKKU3+nGniKF6HfeW6OLd2OUDEPP7TcHtB5+QXOKg2s6B2DaMPE1Nn/kxKQ== +eslint-visitor-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8" + integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ== + eslint@^7.1.0: version "7.4.0" resolved "https://registry.npmjs.org/eslint/-/eslint-7.4.0.tgz#4e35a2697e6c1972f9d6ef2b690ad319f80f206f" @@ -12189,7 +12221,7 @@ globalthis@^1.0.0: dependencies: define-properties "^1.1.3" -globby@11.0.1, globby@^11.0.0: +globby@11.0.1, globby@^11.0.0, globby@^11.0.1: version "11.0.1" resolved "https://registry.npmjs.org/globby/-/globby-11.0.1.tgz#9a2bf107a068f3ffeabc49ad702c79ede8cfd357" integrity sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ== From e4d0aa4609a9bd3ce62044a28efec9707499f840 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 17 Oct 2020 15:11:28 +0200 Subject: [PATCH 109/139] docgen: fix duplicate help text --- packages/docgen/src/index.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/docgen/src/index.ts b/packages/docgen/src/index.ts index f41629e7fd..4b69fdbe01 100644 --- a/packages/docgen/src/index.ts +++ b/packages/docgen/src/index.ts @@ -49,10 +49,6 @@ const main = (argv: string[]) => { process.exit(1); }); - if (!process.argv.slice(2).length) { - program.outputHelp(chalk.yellow); - } - program.parse(argv); }; From 547cd78456053837c173f654aad222a41a6168a1 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 13 Oct 2020 11:47:50 +0200 Subject: [PATCH 110/139] contrib/kubernetes: added plain single pod deployment example --- .github/styles/vocab.txt | 1 + .../plain_single_backend_deplyoment/README.md | 42 +++++++ .../deployment.yaml | 108 ++++++++++++++++++ 3 files changed, 151 insertions(+) create mode 100644 contrib/kubernetes/plain_single_backend_deplyoment/README.md create mode 100644 contrib/kubernetes/plain_single_backend_deplyoment/deployment.yaml diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index 6a2eecdee0..40346fc0e8 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -105,6 +105,7 @@ Monorepo monorepos msw namespace +namespaces Namespaces neuro newrelic diff --git a/contrib/kubernetes/plain_single_backend_deplyoment/README.md b/contrib/kubernetes/plain_single_backend_deplyoment/README.md new file mode 100644 index 0000000000..685bed92a5 --- /dev/null +++ b/contrib/kubernetes/plain_single_backend_deplyoment/README.md @@ -0,0 +1,42 @@ +# Plain Kubernetes Deployment + +This directory contains an example of a simple Kubernetes deployment of Backstage. It is not intended to serve as a complete production deployment, but as a starting point for setting one up. + +## Usage + +You can try the deployment out as is. The easiest way is to use [Docker Desktop](https://www.docker.com/products/docker-desktop) with [Kubernetes](https://docs.docker.com/get-started/kube-deploy/). + +From a fresh clone of this repo, run the following in the root: + +```bash +yarn install + +yarn docker-build + +kubectl apply -f contrib/kubernetes/plain_single_backend_deplyoment/deployment.yaml +``` + +You can use the following commands to monitor the deployment: + +```bash +# List all resources in the backstage namespace +kubectl -n backstage get all + +# Inspect the status of the deployment resource +kubectl -n backstage describe deployment backstage-backend + +# Inspect the status of the pod running the backstage backend +kubectl -n backstage describe pod -l app=backstage,component=backend +``` + +Once the deployment is up and running, you can use the following to set up a proxy to reach the backend locally: + +```bash +kubectl proxy +``` + +With the proxy up and running, you should be able to navigate to [http://localhost:8001/api/v1/namespaces/backstage/services/backstage-backend:http/proxy](http://localhost:8001/api/v1/namespaces/backstage/services/backstage-backend:http/proxy) and see Backstage. Note that you'll end up on a 404 page, but hitting the home icon in the sidebar should take you to the catalog page where you can see a few example services. + +## Caveats + +This deployment is for demonstration purposes only, for a production deployment you will need to set up at least a persistent database and some form of ingress. If your organization doesn't already have established patterns for these, you could look at options of managed PostgreSQL instances from cloud providers, or something like Zalando's [postgres-operator](https://github.com/zalando/postgres-operator). For ingress there are also [plenty of options](https://ramitsurana.gitbook.io/awesome-kubernetes/docs/projects/projects#load-balancing), where `nginx` is a popular choice to get started. diff --git a/contrib/kubernetes/plain_single_backend_deplyoment/deployment.yaml b/contrib/kubernetes/plain_single_backend_deplyoment/deployment.yaml new file mode 100644 index 0000000000..87dee94895 --- /dev/null +++ b/contrib/kubernetes/plain_single_backend_deplyoment/deployment.yaml @@ -0,0 +1,108 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: backstage +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: backstage-backend + namespace: backstage +spec: + replicas: 1 + selector: + matchLabels: + app: backstage + component: backend + template: + metadata: + labels: + app: backstage + component: backend + spec: + containers: + - name: backend + # This image is built with `yarn docker-build` in the repo root. + # Replace this with your own image to deploy your own Backstage app. + image: example-backend:latest + imagePullPolicy: Never + + env: + # We set this to development to make the backend start with incomplete configuration. In a production + # deployment you will want to make sure that you have a full configuration, and remove any plugins that + # you are not using. + - name: NODE_ENV + value: development + + # This makes us load in `app-config.production.yaml` if there is one. + - name: APP_ENV + value: production + + # This makes it possible for the app to reach the backend when serving through `kubectl proxy` + # If you expose the service using for example an ingress controller, you should + # switch this out or remove it. + # + # Note that we're not setting app.baseUrl here, as setting the base path is not working at the moment. + # Further work is needed around the routing in the frontend or react-router before we can support that. + - name: APP_CONFIG_backend_baseUrl + value: http://localhost:8001/api/v1/namespaces/backstage/services/backstage-backend:http/proxy + + ports: + - name: http + containerPort: 7000 + + volumeMounts: + - name: config-volume + mountPath: /usr/src/app/app-config.local.yaml + subPath: app-config.local.yaml + + resources: + limits: + cpu: 1 + memory: 0.5Gi + + readinessProbe: + httpGet: + port: 7000 + path: /healthcheck + livenessProbe: + httpGet: + port: 7000 + path: /healthcheck + + volumes: + - name: config-volume + configMap: + name: backstage-config + items: + - key: app-config + path: app-config.local.yaml +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: backstage-config + namespace: backstage +data: + # Note that the config here is only applied to the backend. The frontend config is applied at build time. + # To override frontend config in this deployment, use `APP_CONFIG_` env vars. + app-config: | + app: + baseUrl: http://localhost:8001/api/v1/namespaces/backstage/services/backstage-backend:http/proxy + backend: + baseUrl: http://localhost:8001/api/v1/namespaces/backstage/services/backstage-backend:http/proxy +--- +apiVersion: v1 +kind: Service +metadata: + name: backstage-backend + namespace: backstage +spec: + selector: + app: backstage + component: backend + ports: + - name: http + port: 80 + targetPort: http From 2d15cfb1a4837a9efbc244b0c90ef275bc4254af Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 17 Oct 2020 12:55:35 +0200 Subject: [PATCH 111/139] docs/getting-started: added some minimal docs for k8s deployment, referencing contrib example --- docs/getting-started/deployment-k8s.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/getting-started/deployment-k8s.md b/docs/getting-started/deployment-k8s.md index ddcbd290c6..69aa59a600 100644 --- a/docs/getting-started/deployment-k8s.md +++ b/docs/getting-started/deployment-k8s.md @@ -4,4 +4,10 @@ title: Kubernetes description: Documentation on Kubernetes and K8s Deployment --- -Coming soon! +Backstage itself provides tooling up to the point of building docker images. +Beyond that point we do not have an opinionated way to deploy Backstage within +Kubernetes, as each cluster has its own unique set of tooling and patterns. + +We do provide examples to help you get started though. Check out +[this example](https://github.com/spotify/backstage/tree/master/contrib/kubernetes/plain_single_backend_deplyoment/) +for a basic single-deployment setup. From bbf5faec6fd7663b61bead43a24436c52649468d Mon Sep 17 00:00:00 2001 From: amitkhurana92 <41064453+amitkhurana92@users.noreply.github.com> Date: Sat, 17 Oct 2020 19:22:20 +0530 Subject: [PATCH 112/139] Jenkins Plugin: Readme.md file updated (#2943) * Readme.md file updated The documentation was not concise and does not include the case if I don't want to use env variables. * Prettier invoked. Executed prettier command. Co-authored-by: Amit Khuraba --- plugins/jenkins/README.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/plugins/jenkins/README.md b/plugins/jenkins/README.md index 565906ca85..1d3e7f60e5 100644 --- a/plugins/jenkins/README.md +++ b/plugins/jenkins/README.md @@ -33,7 +33,7 @@ proxy: $env: JENKINS_BASIC_AUTH_HEADER ``` -4. Add an environment variable which contains the Jenkins credentials, (note: use an API token not your password) +4. Add an environment variable which contains the Jenkins credentials, (note: use an API token not your password). Here user is the name of the user created in Jenkins. ```shell HEADER=$(echo -n user:api-token | base64) @@ -61,6 +61,21 @@ spec: 8. Click the component in the catalog you should now see Jenkins builds, and a last build result for your master build. +Note: + +If you are not using environment variable then you can directly type API token in app-config.yaml + +```yaml +proxy: + '/jenkins/api': + target: 'http://localhost:8080' # your Jenkins URL + changeOrigin: true + headers: + Authorization: Basic YWRtaW46MTFlYzI1NmU0Mzg1MDFjM2Y1Yzc2Yjc1MWE3ZTQ3YWY4Mw== +``` + +YWRtaW46MTFlYzI1NmU0Mzg1MDFjM2Y1Yzc2Yjc1MWE3ZTQ3YWY4Mw== is the base64 of user and it's API token e.g. admin:11ec256e438501c3f5c76b751a7e47af83 + ## Features - View all runs inside a folder From 406015b0d8b9801a9e6882d8f524cc6963bd941c Mon Sep 17 00:00:00 2001 From: Braxton Christensen Date: Sat, 17 Oct 2020 11:38:20 -0600 Subject: [PATCH 113/139] Fix/1577/item card color contrast (#2921) * fix(layout): adjust ItemCard header colors - adjust header colors to pass color contrast standards * chore(changeset): add changeset -add change set --- .changeset/heavy-berries-hear.md | 5 +++++ packages/core/src/layout/ItemCard/ItemCard.tsx | 3 +-- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 .changeset/heavy-berries-hear.md diff --git a/.changeset/heavy-berries-hear.md b/.changeset/heavy-berries-hear.md new file mode 100644 index 0000000000..66aa6d93e1 --- /dev/null +++ b/.changeset/heavy-berries-hear.md @@ -0,0 +1,5 @@ +--- +'@backstage/core': patch +--- + +Update ItemCard headers to pass color contrast standards. diff --git a/packages/core/src/layout/ItemCard/ItemCard.tsx b/packages/core/src/layout/ItemCard/ItemCard.tsx index db73939fe7..2d425026a5 100644 --- a/packages/core/src/layout/ItemCard/ItemCard.tsx +++ b/packages/core/src/layout/ItemCard/ItemCard.tsx @@ -20,8 +20,7 @@ const useStyles = makeStyles(theme => ({ header: { color: theme.palette.common.white, padding: theme.spacing(2, 2, 6), - backgroundImage: - 'linear-gradient(-137deg, rgb(25, 230, 140) 0%, rgb(29, 127, 110) 100%)', + backgroundImage: 'linear-gradient(-137deg, #4BB8A5 0%, #187656 100%)', }, content: { padding: theme.spacing(2), From ebca83d4804d930f99e50194273601986184086c Mon Sep 17 00:00:00 2001 From: Abhishek Jakhar Date: Sat, 17 Oct 2020 21:56:42 +0530 Subject: [PATCH 114/139] add test cases for Status components --- .changeset/olive-bikes-jog.md | 5 ++ .../src/components/Status/Status.test.jsx | 69 +++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 .changeset/olive-bikes-jog.md create mode 100644 packages/core/src/components/Status/Status.test.jsx diff --git a/.changeset/olive-bikes-jog.md b/.changeset/olive-bikes-jog.md new file mode 100644 index 0000000000..fdbf1e0d9f --- /dev/null +++ b/.changeset/olive-bikes-jog.md @@ -0,0 +1,5 @@ +--- +'@backstage/core': patch +--- + +add test cases for Status components diff --git a/packages/core/src/components/Status/Status.test.jsx b/packages/core/src/components/Status/Status.test.jsx new file mode 100644 index 0000000000..9a8daecd33 --- /dev/null +++ b/packages/core/src/components/Status/Status.test.jsx @@ -0,0 +1,69 @@ +/* + * 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 { renderInTestApp } from '@backstage/test-utils'; + +import { + StatusOK, + StatusWarning, + StatusError, + StatusPending, + StatusRunning, + StatusAborted, +} from './Status'; + +describe('', () => { + it('renders without exploding', async () => { + const { getByLabelText } = await renderInTestApp(); + expect(getByLabelText('Status ok')).toBeInTheDocument(); + }); +}); + +describe('', () => { + it('renders without exploding', async () => { + const { getByLabelText } = await renderInTestApp(); + expect(getByLabelText('Status warning')).toBeInTheDocument(); + }); +}); + +describe('', () => { + it('renders without exploding', async () => { + const { getByLabelText } = await renderInTestApp(); + expect(getByLabelText('Status error')).toBeInTheDocument(); + }); +}); + +describe('', () => { + it('renders without exploding', async () => { + const { getByLabelText } = await renderInTestApp(); + expect(getByLabelText('Status pending')).toBeInTheDocument(); + }); +}); + +describe('', () => { + it('renders without exploding', async () => { + const { getByLabelText } = await renderInTestApp(); + expect(getByLabelText('Status running')).toBeInTheDocument(); + }); +}); + +describe('', () => { + it('renders without exploding', async () => { + const { getByLabelText } = await renderInTestApp(); + expect(getByLabelText('Status aborted')).toBeInTheDocument(); + }); +}); From c6ff101a99194be011225283c07088136543a25c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20=C3=85lund?= Date: Sat, 17 Oct 2020 20:38:40 +0200 Subject: [PATCH 115/139] UX improvements to Settings --- .../components/AuthProviders/AuthProviders.tsx | 2 +- .../AuthProviders/ProviderSettingsItem.tsx | 16 +++++++--------- .../src/components/FeatureFlags/FeatureFlags.tsx | 2 +- .../components/FeatureFlags/FeatureFlagsItem.tsx | 15 ++++++--------- .../src/components/General/General.tsx | 8 ++++---- 5 files changed, 19 insertions(+), 24 deletions(-) diff --git a/plugins/user-settings/src/components/AuthProviders/AuthProviders.tsx b/plugins/user-settings/src/components/AuthProviders/AuthProviders.tsx index 563bf44150..bc2a68578d 100644 --- a/plugins/user-settings/src/components/AuthProviders/AuthProviders.tsx +++ b/plugins/user-settings/src/components/AuthProviders/AuthProviders.tsx @@ -37,7 +37,7 @@ export const AuthProviders = ({ providerSettings }: Props) => { } return ( - + {providers} ); diff --git a/plugins/user-settings/src/components/AuthProviders/ProviderSettingsItem.tsx b/plugins/user-settings/src/components/AuthProviders/ProviderSettingsItem.tsx index 7213ee2b83..4bae3dbfbb 100644 --- a/plugins/user-settings/src/components/AuthProviders/ProviderSettingsItem.tsx +++ b/plugins/user-settings/src/components/AuthProviders/ProviderSettingsItem.tsx @@ -22,14 +22,13 @@ import { SessionState, } from '@backstage/core'; import { + Button, ListItem, ListItemIcon, ListItemSecondaryAction, ListItemText, Tooltip, } from '@material-ui/core'; -import PowerButton from '@material-ui/icons/PowerSettingsNew'; -import { ToggleButton } from '@material-ui/lab'; type Props = { title: string; @@ -84,14 +83,13 @@ export const ProviderSettingsItem = ({ arrow title={signedIn ? `Sign out from ${title}` : `Sign in to ${title}`} > - (signedIn ? api.signOut() : api.signIn())} +
diff --git a/plugins/user-settings/src/components/FeatureFlags/FeatureFlags.tsx b/plugins/user-settings/src/components/FeatureFlags/FeatureFlags.tsx index 71f59c52b8..e0f4d6f40e 100644 --- a/plugins/user-settings/src/components/FeatureFlags/FeatureFlags.tsx +++ b/plugins/user-settings/src/components/FeatureFlags/FeatureFlags.tsx @@ -62,7 +62,7 @@ export const FeatureFlags = () => { } return ( - + {featureFlags.map(featureFlag => { const enabled = Boolean(state[featureFlag.name]); diff --git a/plugins/user-settings/src/components/FeatureFlags/FeatureFlagsItem.tsx b/plugins/user-settings/src/components/FeatureFlags/FeatureFlagsItem.tsx index 92065f2ed6..460a15f56b 100644 --- a/plugins/user-settings/src/components/FeatureFlags/FeatureFlagsItem.tsx +++ b/plugins/user-settings/src/components/FeatureFlags/FeatureFlagsItem.tsx @@ -19,10 +19,9 @@ import { ListItem, ListItemSecondaryAction, ListItemText, + Switch, Tooltip, } from '@material-ui/core'; -import CheckIcon from '@material-ui/icons/CheckCircle'; -import { ToggleButton } from '@material-ui/lab'; import { FeatureFlagsRegistryItem } from '@backstage/core'; type Props = { @@ -39,14 +38,12 @@ export const FlagItem = ({ flag, enabled, toggleHandler }: Props) => ( /> - toggleHandler(flag.name)} - > - - + name={flag.name} + /> diff --git a/plugins/user-settings/src/components/General/General.tsx b/plugins/user-settings/src/components/General/General.tsx index 00ac17dfe1..424f352502 100644 --- a/plugins/user-settings/src/components/General/General.tsx +++ b/plugins/user-settings/src/components/General/General.tsx @@ -21,12 +21,12 @@ import { Profile } from './Profile'; import { ThemeToggle } from './ThemeToggle'; export const General = () => ( - - + + - - + + From 3af02a945700c908515ecdb8df703d4bb9de5506 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20=C3=85lund?= Date: Sat, 17 Oct 2020 20:42:18 +0200 Subject: [PATCH 116/139] Trendlines noPadding in tables (#2945) --- packages/core/src/components/TrendLine/TrendLine.stories.tsx | 2 +- plugins/lighthouse/src/components/AuditList/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/components/TrendLine/TrendLine.stories.tsx b/packages/core/src/components/TrendLine/TrendLine.stories.tsx index 4eb74b2f17..5d2972095a 100644 --- a/packages/core/src/components/TrendLine/TrendLine.stories.tsx +++ b/packages/core/src/components/TrendLine/TrendLine.stories.tsx @@ -67,7 +67,7 @@ const columns = [ export const Default = () => (
- +
= () => { - {content} + {content} From 4df45e0083d6c94b7a8586ee22574630e266e0ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20=C3=85lund?= Date: Sat, 17 Oct 2020 20:54:47 +0200 Subject: [PATCH 117/139] (docs) Add Helm charts, cleanup (#2944) * (docs) Add Helm charts, cleanup * (docs) Add Helm charts, cleanup --- .github/styles/vocab.txt | 1 + DEPLOYMENT.md | 27 ------------- docs/FAQ.md | 10 +---- docs/getting-started/deployment-helm.md | 15 +++----- docs/getting-started/deployment-other.md | 30 +++++++++++++++ docs/getting-started/installation.md | 7 ---- docs/overview/roadmap.md | 49 +++++++++++++----------- microsite/sidebars.json | 1 + mkdocs.yml | 2 +- 9 files changed, 67 insertions(+), 75 deletions(-) delete mode 100644 DEPLOYMENT.md delete mode 100644 docs/getting-started/installation.md diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index 40346fc0e8..0cbd0ffa05 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -68,6 +68,7 @@ graphviz Hackathons haproxy heroku +Heroku horizontalpodautoscalers Hostname http diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md deleted file mode 100644 index 1aebbeb0eb..0000000000 --- a/DEPLOYMENT.md +++ /dev/null @@ -1,27 +0,0 @@ -# Deploying Backstage - -## Heroku - -Deploying to heroku is relatively easy following these steps. - -First, make sure you have the [heroku CLI installed](https://devcenter.heroku.com/articles/heroku-cli) and log into it as well as login into Heroku's [container registry](https://devcenter.heroku.com/articles/container-registry-and-runtime). - -```bash -$ heroku login -$ heroku container:login -``` - -You _might_ also need to set your Heroku app's stack to `container` - -```bash -$ heroku stack:set container -a -``` - -We can now build/push the Docker image to Heroku's container registry and release it to the `web` worker. - -```bash -$ heroku container:push web -a -$ heroku container:release web -a -``` - -With that, you should have Backstage up and running! diff --git a/docs/FAQ.md b/docs/FAQ.md index 355cf97880..7163d955c7 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -178,8 +178,7 @@ Spotify will maintain the open source core, but we envision different parts of the project being maintained by various companies and contributors. We also envision a large, diverse ecosystem of open source plugins, which would be maintained by their original authors/contributors or by the community. When it -comes to -[deployment](https://github.com/spotify/backstage/blob/master/DEPLOYMENT.md), +comes to [deployment](https://backstage.io/docs/getting-started/deployment-k8s), the system integrator (typically, the infrastructure team in your organization) maintains Backstage in your own environment. @@ -187,7 +186,7 @@ maintains Backstage in your own environment. No, this is not a service offering. We build the piece of software, and someone in your infrastructure team is responsible for -[deploying](https://github.com/spotify/backstage/blob/master/DEPLOYMENT.md) and +[deploying](https://backstage.io/docs/getting-started/deployment-k8s) and maintaining it. ### How secure is Backstage? @@ -230,8 +229,3 @@ write an open source plugin for Backstage, like this See all the ways you can [contribute here](https://github.com/spotify/backstage/blob/master/CONTRIBUTING.md). We'd love to have you as part of the community. - -### Can I join the Backstage team? - -If you're interested in being part of the Backstage team, reach out to -[fossopportunities@spotify.com](mailto:fossopportunities@spotify.com) diff --git a/docs/getting-started/deployment-helm.md b/docs/getting-started/deployment-helm.md index bc239d9306..e674d28742 100644 --- a/docs/getting-started/deployment-helm.md +++ b/docs/getting-started/deployment-helm.md @@ -2,17 +2,18 @@ id: deployment-helm title: Helm description: Documentation on Kubernetes and Helm Deployment +sidebar_label: Kubernetes and Helm --- # Helm charts -An example Backstage app can be deployed in Kubernetes using the [Backstage Helm -charts][backstage-helm-charts] +An example Backstage app can be deployed in Kubernetes using the +[Backstage Helm charts](https://github.com/spotify/backstage/tree/master/contrib/chart/backstage) First, choose a DNS name where backstage will be hosted create a yaml file for your custom configuration. -``` +```yaml appConfig: app: baseUrl: https://backstage.mydomain.com @@ -26,7 +27,6 @@ appConfig: techdocs: storageUrl: https://backstage.mydomain.com/api/techdocs/static/docs requestUrl: https://backstage.mydomain.com/api/techdocs - ``` Then use it to run: @@ -61,9 +61,4 @@ backstage-ingress * 123.1.2.3 80 17m > **NOTE**: this is not a production ready deployment. For more information on how to customize the deployment check the -[README][charts-readme]. - -[backstage-helm-charts]: - (https://github.com/spotify/backstage/tree/master/contrib/chart/backstage) -[charts-readme]: - (https://github.com/spotify/backstage/tree/master/contrib/chart/backstage/README.md) +[README](https://github.com/spotify/backstage/tree/master/contrib/chart/backstage/README.md). diff --git a/docs/getting-started/deployment-other.md b/docs/getting-started/deployment-other.md index 509bbdc978..3e06e17c77 100644 --- a/docs/getting-started/deployment-other.md +++ b/docs/getting-started/deployment-other.md @@ -17,3 +17,33 @@ $ docker run --rm -it -p 7000:7000 -e APP_ENV=production -e NODE_ENV=development ``` Then open http://localhost/ on your browser. + +## Heroku + +Deploying to Heroku is relatively easy following these steps. + +First, make sure you have the +[heroku CLI installed](https://devcenter.heroku.com/articles/heroku-cli) and log +into it as well as login into Heroku's +[container registry](https://devcenter.heroku.com/articles/container-registry-and-runtime). + +```bash +$ heroku login +$ heroku container:login +``` + +You _might_ also need to set your Heroku app's stack to `container` + +```bash +$ heroku stack:set container -a +``` + +We can now build/push the Docker image to Heroku's container registry and +release it to the `web` worker. + +```bash +$ heroku container:push web -a +$ heroku container:release web -a +``` + +With that, you should have Backstage up and running! diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md deleted file mode 100644 index 3b11d2b8e1..0000000000 --- a/docs/getting-started/installation.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -id: installation -title: Installation -description: Documentation on Installation ---- - -Coming soon! diff --git a/docs/overview/roadmap.md b/docs/overview/roadmap.md index 59505aa915..51f09347ce 100644 --- a/docs/overview/roadmap.md +++ b/docs/overview/roadmap.md @@ -47,40 +47,29 @@ guidelines to get started. ### Ongoing work 🚧 -- **[Plugins for managing micro services end-2-end](https://github.com/spotify/backstage/milestone/14)** - +- **[Plugins for managing micro services end-2-end](https://github.com/spotify/backstage/milestone/14)** Out of the box Backstage will ship with a set of plugins (Overview, CI, API and Docs) that will demonstrate how a user can manage a micro service and follow a change all the way out in production. Completing this work will make it much easier to see how a plugin can be built that integrates with the Backstage Service Catalog. -- **[Kubernetes support](https://github.com/spotify/backstage/milestone/20)** - - Native support for Kubernetes, making it easier for developers to see and - manage their services running in k8s. - -- **[Helm charts](https://github.com/spotify/backstage/issues/2540)** - Provide - Helm charts for easy deployments of Backstage and its subsystems on - Kubernetes. +- **[Users and teams](https://github.com/spotify/backstage/issues/1807)** + Ownership is a central concept in Backstage. It should be easy to import your + existing organizational data, such as users and groups/teams, into Backstage. + A user the logs into Backstage should see software components owned by the + team(s) they are in. - **[Backstage platform is stable](https://github.com/spotify/backstage/milestone/19)** - The platform APIs and features are stable and can be depended on for production use. After this plugins will require little to no maintenance. -- Further improvements to platform documentation +* **[Improved Kubernetes plugin](https://github.com/spotify/backstage/issues/2857)** - + Native support for Kubernetes, making it easier for developers to see and + manage their services running in k8s. -### Plugins - -Building and maintaining [plugins](https://backstage.io/plugins) is the work of -the entire Backstage community. - -A list of plugins that are in development is -[available here](https://github.com/spotify/backstage/issues?q=is%3Aissue+is%3Aopen+label%3Aplugin+sort%3Areactions-%2B1-desc). -We strongly recommend to upvote 👍 plugins you are interested in. This helps us -and the community prioritize what plugins to build. - -Are you missing a plugin for your favorite tool? Please -[suggest a new one](https://github.com/spotify/backstage/issues/new?labels=plugin&template=plugin_template.md&title=%5BPlugin%5D+THE+PLUGIN+NAME). -Chances are that someone will jump in and help build it. +- Further improvements to platform documentation. Examples include a Golden Path + for plugin development. ### Future work 🔮 @@ -107,8 +96,24 @@ Chances are that someone will jump in and help build it. query. Plugins can easily query this API as well as extend the model where needed. +### Plugins + +Building and maintaining [plugins](https://backstage.io/plugins) is the work of +the entire Backstage community. + +A list of plugins that are in development is +[available here](https://github.com/spotify/backstage/issues?q=is%3Aissue+is%3Aopen+label%3Aplugin+sort%3Areactions-%2B1-desc). +We strongly recommend to upvote 👍 plugins you are interested in. This helps us +and the community prioritize what plugins to build. + +Are you missing a plugin for your favorite tool? Please +[suggest a new one](https://github.com/spotify/backstage/issues/new?labels=plugin&template=plugin_template.md&title=%5BPlugin%5D+THE+PLUGIN+NAME). +Chances are that someone will jump in and help build it. + ### Completed milestones ✅ +- [Kubernetes plugin - v1](https://github.com/spotify/backstage/tree/master/plugins/kubernetes) +- [Helm charts](https://github.com/spotify/backstage/tree/master/contrib/chart/backstage) - [Backstage Design System 💅](https://backstage.io/blog/2020/09/30/backstage-design-system) - [Cost Insights plugin 💸](https://engineering.atspotify.com/2020/09/29/managing-clouds-from-the-ground-up-cost-engineering-at-spotify/) - [Donate Backstage to the CNCF 🎉](https://backstage.io/blog/2020/09/23/backstage-cncf-sandbox) diff --git a/microsite/sidebars.json b/microsite/sidebars.json index ad48fb1e7c..64fdd2f420 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -28,6 +28,7 @@ "label": "Deployment", "ids": [ "getting-started/deployment-k8s", + "getting-started/deployment-helm", "getting-started/deployment-other" ] } diff --git a/mkdocs.yml b/mkdocs.yml index b4606131a7..8a4a78ddf1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -13,7 +13,6 @@ nav: - Getting started: - Getting Started: 'getting-started/index.md' - Running Backstage locally: 'getting-started/running-backstage-locally.md' - - Installation: 'getting-started/installation.md' - Local development: 'getting-started/development-environment.md' - Demo deployment: https://backstage-demo.roadie.io - Production deployments: @@ -23,6 +22,7 @@ nav: - Customize the look-and-feel of your App: 'getting-started/app-custom-theme.md' - Deployment scenarios: - Kubernetes: 'getting-started/deployment-k8s.md' + - Kubernetes and Helm: 'getting-started/deployment-helm.md' - Other: 'getting-started/deployment-other.md' - Features: - Software Catalog: From 7a2f8b04cd1e2bae5db4071f62cfa6172155f3f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20=C3=85lund?= Date: Sat, 17 Oct 2020 21:19:53 +0200 Subject: [PATCH 118/139] Review comments --- .../src/components/General/General.tsx | 4 +-- .../src/components/General/PinButton.tsx | 25 ++++++------------- .../src/components/General/ThemeToggle.tsx | 12 +++++++-- 3 files changed, 19 insertions(+), 22 deletions(-) diff --git a/plugins/user-settings/src/components/General/General.tsx b/plugins/user-settings/src/components/General/General.tsx index 424f352502..b6d30d0103 100644 --- a/plugins/user-settings/src/components/General/General.tsx +++ b/plugins/user-settings/src/components/General/General.tsx @@ -22,10 +22,10 @@ import { ThemeToggle } from './ThemeToggle'; export const General = () => ( - + - + diff --git a/plugins/user-settings/src/components/General/PinButton.tsx b/plugins/user-settings/src/components/General/PinButton.tsx index e36fc365f8..181574fccf 100644 --- a/plugins/user-settings/src/components/General/PinButton.tsx +++ b/plugins/user-settings/src/components/General/PinButton.tsx @@ -19,18 +19,11 @@ import { ListItem, ListItemSecondaryAction, ListItemText, + Switch, Tooltip, } from '@material-ui/core'; -import LockIcon from '@material-ui/icons/Lock'; -import LockOpenIcon from '@material-ui/icons/LockOpen'; -import { ToggleButton } from '@material-ui/lab'; import { SidebarPinStateContext } from '@backstage/core'; -type PinIconProps = { isPinned: boolean }; - -const PinIcon = ({ isPinned }: PinIconProps) => - isPinned ? : ; - export const PinButton = () => { const { isPinned, toggleSidebarPinState } = useContext( SidebarPinStateContext, @@ -48,16 +41,12 @@ export const PinButton = () => { arrow title={`${isPinned ? 'Unpin' : 'Pin'} Sidebar`} > - { - toggleSidebarPinState(); - }} - > - - + toggleSidebarPinState()} + name="pin" + /> diff --git a/plugins/user-settings/src/components/General/ThemeToggle.tsx b/plugins/user-settings/src/components/General/ThemeToggle.tsx index 111bd838a8..dc24a61f8f 100644 --- a/plugins/user-settings/src/components/General/ThemeToggle.tsx +++ b/plugins/user-settings/src/components/General/ThemeToggle.tsx @@ -102,12 +102,20 @@ export const ThemeToggle = () => { title={`Select ${theme.title}`} value={theme.variant} > - + <> + {theme.variant}  + + ); })} - + + Auto  From 014bc250b379ae94fa263a27f86c544583d027ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Gomez?= Date: Sat, 10 Oct 2020 15:12:49 +0200 Subject: [PATCH 119/139] Fix API references parsing Allows API entities to use a different namespace than the default one. --- .../src/components/useComponentApiEntities.ts | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/plugins/api-docs/src/components/useComponentApiEntities.ts b/plugins/api-docs/src/components/useComponentApiEntities.ts index 11b5de988f..9e5cbd968e 100644 --- a/plugins/api-docs/src/components/useComponentApiEntities.ts +++ b/plugins/api-docs/src/components/useComponentApiEntities.ts @@ -16,7 +16,11 @@ import { useAsyncRetry } from 'react-use'; import { errorApiRef, useApi } from '@backstage/core'; -import { ApiEntity, ComponentEntity } from '@backstage/catalog-model'; +import { + ApiEntity, + ComponentEntity, + parseEntityName, +} from '@backstage/catalog-model'; import { catalogApiRef } from '@backstage/plugin-catalog'; import { useComponentApiNames } from './useComponentApiNames'; @@ -43,10 +47,20 @@ export function useComponentApiEntities({ await Promise.all( apiNames.map(async name => { try { - const api = (await catalogApi.getEntityByName({ - kind: 'API', - name, - })) as ApiEntity | undefined; + const apiEntityName = parseEntityName(name, { + defaultNamespace: entity.metadata.namespace, + defaultKind: 'API', + }); + + if (apiEntityName.kind !== 'API') { + throw new Error( + `Referenced entity of kind "${apiEntityName.kind}" as an API`, + ); + } + + const api = (await catalogApi.getEntityByName(apiEntityName)) as + | ApiEntity + | undefined; if (api) { resultMap.set(api.metadata.name, api); From d90a8109c276423c386fde4e35182042d4e8e043 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20=C3=85lund?= Date: Sun, 18 Oct 2020 07:53:22 +0200 Subject: [PATCH 120/139] Fix test? --- .../user-settings/src/components/General/PinButton.test.tsx | 3 +-- plugins/user-settings/src/components/General/PinButton.tsx | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/user-settings/src/components/General/PinButton.test.tsx b/plugins/user-settings/src/components/General/PinButton.test.tsx index 76af8ce8d4..88f636d918 100644 --- a/plugins/user-settings/src/components/General/PinButton.test.tsx +++ b/plugins/user-settings/src/components/General/PinButton.test.tsx @@ -32,10 +32,9 @@ describe('', () => { , ), ); - expect(rendered.getByText('Pin Sidebar')).toBeInTheDocument(); - const pinButton = rendered.getByTitle('Pin Sidebar'); + const pinButton = rendered.getByTestId('pin'); fireEvent.click(pinButton); expect(mockToggleFn).toHaveBeenCalled(); }); diff --git a/plugins/user-settings/src/components/General/PinButton.tsx b/plugins/user-settings/src/components/General/PinButton.tsx index 181574fccf..bfcdb0c7ff 100644 --- a/plugins/user-settings/src/components/General/PinButton.tsx +++ b/plugins/user-settings/src/components/General/PinButton.tsx @@ -46,6 +46,7 @@ export const PinButton = () => { checked={isPinned} onChange={() => toggleSidebarPinState()} name="pin" + data-test-id="pin" /> From c3113d1d39ddc6dd0b3a44ecad5e56d235232a40 Mon Sep 17 00:00:00 2001 From: Eduardo Apolinario Date: Sat, 17 Oct 2020 23:13:06 -0700 Subject: [PATCH 121/139] Fix Core Features configuration id (#2948) The current configuration id is pointing to an inexistent place. This PR fixes the name of the configuration node in sidebars.json. --- docs/features/software-catalog/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/software-catalog/configuration.md b/docs/features/software-catalog/configuration.md index 46de2d3d67..dd19d4cd51 100644 --- a/docs/features/software-catalog/configuration.md +++ b/docs/features/software-catalog/configuration.md @@ -1,5 +1,5 @@ --- -id: software-catalog-configuration +id: configuration title: Catalog Configuration description: Documentation on Software Catalog Configuration --- From 763ec1cd2f9c62fd4a7fb62fe0f41c1dfaaac832 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20=C3=85lund?= Date: Sun, 18 Oct 2020 08:20:06 +0200 Subject: [PATCH 122/139] Another try --- plugins/user-settings/src/components/General/PinButton.test.tsx | 2 +- plugins/user-settings/src/components/General/PinButton.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/user-settings/src/components/General/PinButton.test.tsx b/plugins/user-settings/src/components/General/PinButton.test.tsx index 88f636d918..d85fa3ddd9 100644 --- a/plugins/user-settings/src/components/General/PinButton.test.tsx +++ b/plugins/user-settings/src/components/General/PinButton.test.tsx @@ -35,7 +35,7 @@ describe('', () => { expect(rendered.getByText('Pin Sidebar')).toBeInTheDocument(); const pinButton = rendered.getByTestId('pin'); - fireEvent.click(pinButton); + fireEvent.change(pinButton); expect(mockToggleFn).toHaveBeenCalled(); }); }); diff --git a/plugins/user-settings/src/components/General/PinButton.tsx b/plugins/user-settings/src/components/General/PinButton.tsx index bfcdb0c7ff..a812079133 100644 --- a/plugins/user-settings/src/components/General/PinButton.tsx +++ b/plugins/user-settings/src/components/General/PinButton.tsx @@ -47,6 +47,7 @@ export const PinButton = () => { onChange={() => toggleSidebarPinState()} name="pin" data-test-id="pin" + inputProps={{ 'aria-label': 'Pin Sidebar Switch' }} /> From edeb5e45a304bb4c1fe5a2322bead66ec101aab3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matic=20Ples=CC=8Cec?= Date: Sun, 18 Oct 2020 18:19:09 +0200 Subject: [PATCH 123/139] Update docs regarding npm config ignore-scripts flag --- docs/getting-started/create-an-app.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/getting-started/create-an-app.md b/docs/getting-started/create-an-app.md index 9be0e24452..670c7fbf98 100644 --- a/docs/getting-started/create-an-app.md +++ b/docs/getting-started/create-an-app.md @@ -124,3 +124,21 @@ the root directory: ```bash yarn workspace backend start ``` + +### Troubleshooting + +#### Cannot find module + +You may encounter an error similar to below: + +``` +internal/modules/cjs/loader.js:968 + throw err; + ^ + +Error: Cannot find module '../build/Debug/nodegit.node' +``` + +This can occur if an npm dependency is not completely installed. Because some +dependencies run a post-install script, ensure that both your npm and yarn +configs have the `ignore-scripts` flag set to `false`. From f2819b2a9f1d8f25ff4061eb53b9daefaa384d76 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 19 Oct 2020 01:32:35 +0200 Subject: [PATCH 124/139] core-api: move utility api system implementation into apis/system --- .../core-api/src/apis/definitions/AlertApi.ts | 2 +- .../src/apis/definitions/AppThemeApi.ts | 2 +- .../src/apis/definitions/ConfigApi.ts | 2 +- .../src/apis/definitions/DiscoveryApi.ts | 2 +- .../core-api/src/apis/definitions/ErrorApi.ts | 2 +- .../src/apis/definitions/FeatureFlagsApi.ts | 2 +- .../src/apis/definitions/IdentityApi.ts | 2 +- .../src/apis/definitions/OAuthRequestApi.ts | 2 +- .../src/apis/definitions/StorageApi.ts | 2 +- .../core-api/src/apis/definitions/auth.ts | 4 ++-- packages/core-api/src/apis/index.ts | 6 +---- .../apis/{ => system}/ApiAggregator.test.ts | 0 .../src/apis/{ => system}/ApiAggregator.ts | 3 +-- .../{ => system}/ApiFactoryRegistry.test.ts | 0 .../apis/{ => system}/ApiFactoryRegistry.ts | 2 +- .../apis/{ => system}/ApiProvider.test.tsx | 0 .../src/apis/{ => system}/ApiProvider.tsx | 3 +-- .../src/apis/{ => system}/ApiRef.test.ts | 0 .../core-api/src/apis/{ => system}/ApiRef.ts | 8 ++----- .../src/apis/{ => system}/ApiRegistry.test.ts | 0 .../src/apis/{ => system}/ApiRegistry.ts | 3 +-- .../src/apis/{ => system}/ApiResolver.test.ts | 0 .../src/apis/{ => system}/ApiResolver.ts | 2 +- .../core-api/src/apis/{ => system}/helpers.ts | 3 +-- packages/core-api/src/apis/system/index.ts | 23 +++++++++++++++++++ .../core-api/src/apis/{ => system}/types.ts | 6 ++++- packages/core-api/src/app/App.tsx | 3 +-- packages/core-api/src/plugin/types.ts | 2 +- 28 files changed, 50 insertions(+), 36 deletions(-) rename packages/core-api/src/apis/{ => system}/ApiAggregator.test.ts (100%) rename packages/core-api/src/apis/{ => system}/ApiAggregator.ts (93%) rename packages/core-api/src/apis/{ => system}/ApiFactoryRegistry.test.ts (100%) rename packages/core-api/src/apis/{ => system}/ApiFactoryRegistry.ts (98%) rename packages/core-api/src/apis/{ => system}/ApiProvider.test.tsx (100%) rename packages/core-api/src/apis/{ => system}/ApiProvider.tsx (96%) rename packages/core-api/src/apis/{ => system}/ApiRef.test.ts (100%) rename packages/core-api/src/apis/{ => system}/ApiRef.ts (95%) rename packages/core-api/src/apis/{ => system}/ApiRegistry.test.ts (100%) rename packages/core-api/src/apis/{ => system}/ApiRegistry.ts (96%) rename packages/core-api/src/apis/{ => system}/ApiResolver.test.ts (100%) rename packages/core-api/src/apis/{ => system}/ApiResolver.ts (98%) rename packages/core-api/src/apis/{ => system}/helpers.ts (93%) create mode 100644 packages/core-api/src/apis/system/index.ts rename packages/core-api/src/apis/{ => system}/types.ts (94%) diff --git a/packages/core-api/src/apis/definitions/AlertApi.ts b/packages/core-api/src/apis/definitions/AlertApi.ts index 123cd47651..d149940103 100644 --- a/packages/core-api/src/apis/definitions/AlertApi.ts +++ b/packages/core-api/src/apis/definitions/AlertApi.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createApiRef } from '../ApiRef'; +import { createApiRef } from '../system'; import { Observable } from '../../types'; export type AlertMessage = { diff --git a/packages/core-api/src/apis/definitions/AppThemeApi.ts b/packages/core-api/src/apis/definitions/AppThemeApi.ts index 3f580450f0..019aa2f496 100644 --- a/packages/core-api/src/apis/definitions/AppThemeApi.ts +++ b/packages/core-api/src/apis/definitions/AppThemeApi.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createApiRef } from '../ApiRef'; +import { createApiRef } from '../system'; import { BackstageTheme } from '@backstage/theme'; import { Observable } from '../../types'; import { SvgIconProps } from '@material-ui/core'; diff --git a/packages/core-api/src/apis/definitions/ConfigApi.ts b/packages/core-api/src/apis/definitions/ConfigApi.ts index 1fa6e70d1f..6d34055f05 100644 --- a/packages/core-api/src/apis/definitions/ConfigApi.ts +++ b/packages/core-api/src/apis/definitions/ConfigApi.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createApiRef } from '../ApiRef'; +import { createApiRef } from '../system'; import { Config } from '@backstage/config'; // Using interface to make the ConfigApi name show up in docs diff --git a/packages/core-api/src/apis/definitions/DiscoveryApi.ts b/packages/core-api/src/apis/definitions/DiscoveryApi.ts index b0773086c7..13ead7b929 100644 --- a/packages/core-api/src/apis/definitions/DiscoveryApi.ts +++ b/packages/core-api/src/apis/definitions/DiscoveryApi.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createApiRef } from '../ApiRef'; +import { createApiRef } from '../system'; /** * The discovery API is used to provide a mechanism for plugins to diff --git a/packages/core-api/src/apis/definitions/ErrorApi.ts b/packages/core-api/src/apis/definitions/ErrorApi.ts index 7f9676ea5a..365f59f4a3 100644 --- a/packages/core-api/src/apis/definitions/ErrorApi.ts +++ b/packages/core-api/src/apis/definitions/ErrorApi.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createApiRef } from '../ApiRef'; +import { createApiRef } from '../system'; import { Observable } from '../../types'; /** diff --git a/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts b/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts index dd9657d7d8..0b7cddfe15 100644 --- a/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts +++ b/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createApiRef } from '../ApiRef'; +import { createApiRef } from '../system'; import { UserFlags, FeatureFlagsRegistry } from '../../app/FeatureFlags'; import { FeatureFlagName } from '../../plugin'; diff --git a/packages/core-api/src/apis/definitions/IdentityApi.ts b/packages/core-api/src/apis/definitions/IdentityApi.ts index 2684422b1e..87dc4ca989 100644 --- a/packages/core-api/src/apis/definitions/IdentityApi.ts +++ b/packages/core-api/src/apis/definitions/IdentityApi.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createApiRef } from '../ApiRef'; +import { createApiRef } from '../system'; import { ProfileInfo } from './auth'; /** diff --git a/packages/core-api/src/apis/definitions/OAuthRequestApi.ts b/packages/core-api/src/apis/definitions/OAuthRequestApi.ts index 6fefbd1b9c..7bbebf90c1 100644 --- a/packages/core-api/src/apis/definitions/OAuthRequestApi.ts +++ b/packages/core-api/src/apis/definitions/OAuthRequestApi.ts @@ -16,7 +16,7 @@ import { IconComponent } from '../../icons'; import { Observable } from '../../types'; -import { createApiRef } from '../ApiRef'; +import { createApiRef } from '../system'; /** * Information about the auth provider that we're requesting a login towards. diff --git a/packages/core-api/src/apis/definitions/StorageApi.ts b/packages/core-api/src/apis/definitions/StorageApi.ts index 920ee56811..923633318b 100644 --- a/packages/core-api/src/apis/definitions/StorageApi.ts +++ b/packages/core-api/src/apis/definitions/StorageApi.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createApiRef } from '../ApiRef'; +import { createApiRef } from '../system'; import { Observable } from '../../types'; import { ErrorApi } from './ErrorApi'; diff --git a/packages/core-api/src/apis/definitions/auth.ts b/packages/core-api/src/apis/definitions/auth.ts index 109f15b26e..5ffe312591 100644 --- a/packages/core-api/src/apis/definitions/auth.ts +++ b/packages/core-api/src/apis/definitions/auth.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { createApiRef } from '../ApiRef'; -import { Observable } from '../..'; +import { createApiRef } from '../system'; +import { Observable } from '../../types'; /** * This file contains declarations for common interfaces of auth-related APIs. diff --git a/packages/core-api/src/apis/index.ts b/packages/core-api/src/apis/index.ts index c856a13668..03569f9570 100644 --- a/packages/core-api/src/apis/index.ts +++ b/packages/core-api/src/apis/index.ts @@ -14,10 +14,6 @@ * limitations under the License. */ -export { ApiProvider, useApi, useApiHolder } from './ApiProvider'; -export { ApiRegistry } from './ApiRegistry'; -export * from './ApiRef'; -export * from './types'; -export * from './helpers'; +export * from './system'; export * from './definitions'; export * from './implementations'; diff --git a/packages/core-api/src/apis/ApiAggregator.test.ts b/packages/core-api/src/apis/system/ApiAggregator.test.ts similarity index 100% rename from packages/core-api/src/apis/ApiAggregator.test.ts rename to packages/core-api/src/apis/system/ApiAggregator.test.ts diff --git a/packages/core-api/src/apis/ApiAggregator.ts b/packages/core-api/src/apis/system/ApiAggregator.ts similarity index 93% rename from packages/core-api/src/apis/ApiAggregator.ts rename to packages/core-api/src/apis/system/ApiAggregator.ts index da49ee6488..1587a1d10b 100644 --- a/packages/core-api/src/apis/ApiAggregator.ts +++ b/packages/core-api/src/apis/system/ApiAggregator.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { ApiRef } from './ApiRef'; -import { ApiHolder } from './types'; +import { ApiRef, ApiHolder } from './types'; /** * An ApiHolder that queries multiple other holders from for diff --git a/packages/core-api/src/apis/ApiFactoryRegistry.test.ts b/packages/core-api/src/apis/system/ApiFactoryRegistry.test.ts similarity index 100% rename from packages/core-api/src/apis/ApiFactoryRegistry.test.ts rename to packages/core-api/src/apis/system/ApiFactoryRegistry.test.ts diff --git a/packages/core-api/src/apis/ApiFactoryRegistry.ts b/packages/core-api/src/apis/system/ApiFactoryRegistry.ts similarity index 98% rename from packages/core-api/src/apis/ApiFactoryRegistry.ts rename to packages/core-api/src/apis/system/ApiFactoryRegistry.ts index 0d36e6c550..556f63589c 100644 --- a/packages/core-api/src/apis/ApiFactoryRegistry.ts +++ b/packages/core-api/src/apis/system/ApiFactoryRegistry.ts @@ -15,12 +15,12 @@ */ import { + ApiRef, ApiFactoryHolder, ApiFactory, AnyApiRef, AnyApiFactory, } from './types'; -import { ApiRef } from './ApiRef'; type ApiFactoryScope = | 'default' // Default factories registered by core and plugins diff --git a/packages/core-api/src/apis/ApiProvider.test.tsx b/packages/core-api/src/apis/system/ApiProvider.test.tsx similarity index 100% rename from packages/core-api/src/apis/ApiProvider.test.tsx rename to packages/core-api/src/apis/system/ApiProvider.test.tsx diff --git a/packages/core-api/src/apis/ApiProvider.tsx b/packages/core-api/src/apis/system/ApiProvider.tsx similarity index 96% rename from packages/core-api/src/apis/ApiProvider.tsx rename to packages/core-api/src/apis/system/ApiProvider.tsx index 24610a1372..f2aa70244e 100644 --- a/packages/core-api/src/apis/ApiProvider.tsx +++ b/packages/core-api/src/apis/system/ApiProvider.tsx @@ -16,8 +16,7 @@ import React, { FC, createContext, useContext, ReactNode } from 'react'; import PropTypes from 'prop-types'; -import { ApiRef } from './ApiRef'; -import { ApiHolder, TypesToApiRefs } from './types'; +import { ApiRef, ApiHolder, TypesToApiRefs } from './types'; import { ApiAggregator } from './ApiAggregator'; type ApiProviderProps = { diff --git a/packages/core-api/src/apis/ApiRef.test.ts b/packages/core-api/src/apis/system/ApiRef.test.ts similarity index 100% rename from packages/core-api/src/apis/ApiRef.test.ts rename to packages/core-api/src/apis/system/ApiRef.test.ts diff --git a/packages/core-api/src/apis/ApiRef.ts b/packages/core-api/src/apis/system/ApiRef.ts similarity index 95% rename from packages/core-api/src/apis/ApiRef.ts rename to packages/core-api/src/apis/system/ApiRef.ts index 793c1e5386..0e1eb177c8 100644 --- a/packages/core-api/src/apis/ApiRef.ts +++ b/packages/core-api/src/apis/system/ApiRef.ts @@ -14,17 +14,13 @@ * limitations under the License. */ +import type { ApiRef } from './types'; + export type ApiRefConfig = { id: string; description: string; }; -export type ApiRef = { - id: string; - description: string; - T: T; -}; - class ApiRefImpl implements ApiRef { constructor(private readonly config: ApiRefConfig) { const valid = config.id diff --git a/packages/core-api/src/apis/ApiRegistry.test.ts b/packages/core-api/src/apis/system/ApiRegistry.test.ts similarity index 100% rename from packages/core-api/src/apis/ApiRegistry.test.ts rename to packages/core-api/src/apis/system/ApiRegistry.test.ts diff --git a/packages/core-api/src/apis/ApiRegistry.ts b/packages/core-api/src/apis/system/ApiRegistry.ts similarity index 96% rename from packages/core-api/src/apis/ApiRegistry.ts rename to packages/core-api/src/apis/system/ApiRegistry.ts index 64d97ba234..14f58268b4 100644 --- a/packages/core-api/src/apis/ApiRegistry.ts +++ b/packages/core-api/src/apis/system/ApiRegistry.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { ApiRef } from './ApiRef'; -import { ApiHolder } from './types'; +import { ApiRef, ApiHolder } from './types'; type ApiImpl = readonly [ApiRef, T]; diff --git a/packages/core-api/src/apis/ApiResolver.test.ts b/packages/core-api/src/apis/system/ApiResolver.test.ts similarity index 100% rename from packages/core-api/src/apis/ApiResolver.test.ts rename to packages/core-api/src/apis/system/ApiResolver.test.ts diff --git a/packages/core-api/src/apis/ApiResolver.ts b/packages/core-api/src/apis/system/ApiResolver.ts similarity index 98% rename from packages/core-api/src/apis/ApiResolver.ts rename to packages/core-api/src/apis/system/ApiResolver.ts index cb65186a37..0dfeaeedd0 100644 --- a/packages/core-api/src/apis/ApiResolver.ts +++ b/packages/core-api/src/apis/system/ApiResolver.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { ApiRef } from './ApiRef'; import { + ApiRef, ApiHolder, ApiFactoryHolder, AnyApiRef, diff --git a/packages/core-api/src/apis/helpers.ts b/packages/core-api/src/apis/system/helpers.ts similarity index 93% rename from packages/core-api/src/apis/helpers.ts rename to packages/core-api/src/apis/system/helpers.ts index 7d616acd01..0ccd0cdb80 100644 --- a/packages/core-api/src/apis/helpers.ts +++ b/packages/core-api/src/apis/system/helpers.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { ApiFactory, TypesToApiRefs } from './types'; -import { ApiRef } from './ApiRef'; +import { ApiRef, ApiFactory, TypesToApiRefs } from './types'; /** * Used to infer types for a standalone ApiFactory that isn't immediately passed diff --git a/packages/core-api/src/apis/system/index.ts b/packages/core-api/src/apis/system/index.ts new file mode 100644 index 0000000000..10b2e0f084 --- /dev/null +++ b/packages/core-api/src/apis/system/index.ts @@ -0,0 +1,23 @@ +/* + * 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 { ApiProvider, useApi, useApiHolder } from './ApiProvider'; +export { ApiRegistry } from './ApiRegistry'; +export { ApiResolver } from './ApiResolver'; +export { ApiFactoryRegistry } from './ApiFactoryRegistry'; +export { createApiRef } from './ApiRef'; +export * from './types'; +export * from './helpers'; diff --git a/packages/core-api/src/apis/types.ts b/packages/core-api/src/apis/system/types.ts similarity index 94% rename from packages/core-api/src/apis/types.ts rename to packages/core-api/src/apis/system/types.ts index 61c229b18e..b0c19e551c 100644 --- a/packages/core-api/src/apis/types.ts +++ b/packages/core-api/src/apis/system/types.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { ApiRef } from './ApiRef'; +export type ApiRef = { + id: string; + description: string; + T: T; +}; export type AnyApiRef = ApiRef; diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx index c222c3c420..0588cd819c 100644 --- a/packages/core-api/src/app/App.tsx +++ b/packages/core-api/src/app/App.tsx @@ -54,8 +54,7 @@ import { } from '../apis'; import { useAsync } from 'react-use'; import { AppIdentity } from './AppIdentity'; -import { ApiFactoryRegistry } from '../apis/ApiFactoryRegistry'; -import { ApiResolver } from '../apis/ApiResolver'; +import { ApiResolver, ApiFactoryRegistry } from '../apis/system'; type FullAppOptions = { apis: Iterable; diff --git a/packages/core-api/src/plugin/types.ts b/packages/core-api/src/plugin/types.ts index 12992f3620..dacb3668bc 100644 --- a/packages/core-api/src/plugin/types.ts +++ b/packages/core-api/src/plugin/types.ts @@ -16,7 +16,7 @@ import { ComponentType } from 'react'; import { RouteRef } from '../routing'; -import { AnyApiFactory } from '../apis'; +import { AnyApiFactory } from '../apis/system'; export type RouteOptions = { // Whether the route path must match exactly, defaults to true. From 1bf3ff96a7f52e05510d461dccad7175a74a3b04 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 19 Oct 2020 01:37:00 +0200 Subject: [PATCH 125/139] core-api: work around issue with ApiRef export const declarations --- .../core-api/src/apis/definitions/AlertApi.ts | 4 +-- .../src/apis/definitions/AppThemeApi.ts | 4 +-- .../src/apis/definitions/ConfigApi.ts | 4 +-- .../src/apis/definitions/DiscoveryApi.ts | 4 +-- .../core-api/src/apis/definitions/ErrorApi.ts | 4 +-- .../src/apis/definitions/FeatureFlagsApi.ts | 4 +-- .../src/apis/definitions/IdentityApi.ts | 4 +-- .../src/apis/definitions/OAuthRequestApi.ts | 4 +-- .../src/apis/definitions/StorageApi.ts | 4 +-- .../core-api/src/apis/definitions/auth.ts | 34 +++++++++---------- 10 files changed, 35 insertions(+), 35 deletions(-) diff --git a/packages/core-api/src/apis/definitions/AlertApi.ts b/packages/core-api/src/apis/definitions/AlertApi.ts index d149940103..91641ac35b 100644 --- a/packages/core-api/src/apis/definitions/AlertApi.ts +++ b/packages/core-api/src/apis/definitions/AlertApi.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createApiRef } from '../system'; +import { createApiRef, ApiRef } from '../system'; import { Observable } from '../../types'; export type AlertMessage = { @@ -38,7 +38,7 @@ export type AlertApi = { alert$(): Observable; }; -export const alertApiRef = createApiRef({ +export const alertApiRef: ApiRef = createApiRef({ id: 'core.alert', description: 'Used to report alerts and forward them to the app', }); diff --git a/packages/core-api/src/apis/definitions/AppThemeApi.ts b/packages/core-api/src/apis/definitions/AppThemeApi.ts index 019aa2f496..515e8df082 100644 --- a/packages/core-api/src/apis/definitions/AppThemeApi.ts +++ b/packages/core-api/src/apis/definitions/AppThemeApi.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createApiRef } from '../system'; +import { ApiRef, createApiRef } from '../system'; import { BackstageTheme } from '@backstage/theme'; import { Observable } from '../../types'; import { SvgIconProps } from '@material-ui/core'; @@ -77,7 +77,7 @@ export type AppThemeApi = { setActiveThemeId(themeId?: string): void; }; -export const appThemeApiRef = createApiRef({ +export const appThemeApiRef: ApiRef = createApiRef({ id: 'core.apptheme', description: 'API Used to configure the app theme, and enumerate options', }); diff --git a/packages/core-api/src/apis/definitions/ConfigApi.ts b/packages/core-api/src/apis/definitions/ConfigApi.ts index 6d34055f05..2ce972af0e 100644 --- a/packages/core-api/src/apis/definitions/ConfigApi.ts +++ b/packages/core-api/src/apis/definitions/ConfigApi.ts @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createApiRef } from '../system'; +import { ApiRef, createApiRef } from '../system'; import { Config } from '@backstage/config'; // Using interface to make the ConfigApi name show up in docs export type ConfigApi = Config; -export const configApiRef = createApiRef({ +export const configApiRef: ApiRef = createApiRef({ id: 'core.config', description: 'Used to access runtime configuration', }); diff --git a/packages/core-api/src/apis/definitions/DiscoveryApi.ts b/packages/core-api/src/apis/definitions/DiscoveryApi.ts index 13ead7b929..9777835ec8 100644 --- a/packages/core-api/src/apis/definitions/DiscoveryApi.ts +++ b/packages/core-api/src/apis/definitions/DiscoveryApi.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createApiRef } from '../system'; +import { ApiRef, createApiRef } from '../system'; /** * The discovery API is used to provide a mechanism for plugins to @@ -41,7 +41,7 @@ export type DiscoveryApi = { getBaseUrl(pluginId: string): Promise; }; -export const discoveryApiRef = createApiRef({ +export const discoveryApiRef: ApiRef = createApiRef({ id: 'core.discovery', description: 'Provides service discovery of backend plugins', }); diff --git a/packages/core-api/src/apis/definitions/ErrorApi.ts b/packages/core-api/src/apis/definitions/ErrorApi.ts index 365f59f4a3..edd6966968 100644 --- a/packages/core-api/src/apis/definitions/ErrorApi.ts +++ b/packages/core-api/src/apis/definitions/ErrorApi.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createApiRef } from '../system'; +import { ApiRef, createApiRef } from '../system'; import { Observable } from '../../types'; /** @@ -62,7 +62,7 @@ export type ErrorApi = { error$(): Observable<{ error: Error; context?: ErrorContext }>; }; -export const errorApiRef = createApiRef({ +export const errorApiRef: ApiRef = createApiRef({ id: 'core.error', description: 'Used to report errors and forward them to the app', }); diff --git a/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts b/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts index 0b7cddfe15..6e8c8cd8fc 100644 --- a/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts +++ b/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createApiRef } from '../system'; +import { ApiRef, createApiRef } from '../system'; import { UserFlags, FeatureFlagsRegistry } from '../../app/FeatureFlags'; import { FeatureFlagName } from '../../plugin'; @@ -57,7 +57,7 @@ export interface FeatureFlagsRegistryItem { name: FeatureFlagName; } -export const featureFlagsApiRef = createApiRef({ +export const featureFlagsApiRef: ApiRef = createApiRef({ id: 'core.featureflags', description: 'Used to toggle functionality in features across Backstage', }); diff --git a/packages/core-api/src/apis/definitions/IdentityApi.ts b/packages/core-api/src/apis/definitions/IdentityApi.ts index 87dc4ca989..5cfc4723e0 100644 --- a/packages/core-api/src/apis/definitions/IdentityApi.ts +++ b/packages/core-api/src/apis/definitions/IdentityApi.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createApiRef } from '../system'; +import { ApiRef, createApiRef } from '../system'; import { ProfileInfo } from './auth'; /** @@ -51,7 +51,7 @@ export type IdentityApi = { signOut(): Promise; }; -export const identityApiRef = createApiRef({ +export const identityApiRef: ApiRef = createApiRef({ id: 'core.identity', description: 'Provides access to the identity of the signed in user', }); diff --git a/packages/core-api/src/apis/definitions/OAuthRequestApi.ts b/packages/core-api/src/apis/definitions/OAuthRequestApi.ts index 7bbebf90c1..7514a88dc1 100644 --- a/packages/core-api/src/apis/definitions/OAuthRequestApi.ts +++ b/packages/core-api/src/apis/definitions/OAuthRequestApi.ts @@ -16,7 +16,7 @@ import { IconComponent } from '../../icons'; import { Observable } from '../../types'; -import { createApiRef } from '../system'; +import { ApiRef, createApiRef } from '../system'; /** * Information about the auth provider that we're requesting a login towards. @@ -127,7 +127,7 @@ export type OAuthRequestApi = { authRequest$(): Observable; }; -export const oauthRequestApiRef = createApiRef({ +export const oauthRequestApiRef: ApiRef = createApiRef({ id: 'core.oauthrequest', description: 'An API for implementing unified OAuth flows in Backstage', }); diff --git a/packages/core-api/src/apis/definitions/StorageApi.ts b/packages/core-api/src/apis/definitions/StorageApi.ts index 923633318b..d0f71ea3a4 100644 --- a/packages/core-api/src/apis/definitions/StorageApi.ts +++ b/packages/core-api/src/apis/definitions/StorageApi.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createApiRef } from '../system'; +import { ApiRef, createApiRef } from '../system'; import { Observable } from '../../types'; import { ErrorApi } from './ErrorApi'; @@ -65,7 +65,7 @@ export interface StorageApi { observe$(key: string): Observable>; } -export const storageApiRef = createApiRef({ +export const storageApiRef: ApiRef = createApiRef({ id: 'core.storage', description: 'Provides the ability to store data which is unique to the user', }); diff --git a/packages/core-api/src/apis/definitions/auth.ts b/packages/core-api/src/apis/definitions/auth.ts index 5ffe312591..946baeabd9 100644 --- a/packages/core-api/src/apis/definitions/auth.ts +++ b/packages/core-api/src/apis/definitions/auth.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createApiRef } from '../system'; +import { ApiRef, createApiRef } from '../system'; import { Observable } from '../../types'; /** @@ -212,13 +212,13 @@ export type SessionApi = { * Note that the ID token payload is only guaranteed to contain the user's numerical Google ID, * email and expiration information. Do not rely on any other fields, as they might not be present. */ -export const googleAuthApiRef = createApiRef< +export const googleAuthApiRef: ApiRef< OAuthApi & OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi ->({ +> = createApiRef({ id: 'core.auth.google', description: 'Provides authentication towards Google APIs and identities', }); @@ -229,9 +229,9 @@ export const googleAuthApiRef = createApiRef< * See https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/ * for a full list of supported scopes. */ -export const githubAuthApiRef = createApiRef< +export const githubAuthApiRef: ApiRef< OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi ->({ +> = createApiRef({ id: 'core.auth.github', description: 'Provides authentication towards GitHub APIs', }); @@ -242,13 +242,13 @@ export const githubAuthApiRef = createApiRef< * See https://developer.okta.com/docs/guides/implement-oauth-for-okta/scopes/ * for a full list of supported scopes. */ -export const oktaAuthApiRef = createApiRef< +export const oktaAuthApiRef: ApiRef< OAuthApi & OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi ->({ +> = createApiRef({ id: 'core.auth.okta', description: 'Provides authentication towards Okta APIs', }); @@ -259,9 +259,9 @@ export const oktaAuthApiRef = createApiRef< * See https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#limiting-scopes-of-a-personal-access-token * for a full list of supported scopes. */ -export const gitlabAuthApiRef = createApiRef< +export const gitlabAuthApiRef: ApiRef< OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi ->({ +> = createApiRef({ id: 'core.auth.gitlab', description: 'Provides authentication towards GitLab APIs', }); @@ -272,9 +272,9 @@ export const gitlabAuthApiRef = createApiRef< * See https://auth0.com/docs/scopes/current/oidc-scopes * for a full list of supported scopes. */ -export const auth0AuthApiRef = createApiRef< +export const auth0AuthApiRef: ApiRef< OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi ->({ +> = createApiRef({ id: 'core.auth.auth0', description: 'Provides authentication towards Auth0 APIs', }); @@ -286,13 +286,13 @@ export const auth0AuthApiRef = createApiRef< * - https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent * - https://docs.microsoft.com/en-us/graph/permissions-reference */ -export const microsoftAuthApiRef = createApiRef< +export const microsoftAuthApiRef: ApiRef< OAuthApi & OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi ->({ +> = createApiRef({ id: 'core.auth.microsoft', description: 'Provides authentication towards Microsoft APIs and identities', }); @@ -300,13 +300,13 @@ export const microsoftAuthApiRef = createApiRef< /** * Provides authentication for custom identity providers. */ -export const oauth2ApiRef = createApiRef< +export const oauth2ApiRef: ApiRef< OAuthApi & OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi ->({ +> = createApiRef({ id: 'core.auth.oauth2', description: 'Example of how to use oauth2 custom provider', }); @@ -314,9 +314,9 @@ export const oauth2ApiRef = createApiRef< /** * Provides authentication for saml based identity providers */ -export const samlAuthApiRef = createApiRef< +export const samlAuthApiRef: ApiRef< ProfileInfoApi & BackstageIdentityApi & SessionApi ->({ +> = createApiRef({ id: 'core.auth.saml', description: 'Example of how to use SAML custom provider', }); From feeceb96dc906ae12626e11c233039a92f64ddf8 Mon Sep 17 00:00:00 2001 From: Abhishek Jakhar Date: Mon, 19 Oct 2020 07:05:34 +0530 Subject: [PATCH 126/139] handle the case where no entities are available to show --- .../UnregisterEntityDialog/UnregisterEntityDialog.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/catalog/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx b/plugins/catalog/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx index e4eec3c21f..258cc38d15 100644 --- a/plugins/catalog/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx +++ b/plugins/catalog/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx @@ -85,7 +85,7 @@ export const UnregisterEntityDialog: FC = ({ {error.toString()} ) : null} - {entities ? ( + {entities?.length ? ( <> This action will unregister the following entities: @@ -107,11 +107,11 @@ export const UnregisterEntityDialog: FC = ({ - - To undo, just re-register the entity in Backstage. - ) : null} + + To undo, just re-register the entity in Backstage. +