From a566cba67171d37ffa6a626616c94692d9bc132b Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Thu, 10 Sep 2020 16:18:24 +0200 Subject: [PATCH 01/22] wip: big commit to add sidebar settings dialog --- .../core/src/layout/Sidebar/PinButton.tsx | 75 ---------------- .../src/layout/Sidebar/Settings/PinButton.tsx | 64 ++++++++++++++ .../Sidebar/Settings/ProviderSettingsItem.tsx | 52 +++++++---- .../Sidebar/Settings/SettingsDialog.tsx | 70 +++++++++++++++ .../layout/Sidebar/Settings/SignInAvatar.tsx | 63 ++++++++++++++ .../layout/Sidebar/Settings/ThemeToggle.tsx | 87 +++++++++++++++++++ .../layout/Sidebar/Settings/UserProfile.tsx | 61 ------------- .../layout/Sidebar/Settings/UserSettings.tsx | 78 +++++++++++++++++ .../Sidebar/Settings/UserSettingsMenu.tsx | 55 ++++++++++++ .../core/src/layout/Sidebar/Settings/index.ts | 3 +- .../Sidebar/Settings/useUserProfileInfo.ts | 26 ++++++ .../src/layout/Sidebar/SidebarThemeToggle.tsx | 58 ------------- .../core/src/layout/Sidebar/UserSettings.tsx | 53 ----------- packages/core/src/layout/Sidebar/index.ts | 7 +- 14 files changed, 486 insertions(+), 266 deletions(-) delete mode 100644 packages/core/src/layout/Sidebar/PinButton.tsx create mode 100644 packages/core/src/layout/Sidebar/Settings/PinButton.tsx create mode 100644 packages/core/src/layout/Sidebar/Settings/SettingsDialog.tsx create mode 100644 packages/core/src/layout/Sidebar/Settings/SignInAvatar.tsx create mode 100644 packages/core/src/layout/Sidebar/Settings/ThemeToggle.tsx delete mode 100644 packages/core/src/layout/Sidebar/Settings/UserProfile.tsx create mode 100644 packages/core/src/layout/Sidebar/Settings/UserSettings.tsx create mode 100644 packages/core/src/layout/Sidebar/Settings/UserSettingsMenu.tsx create mode 100644 packages/core/src/layout/Sidebar/Settings/useUserProfileInfo.ts delete mode 100644 packages/core/src/layout/Sidebar/SidebarThemeToggle.tsx delete mode 100644 packages/core/src/layout/Sidebar/UserSettings.tsx diff --git a/packages/core/src/layout/Sidebar/PinButton.tsx b/packages/core/src/layout/Sidebar/PinButton.tsx deleted file mode 100644 index 8c52eeb24d..0000000000 --- a/packages/core/src/layout/Sidebar/PinButton.tsx +++ /dev/null @@ -1,75 +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, { FC, useContext } from 'react'; -import { makeStyles } from '@material-ui/core'; -import DoubleArrowIcon from '@material-ui/icons/DoubleArrow'; -import { SidebarContext } from './config'; -import { BackstageTheme } from '@backstage/theme'; -import { SidebarPinStateContext } from './Page'; - -const ARROW_BUTTON_SIZE = 20; -const useStyles = makeStyles(theme => { - return { - root: { - position: 'relative', - alignSelf: 'stretch', - }, - arrowButtonWrapper: { - position: 'absolute', - right: 0, - width: ARROW_BUTTON_SIZE, - height: ARROW_BUTTON_SIZE, - top: -(theme.spacing(6) + ARROW_BUTTON_SIZE) / 2, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - borderRadius: '2px 0px 0px 2px', - background: theme.palette.pinSidebarButton.background, - color: theme.palette.pinSidebarButton.icon, - border: 'none', - outline: 'none', - cursor: 'pointer', - }, - arrowButtonIcon: { - transform: ({ isPinned }) => (isPinned ? 'rotate(180deg)' : 'none'), - }, - }; -}); - -export const SidebarPinButton: FC<{}> = () => { - const { isOpen } = useContext(SidebarContext); - const { isPinned, toggleSidebarPinState } = useContext( - SidebarPinStateContext, - ); - const classes = useStyles({ isPinned }); - - return ( -
- {isOpen && ( - - )} -
- ); -}; diff --git a/packages/core/src/layout/Sidebar/Settings/PinButton.tsx b/packages/core/src/layout/Sidebar/Settings/PinButton.tsx new file mode 100644 index 0000000000..dcd0818ad6 --- /dev/null +++ b/packages/core/src/layout/Sidebar/Settings/PinButton.tsx @@ -0,0 +1,64 @@ +/* + * 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, { useContext } from 'react'; +import { + ListItem, + ListItemSecondaryAction, + ListItemText, + 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 '../Page'; + +export const SidebarPinButton = () => { + const { isPinned, toggleSidebarPinState } = useContext( + SidebarPinStateContext, + ); + + const PinIcon = () => ( + + {isPinned ? : } + + ); + + return ( + + + + { + toggleSidebarPinState(); + }} + > + + + + + ); +}; diff --git a/packages/core/src/layout/Sidebar/Settings/ProviderSettingsItem.tsx b/packages/core/src/layout/Sidebar/Settings/ProviderSettingsItem.tsx index 3d3d7439e1..653c64575b 100644 --- a/packages/core/src/layout/Sidebar/Settings/ProviderSettingsItem.tsx +++ b/packages/core/src/layout/Sidebar/Settings/ProviderSettingsItem.tsx @@ -14,31 +14,53 @@ * limitations under the License. */ -import React, { FC } from 'react'; -import { OAuthApi, OpenIdConnectApi, IconComponent } from '@backstage/core-api'; -import { SidebarItem } from '../Items'; -import { IconButton, Tooltip } from '@material-ui/core'; -import StarBorder from '@material-ui/icons/StarBorder'; +import React from 'react'; +import { IconComponent, OAuthApi, OpenIdConnectApi } from '@backstage/core-api'; +import { + ListItem, + ListItemIcon, + ListItemSecondaryAction, + ListItemText, + Tooltip, +} from '@material-ui/core'; import PowerButton from '@material-ui/icons/PowerSettingsNew'; +import { ToggleButton } from '@material-ui/lab'; -export const ProviderSettingsItem: FC<{ +type Props = { title: string; icon: IconComponent; signedIn: boolean; api: OAuthApi | OpenIdConnectApi; signInHandler: Function; -}> = ({ title, icon, signedIn, api, signInHandler }) => { - return ( - - (signedIn ? api.logout() : signInHandler())}> +}; + +export const ProviderSettingsItem = ({ + title, + icon: Icon, + signedIn, + api, + signInHandler, +}: Props) => ( + + + + + + + (signedIn ? api.logout() : signInHandler())} + > - + - - - ); -}; + + + +); diff --git a/packages/core/src/layout/Sidebar/Settings/SettingsDialog.tsx b/packages/core/src/layout/Sidebar/Settings/SettingsDialog.tsx new file mode 100644 index 0000000000..b37053763b --- /dev/null +++ b/packages/core/src/layout/Sidebar/Settings/SettingsDialog.tsx @@ -0,0 +1,70 @@ +/* + * 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 { + Card, + CardContent, + CardHeader, + Divider, + makeStyles, +} from '@material-ui/core'; +import List from '@material-ui/core/List'; +import ListSubheader from '@material-ui/core/ListSubheader'; +import { SidebarPinButton } from './PinButton'; +import { SignInAvatar } from './SignInAvatar'; +import { SidebarThemeToggle } from './ThemeToggle'; +import { UserSettingsMenu } from './UserSettingsMenu'; +import { useUserProfile } from './useUserProfileInfo'; + +const useStyles = makeStyles({ + root: { + minWidth: 400, + }, +}); + +export const SettingsDialog = ({ + providerSettings, +}: { + providerSettings?: React.ReactNode; +}) => { + const classes = useStyles(); + const { profile, displayName } = useUserProfile(); + + return ( + + } + action={} + title={displayName} + subheader={profile.email} + /> + + + App Settings}> + + + + + Available Auth Providers} + > + {providerSettings} + + + + ); +}; diff --git a/packages/core/src/layout/Sidebar/Settings/SignInAvatar.tsx b/packages/core/src/layout/Sidebar/Settings/SignInAvatar.tsx new file mode 100644 index 0000000000..c6eed74a18 --- /dev/null +++ b/packages/core/src/layout/Sidebar/Settings/SignInAvatar.tsx @@ -0,0 +1,63 @@ +/* + * 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 { BackstageTheme } from '@backstage/theme'; +import { makeStyles, Avatar } from '@material-ui/core'; +import { useUserProfile } from './useUserProfileInfo'; + +// const useStyles = makeStyles({ +// avatar: { +// width: ({ size }) => size, +// height: ({ size }) => size, +// }, +// }); + +// export const SignInAvatar = ({ size = 24 }: { size?: number }) => { +// const classes = useStyles({ size }); +// const { profile, displayName } = useUserProfile(); + +// return ( +// +// {displayName[0]} +// +// ); +// }; + +// const useStyles = makeStyles({ +// avatar: { +// width: 24, +// height: 24, +// }, +// }); + +const useStyles = makeStyles({ + avatar: { + width: 24, + height: 24, + }, +}); + +export const SignInAvatar = () => { + const classes = useStyles(); + const { profile, displayName } = useUserProfile(); + + return ( + + {displayName[0]} + + ); +}; diff --git a/packages/core/src/layout/Sidebar/Settings/ThemeToggle.tsx b/packages/core/src/layout/Sidebar/Settings/ThemeToggle.tsx new file mode 100644 index 0000000000..29cfb24885 --- /dev/null +++ b/packages/core/src/layout/Sidebar/Settings/ThemeToggle.tsx @@ -0,0 +1,87 @@ +/* + * 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 { useObservable } from 'react-use'; +import LightIcon from '@material-ui/icons/WbSunny'; +import DarkIcon from '@material-ui/icons/Brightness2'; +import AutoIcon from '@material-ui/icons/BrightnessAuto'; +import { appThemeApiRef, useApi } from '@backstage/core-api'; +import ToggleButton from '@material-ui/lab/ToggleButton'; +import ToggleButtonGroup from '@material-ui/lab/ToggleButtonGroup'; +import { + ListItem, + ListItemText, + ListItemSecondaryAction, + Tooltip, +} from '@material-ui/core'; + +export const SidebarThemeToggle = () => { + const appThemeApi = useApi(appThemeApiRef); + const themeId = useObservable( + appThemeApi.activeThemeId$(), + appThemeApi.getActiveThemeId(), + ); + + const themeIds = appThemeApi.getInstalledThemes(); + // TODO: can these be put on the theme itself? + const themeIcons = { + dark: , + light: , + }; + + const handleSetTheme = ( + _event: React.MouseEvent, + newThemeId: string | undefined, + ) => { + if (themeIds.some(t => t.id === newThemeId)) { + appThemeApi.setActiveThemeId(newThemeId); + } else { + appThemeApi.setActiveThemeId(undefined); + } + }; + + return ( + + + + + {themeIds.map(theme => ( + + + {themeIcons[theme.variant]} + + + ))} + + + + + + + + + ); +}; diff --git a/packages/core/src/layout/Sidebar/Settings/UserProfile.tsx b/packages/core/src/layout/Sidebar/Settings/UserProfile.tsx deleted file mode 100644 index 3b854801c9..0000000000 --- a/packages/core/src/layout/Sidebar/Settings/UserProfile.tsx +++ /dev/null @@ -1,61 +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, { FC, useRef } from 'react'; -import { makeStyles, Avatar, Divider } from '@material-ui/core'; -import { useApi, identityApiRef } from '@backstage/core-api'; -import { SidebarItem } from '../Items'; -import ExpandLess from '@material-ui/icons/ExpandLess'; -import ExpandMore from '@material-ui/icons/ExpandMore'; - -const useStyles = makeStyles({ - avatar: { - width: 24, - height: 24, - }, -}); - -export const UserProfile: FC<{ open: boolean; setOpen: Function }> = ({ - open, - setOpen, -}) => { - const ref = useRef(); // for scrolling down when collapse item opens - const classes = useStyles(); - const identityApi = useApi(identityApiRef); - - const handleClick = () => { - setOpen(!open); - setTimeout(() => ref.current?.scrollIntoView({ behavior: 'smooth' }), 300); - }; - - const userId = identityApi.getUserId(); - const profile = identityApi.getProfile(); - const displayName = profile.displayName ?? userId; - const SignInAvatar = () => ( - - {displayName[0]} - - ); - - return ( - <> - - - {open ? : } - - - ); -}; diff --git a/packages/core/src/layout/Sidebar/Settings/UserSettings.tsx b/packages/core/src/layout/Sidebar/Settings/UserSettings.tsx new file mode 100644 index 0000000000..8f541364ff --- /dev/null +++ b/packages/core/src/layout/Sidebar/Settings/UserSettings.tsx @@ -0,0 +1,78 @@ +/* + * 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, { useEffect, useContext } from 'react'; +import { Popover } from '@material-ui/core'; +import { SignInAvatar } from './SignInAvatar'; +import { SettingsDialog } from './SettingsDialog'; +import { SidebarItem } from '../Items'; +import { useUserProfile } from './useUserProfileInfo'; +import { SidebarContext } from '../config'; + +export const SidebarUserSettings = ({ + providerSettings, +}: { + providerSettings?: React.ReactNode; +}) => { + const { isOpen: sidebarOpen } = useContext(SidebarContext); + const { displayName } = useUserProfile(); + const [open, setOpen] = React.useState(false); + const [anchorEl, setAnchorEl] = React.useState( + undefined, + ); + + const handleOpen = (event?: React.MouseEvent) => { + setAnchorEl(event?.currentTarget ?? undefined); + setOpen(true); + }; + + const handleClose = () => { + setAnchorEl(undefined); + setOpen(false); + }; + + // Close the provider list when sidebar collapse + useEffect(() => { + if (!sidebarOpen && open) setOpen(false); + }, [open, sidebarOpen]); + + const SidebarAvatar = () => ; + + return ( + <> + + + + + + ); +}; diff --git a/packages/core/src/layout/Sidebar/Settings/UserSettingsMenu.tsx b/packages/core/src/layout/Sidebar/Settings/UserSettingsMenu.tsx new file mode 100644 index 0000000000..9ac287c57e --- /dev/null +++ b/packages/core/src/layout/Sidebar/Settings/UserSettingsMenu.tsx @@ -0,0 +1,55 @@ +/* + * 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 { identityApiRef, useApi } from '@backstage/core-api'; +import { IconButton, ListItemIcon, Menu, MenuItem } from '@material-ui/core'; +import SignOutIcon from '@material-ui/icons/MeetingRoom'; +import MoreVertIcon from '@material-ui/icons/MoreVert'; + +export const UserSettingsMenu = () => { + const identityApi = useApi(identityApiRef); + const [open, setOpen] = React.useState(false); + const [anchorEl, setAnchorEl] = React.useState( + undefined, + ); + + const handleOpen = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + setOpen(true); + }; + + const handleClose = () => { + setAnchorEl(undefined); + setOpen(false); + }; + + return ( + <> + + + + + identityApi.logout()}> + + + + Sign Out + + + + ); +}; diff --git a/packages/core/src/layout/Sidebar/Settings/index.ts b/packages/core/src/layout/Sidebar/Settings/index.ts index 6557ace53a..5479236a3b 100644 --- a/packages/core/src/layout/Sidebar/Settings/index.ts +++ b/packages/core/src/layout/Sidebar/Settings/index.ts @@ -17,4 +17,5 @@ export { ProviderSettingsItem } from './ProviderSettingsItem'; export { OAuthProviderSettings } from './OAuthProviderSettings'; export { OIDCProviderSettings } from './OIDCProviderSettings'; -export { UserProfile } from './UserProfile'; +// export { UserProfile } from './UserProfile'; +export { SidebarUserSettings } from './UserSettings'; diff --git a/packages/core/src/layout/Sidebar/Settings/useUserProfileInfo.ts b/packages/core/src/layout/Sidebar/Settings/useUserProfileInfo.ts new file mode 100644 index 0000000000..60dae294a5 --- /dev/null +++ b/packages/core/src/layout/Sidebar/Settings/useUserProfileInfo.ts @@ -0,0 +1,26 @@ +/* + * 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 { useApi, identityApiRef } from '@backstage/core-api'; + +export const useUserProfile = () => { + const identityApi = useApi(identityApiRef); + const userId = identityApi.getUserId(); + const profile = identityApi.getProfile(); + const displayName = profile.displayName ?? userId; + + return { profile, displayName }; +}; diff --git a/packages/core/src/layout/Sidebar/SidebarThemeToggle.tsx b/packages/core/src/layout/Sidebar/SidebarThemeToggle.tsx deleted file mode 100644 index 32bc9d9632..0000000000 --- a/packages/core/src/layout/Sidebar/SidebarThemeToggle.tsx +++ /dev/null @@ -1,58 +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, { FC } from 'react'; -import { useObservable } from 'react-use'; -import LightIcon from '@material-ui/icons/WbSunny'; -import DarkIcon from '@material-ui/icons/Brightness2'; -import AutoIcon from '@material-ui/icons/BrightnessAuto'; -import { appThemeApiRef, useApi } from '@backstage/core-api'; -import { SidebarItem } from './Items'; - -export const SidebarThemeToggle: FC<{}> = () => { - const appThemeApi = useApi(appThemeApiRef); - const themeId = useObservable( - appThemeApi.activeThemeId$(), - appThemeApi.getActiveThemeId(), - ); - - let text = 'Auto'; - let icon = AutoIcon; - switch (themeId) { - case 'dark': - text = 'Dark mode'; - icon = DarkIcon; - break; - case 'light': - text = 'Light mode'; - icon = LightIcon; - break; - default: - break; - } - - const handleToggle = () => { - if (!themeId) { - appThemeApi.setActiveThemeId('light'); - } else if (themeId === 'light') { - appThemeApi.setActiveThemeId('dark'); - } else { - appThemeApi.setActiveThemeId(undefined); - } - }; - - return ; -}; diff --git a/packages/core/src/layout/Sidebar/UserSettings.tsx b/packages/core/src/layout/Sidebar/UserSettings.tsx deleted file mode 100644 index f586f6d077..0000000000 --- a/packages/core/src/layout/Sidebar/UserSettings.tsx +++ /dev/null @@ -1,53 +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 { identityApiRef, useApi } from '@backstage/core-api'; -import Collapse from '@material-ui/core/Collapse'; -import SignOutIcon from '@material-ui/icons/MeetingRoom'; -import React, { useContext, useEffect } from 'react'; -import { SidebarContext } from './config'; -import { SidebarItem } from './Items'; -import { UserProfile as SidebarUserProfile } from './Settings'; - -type SidebarUserSettingsProps = { providerSettings?: React.ReactNode }; - -export function SidebarUserSettings({ - providerSettings, -}: SidebarUserSettingsProps) { - const { isOpen: sidebarOpen } = useContext(SidebarContext); - const [open, setOpen] = React.useState(false); - const identityApi = useApi(identityApiRef); - - // Close the provider list when sidebar collapse - useEffect(() => { - if (!sidebarOpen && open) setOpen(false); - }, [open, sidebarOpen]); - - return ( - <> - - - {providerSettings} - - identityApi.logout()} - /> - - - ); -} diff --git a/packages/core/src/layout/Sidebar/index.ts b/packages/core/src/layout/Sidebar/index.ts index fdca7e4eea..fb9a457dea 100644 --- a/packages/core/src/layout/Sidebar/index.ts +++ b/packages/core/src/layout/Sidebar/index.ts @@ -25,14 +25,15 @@ export { SidebarSpacer, } from './Items'; export { IntroCard, SidebarIntro } from './Intro'; -export { SidebarPinButton } from './PinButton'; +// export { SidebarPinButton } from './PinButton'; export { SIDEBAR_INTRO_LOCAL_STORAGE, SidebarContext, sidebarConfig, } from './config'; export type { SidebarContextType } from './config'; -export { SidebarThemeToggle } from './SidebarThemeToggle'; -export { SidebarUserSettings } from './UserSettings'; +// export { SidebarThemeToggle } from './SidebarThemeToggle'; +// export { SidebarUserSettings } from './UserSettings'; export { DefaultProviderSettings } from './DefaultProviderSettings'; export * from './Settings'; +// export { SidebarUserSettings } from './UserSettings'; From 5e9acd5947b02bde74ebc77a787051db0812454a Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Thu, 10 Sep 2020 17:59:40 +0200 Subject: [PATCH 02/22] Option to change size of SignInAvatar --- .../Sidebar/Settings/SettingsDialog.tsx | 2 +- .../layout/Sidebar/Settings/SignInAvatar.tsx | 37 ++++--------------- 2 files changed, 8 insertions(+), 31 deletions(-) diff --git a/packages/core/src/layout/Sidebar/Settings/SettingsDialog.tsx b/packages/core/src/layout/Sidebar/Settings/SettingsDialog.tsx index b37053763b..9077654b5c 100644 --- a/packages/core/src/layout/Sidebar/Settings/SettingsDialog.tsx +++ b/packages/core/src/layout/Sidebar/Settings/SettingsDialog.tsx @@ -47,7 +47,7 @@ export const SettingsDialog = ({ return ( } + avatar={} action={} title={displayName} subheader={profile.email} diff --git a/packages/core/src/layout/Sidebar/Settings/SignInAvatar.tsx b/packages/core/src/layout/Sidebar/Settings/SignInAvatar.tsx index c6eed74a18..519d467436 100644 --- a/packages/core/src/layout/Sidebar/Settings/SignInAvatar.tsx +++ b/packages/core/src/layout/Sidebar/Settings/SignInAvatar.tsx @@ -18,41 +18,18 @@ import React from 'react'; import { BackstageTheme } from '@backstage/theme'; import { makeStyles, Avatar } from '@material-ui/core'; import { useUserProfile } from './useUserProfileInfo'; +import { sidebarConfig } from '../config'; -// const useStyles = makeStyles({ -// avatar: { -// width: ({ size }) => size, -// height: ({ size }) => size, -// }, -// }); - -// export const SignInAvatar = ({ size = 24 }: { size?: number }) => { -// const classes = useStyles({ size }); -// const { profile, displayName } = useUserProfile(); - -// return ( -// -// {displayName[0]} -// -// ); -// }; - -// const useStyles = makeStyles({ -// avatar: { -// width: 24, -// height: 24, -// }, -// }); - -const useStyles = makeStyles({ +const useStyles = makeStyles({ avatar: { - width: 24, - height: 24, + width: ({ size }) => size, + height: ({ size }) => size, }, }); -export const SignInAvatar = () => { - const classes = useStyles(); +export const SignInAvatar = ({ size }: { size?: number }) => { + const { iconSize } = sidebarConfig; + const classes = useStyles(size ? { size } : { size: iconSize }); const { profile, displayName } = useUserProfile(); return ( From cd66f03f9d2ec31bc543bb1374ae754e6c6d9c74 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Fri, 11 Sep 2020 09:58:45 +0200 Subject: [PATCH 03/22] Cleanup and breaking out into more components --- .../Sidebar/Settings/AppSettingsList.tsx | 26 +++++++++++++++++ .../Sidebar/Settings/AuthProviderList.tsx | 29 +++++++++++++++++++ .../src/layout/Sidebar/Settings/PinButton.tsx | 2 +- .../Sidebar/Settings/SettingsDialog.tsx | 28 +++++++----------- .../layout/Sidebar/Settings/SignInAvatar.tsx | 4 ++- .../layout/Sidebar/Settings/ThemeToggle.tsx | 2 +- .../layout/Sidebar/Settings/UserSettings.tsx | 9 +++--- 7 files changed, 75 insertions(+), 25 deletions(-) create mode 100644 packages/core/src/layout/Sidebar/Settings/AppSettingsList.tsx create mode 100644 packages/core/src/layout/Sidebar/Settings/AuthProviderList.tsx diff --git a/packages/core/src/layout/Sidebar/Settings/AppSettingsList.tsx b/packages/core/src/layout/Sidebar/Settings/AppSettingsList.tsx new file mode 100644 index 0000000000..5dad178ccd --- /dev/null +++ b/packages/core/src/layout/Sidebar/Settings/AppSettingsList.tsx @@ -0,0 +1,26 @@ +/* + * 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 { List, ListSubheader } from '@material-ui/core'; +import { SidebarThemeToggle } from './ThemeToggle'; +import { SidebarPinButton } from './PinButton'; + +export const AppSettingsList = () => ( + App Settings}> + + + +); diff --git a/packages/core/src/layout/Sidebar/Settings/AuthProviderList.tsx b/packages/core/src/layout/Sidebar/Settings/AuthProviderList.tsx new file mode 100644 index 0000000000..5d13d61148 --- /dev/null +++ b/packages/core/src/layout/Sidebar/Settings/AuthProviderList.tsx @@ -0,0 +1,29 @@ +/* + * 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 List from '@material-ui/core/List'; +import ListSubheader from '@material-ui/core/ListSubheader'; + +type Props = { + providerSettings?: React.ReactNode; +}; + +export const AuthProvidersList = ({ providerSettings }: Props) => ( + Available Auth Providers}> + {providerSettings} + +); diff --git a/packages/core/src/layout/Sidebar/Settings/PinButton.tsx b/packages/core/src/layout/Sidebar/Settings/PinButton.tsx index dcd0818ad6..2727313ede 100644 --- a/packages/core/src/layout/Sidebar/Settings/PinButton.tsx +++ b/packages/core/src/layout/Sidebar/Settings/PinButton.tsx @@ -50,7 +50,7 @@ export const SidebarPinButton = () => { { toggleSidebarPinState(); diff --git a/packages/core/src/layout/Sidebar/Settings/SettingsDialog.tsx b/packages/core/src/layout/Sidebar/Settings/SettingsDialog.tsx index 9077654b5c..ee2f0bdfb4 100644 --- a/packages/core/src/layout/Sidebar/Settings/SettingsDialog.tsx +++ b/packages/core/src/layout/Sidebar/Settings/SettingsDialog.tsx @@ -22,11 +22,10 @@ import { Divider, makeStyles, } from '@material-ui/core'; -import List from '@material-ui/core/List'; -import ListSubheader from '@material-ui/core/ListSubheader'; -import { SidebarPinButton } from './PinButton'; +import { AppSettingsList } from './AppSettingsList'; +import { AuthProvidersList } from './AuthProviderList'; +import { FeatureFlagsList } from './FeatureFlagsList'; import { SignInAvatar } from './SignInAvatar'; -import { SidebarThemeToggle } from './ThemeToggle'; import { UserSettingsMenu } from './UserSettingsMenu'; import { useUserProfile } from './useUserProfileInfo'; @@ -36,11 +35,11 @@ const useStyles = makeStyles({ }, }); -export const SettingsDialog = ({ - providerSettings, -}: { +type Props = { providerSettings?: React.ReactNode; -}) => { +}; + +export const SettingsDialog = ({ providerSettings }: Props) => { const classes = useStyles(); const { profile, displayName } = useUserProfile(); @@ -54,16 +53,11 @@ export const SettingsDialog = ({ /> - App Settings}> - - - + - Available Auth Providers} - > - {providerSettings} - + + + ); diff --git a/packages/core/src/layout/Sidebar/Settings/SignInAvatar.tsx b/packages/core/src/layout/Sidebar/Settings/SignInAvatar.tsx index 519d467436..f0430edfcf 100644 --- a/packages/core/src/layout/Sidebar/Settings/SignInAvatar.tsx +++ b/packages/core/src/layout/Sidebar/Settings/SignInAvatar.tsx @@ -27,7 +27,9 @@ const useStyles = makeStyles({ }, }); -export const SignInAvatar = ({ size }: { size?: number }) => { +type Props = { size?: number }; + +export const SignInAvatar = ({ size }: Props) => { const { iconSize } = sidebarConfig; const classes = useStyles(size ? { size } : { size: iconSize }); const { profile, displayName } = useUserProfile(); diff --git a/packages/core/src/layout/Sidebar/Settings/ThemeToggle.tsx b/packages/core/src/layout/Sidebar/Settings/ThemeToggle.tsx index 29cfb24885..a5e703089c 100644 --- a/packages/core/src/layout/Sidebar/Settings/ThemeToggle.tsx +++ b/packages/core/src/layout/Sidebar/Settings/ThemeToggle.tsx @@ -37,7 +37,7 @@ export const SidebarThemeToggle = () => { ); const themeIds = appThemeApi.getInstalledThemes(); - // TODO: can these be put on the theme itself? + // TODO(marcuseide): can these be put on the theme itself? const themeIcons = { dark: , light: , diff --git a/packages/core/src/layout/Sidebar/Settings/UserSettings.tsx b/packages/core/src/layout/Sidebar/Settings/UserSettings.tsx index 8f541364ff..85dbefff9b 100644 --- a/packages/core/src/layout/Sidebar/Settings/UserSettings.tsx +++ b/packages/core/src/layout/Sidebar/Settings/UserSettings.tsx @@ -22,11 +22,11 @@ import { SidebarItem } from '../Items'; import { useUserProfile } from './useUserProfileInfo'; import { SidebarContext } from '../config'; -export const SidebarUserSettings = ({ - providerSettings, -}: { +type Props = { providerSettings?: React.ReactNode; -}) => { +}; + +export const SidebarUserSettings = ({ providerSettings }: Props) => { const { isOpen: sidebarOpen } = useContext(SidebarContext); const { displayName } = useUserProfile(); const [open, setOpen] = React.useState(false); @@ -44,7 +44,6 @@ export const SidebarUserSettings = ({ setOpen(false); }; - // Close the provider list when sidebar collapse useEffect(() => { if (!sidebarOpen && open) setOpen(false); }, [open, sidebarOpen]); From 12e2d169f8b547d86a0890e4dbe7720ffc52ded9 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Fri, 11 Sep 2020 09:59:53 +0200 Subject: [PATCH 04/22] Add featureflags components and toggle method on api --- packages/core-api/src/app/FeatureFlags.tsx | 7 ++ .../Sidebar/Settings/FeatureFlagsItem.tsx | 67 +++++++++++++++++++ .../Sidebar/Settings/FeatureFlagsList.tsx | 38 +++++++++++ 3 files changed, 112 insertions(+) create mode 100644 packages/core/src/layout/Sidebar/Settings/FeatureFlagsItem.tsx create mode 100644 packages/core/src/layout/Sidebar/Settings/FeatureFlagsList.tsx diff --git a/packages/core-api/src/app/FeatureFlags.tsx b/packages/core-api/src/app/FeatureFlags.tsx index 11c084d3ed..79c3e5d6af 100644 --- a/packages/core-api/src/app/FeatureFlags.tsx +++ b/packages/core-api/src/app/FeatureFlags.tsx @@ -80,6 +80,13 @@ export class UserFlags extends Map { return output; } + toggle(name: FeatureFlagName): FeatureFlagState { + Boolean(super.get(name)) + ? super.set(name, FeatureFlagState.Off) + : super.set(name, FeatureFlagState.On); + return super.get(name) || FeatureFlagState.Off; + } + delete(name: FeatureFlagName): boolean { const output = super.delete(name); this.save(); diff --git a/packages/core/src/layout/Sidebar/Settings/FeatureFlagsItem.tsx b/packages/core/src/layout/Sidebar/Settings/FeatureFlagsItem.tsx new file mode 100644 index 0000000000..d7da3afbe6 --- /dev/null +++ b/packages/core/src/layout/Sidebar/Settings/FeatureFlagsItem.tsx @@ -0,0 +1,67 @@ +/* + * 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 { FeatureFlagName, FeatureFlagsApi } from '@backstage/core-api'; +import { + ListItem, + ListItemSecondaryAction, + ListItemText, + Tooltip, +} from '@material-ui/core'; +import CheckIcon from '@material-ui/icons/CheckCircle'; +import { ToggleButton } from '@material-ui/lab'; + +type Props = { + featureFlag: { name: FeatureFlagName; pluginId: string }; + api: FeatureFlagsApi; +}; + +export const FlagItem = ({ featureFlag, api }: Props) => { + const [enabled, setEnabled] = React.useState( + Boolean(api.getFlags().get(featureFlag.name)), + ); + + const toggleFlag = () => { + const newState = api.getFlags().toggle(featureFlag.name); + setEnabled(Boolean(newState)); + }; + + return ( + + + + + + + + + + + ); +}; diff --git a/packages/core/src/layout/Sidebar/Settings/FeatureFlagsList.tsx b/packages/core/src/layout/Sidebar/Settings/FeatureFlagsList.tsx new file mode 100644 index 0000000000..13e3a28c35 --- /dev/null +++ b/packages/core/src/layout/Sidebar/Settings/FeatureFlagsList.tsx @@ -0,0 +1,38 @@ +/* + * 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 List from '@material-ui/core/List'; +import ListSubheader from '@material-ui/core/ListSubheader'; +import { useApi, featureFlagsApiRef } from '@backstage/core-api'; +import { FlagItem } from './FeatureFlagsItem'; + +export const FeatureFlagsList = () => { + const featureFlagsApi = useApi(featureFlagsApiRef); + const featureFlags = featureFlagsApi.getRegisteredFlags(); + + return ( + Feature Flags}> + {featureFlags.map(featureFlag => ( + + ))} + + ); +}; From f01596d8c45ae54ad35a5afe5aba48353b619a95 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Fri, 11 Sep 2020 10:07:48 +0200 Subject: [PATCH 05/22] Update sidepar --- packages/app/src/components/Root/Root.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index e91504d619..03e24cf4ae 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -35,8 +35,6 @@ import { SidebarSearchField, SidebarSpace, SidebarUserSettings, - SidebarThemeToggle, - SidebarPinButton, DefaultProviderSettings, } from '@backstage/core'; import { NavLink } from 'react-router-dom'; @@ -103,9 +101,7 @@ const Root: FC<{}> = ({ children }) => ( /> - } /> - {children} From c8292fa05c4f6932dcf8a2d3269e9ef8f8bb56e2 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Fri, 11 Sep 2020 10:46:47 +0200 Subject: [PATCH 06/22] Change logic of FeatureFlags.toggle() --- packages/core-api/src/app/FeatureFlags.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/core-api/src/app/FeatureFlags.tsx b/packages/core-api/src/app/FeatureFlags.tsx index 79c3e5d6af..3db2a18a02 100644 --- a/packages/core-api/src/app/FeatureFlags.tsx +++ b/packages/core-api/src/app/FeatureFlags.tsx @@ -81,9 +81,11 @@ export class UserFlags extends Map { } toggle(name: FeatureFlagName): FeatureFlagState { - Boolean(super.get(name)) - ? super.set(name, FeatureFlagState.Off) - : super.set(name, FeatureFlagState.On); + if (super.get(name) === FeatureFlagState.On) { + super.set(name, FeatureFlagState.Off); + } else { + super.set(name, FeatureFlagState.On); + } return super.get(name) || FeatureFlagState.Off; } From 88fa72f91f7c8e9da2b92e5e04caeb6f775c6c95 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Fri, 11 Sep 2020 10:58:37 +0200 Subject: [PATCH 07/22] Handle empty sections --- .../Sidebar/Settings/AuthProviderList.tsx | 20 ++++++++++---- .../Sidebar/Settings/FeatureFlagsList.tsx | 26 ++++++++++++------- .../Sidebar/Settings/SettingsDialog.tsx | 11 +------- 3 files changed, 33 insertions(+), 24 deletions(-) diff --git a/packages/core/src/layout/Sidebar/Settings/AuthProviderList.tsx b/packages/core/src/layout/Sidebar/Settings/AuthProviderList.tsx index 5d13d61148..50832da681 100644 --- a/packages/core/src/layout/Sidebar/Settings/AuthProviderList.tsx +++ b/packages/core/src/layout/Sidebar/Settings/AuthProviderList.tsx @@ -17,13 +17,23 @@ import React from 'react'; import List from '@material-ui/core/List'; import ListSubheader from '@material-ui/core/ListSubheader'; +import { Divider } from '@material-ui/core'; type Props = { providerSettings?: React.ReactNode; }; -export const AuthProvidersList = ({ providerSettings }: Props) => ( - Available Auth Providers}> - {providerSettings} - -); +export const AuthProvidersList = ({ providerSettings }: Props) => { + if (!providerSettings) { + return null; + } + + return ( + <> + + Available Auth Providers}> + {providerSettings} + + + ); +}; diff --git a/packages/core/src/layout/Sidebar/Settings/FeatureFlagsList.tsx b/packages/core/src/layout/Sidebar/Settings/FeatureFlagsList.tsx index 13e3a28c35..d74a501c13 100644 --- a/packages/core/src/layout/Sidebar/Settings/FeatureFlagsList.tsx +++ b/packages/core/src/layout/Sidebar/Settings/FeatureFlagsList.tsx @@ -19,20 +19,28 @@ import List from '@material-ui/core/List'; import ListSubheader from '@material-ui/core/ListSubheader'; import { useApi, featureFlagsApiRef } from '@backstage/core-api'; import { FlagItem } from './FeatureFlagsItem'; +import { Divider } from '@material-ui/core'; export const FeatureFlagsList = () => { const featureFlagsApi = useApi(featureFlagsApiRef); const featureFlags = featureFlagsApi.getRegisteredFlags(); + if (featureFlags.length === 0) { + return null; + } + return ( - Feature Flags}> - {featureFlags.map(featureFlag => ( - - ))} - + <> + + Feature Flags}> + {featureFlags.map(featureFlag => ( + + ))} + + ); }; diff --git a/packages/core/src/layout/Sidebar/Settings/SettingsDialog.tsx b/packages/core/src/layout/Sidebar/Settings/SettingsDialog.tsx index ee2f0bdfb4..9c452982e3 100644 --- a/packages/core/src/layout/Sidebar/Settings/SettingsDialog.tsx +++ b/packages/core/src/layout/Sidebar/Settings/SettingsDialog.tsx @@ -15,13 +15,7 @@ */ import React from 'react'; -import { - Card, - CardContent, - CardHeader, - Divider, - makeStyles, -} from '@material-ui/core'; +import { Card, CardContent, CardHeader, makeStyles } from '@material-ui/core'; import { AppSettingsList } from './AppSettingsList'; import { AuthProvidersList } from './AuthProviderList'; import { FeatureFlagsList } from './FeatureFlagsList'; @@ -52,11 +46,8 @@ export const SettingsDialog = ({ providerSettings }: Props) => { subheader={profile.email} /> - - - From e9c59b773e901804d623654e083d01709e84c6a2 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Fri, 11 Sep 2020 11:00:35 +0200 Subject: [PATCH 08/22] Update default-app template --- .../templates/default-app/packages/app/src/sidebar.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/create-app/templates/default-app/packages/app/src/sidebar.tsx b/packages/create-app/templates/default-app/packages/app/src/sidebar.tsx index 96d18965ec..9dd9ea64c3 100644 --- a/packages/create-app/templates/default-app/packages/app/src/sidebar.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/sidebar.tsx @@ -18,8 +18,6 @@ import { SidebarContext, SidebarSpace, SidebarUserSettings, - SidebarThemeToggle, - SidebarPinButton, DefaultProviderSettings, } from '@backstage/core'; @@ -39,9 +37,7 @@ export const AppSidebar = () => ( - } /> - ); From 232f0538df0229c36fea7e5a879a3d6b77d2d3ad Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Thu, 17 Sep 2020 13:01:25 +0200 Subject: [PATCH 09/22] docs: improve navigation around ADRs --- docs/architecture-decisions/adr001-add-adr-log.md | 1 - .../architecture-decisions/adr002-default-catalog-file-format.md | 1 - docs/architecture-decisions/adr003-avoid-default-exports.md | 1 - docs/architecture-decisions/adr004-module-export-structure.md | 1 - docs/architecture-decisions/adr005-catalog-core-entities.md | 1 - docs/architecture-decisions/adr006-avoid-react-fc.md | 1 - .../adr007-use-msw-to-mock-service-requests.md | 1 - docs/architecture-decisions/adr008-default-catalog-file-name.md | 1 - 8 files changed, 8 deletions(-) diff --git a/docs/architecture-decisions/adr001-add-adr-log.md b/docs/architecture-decisions/adr001-add-adr-log.md index 453905ab7a..16783367ab 100644 --- a/docs/architecture-decisions/adr001-add-adr-log.md +++ b/docs/architecture-decisions/adr001-add-adr-log.md @@ -1,7 +1,6 @@ --- id: adrs-adr001 title: ADR001: Architecture Decision Record (ADR) log -sidebar_label: ADR001 description: Architecture Decision Record (ADR) logs as a reference point for the team --- diff --git a/docs/architecture-decisions/adr002-default-catalog-file-format.md b/docs/architecture-decisions/adr002-default-catalog-file-format.md index da98aafaed..f4fd3a1158 100644 --- a/docs/architecture-decisions/adr002-default-catalog-file-format.md +++ b/docs/architecture-decisions/adr002-default-catalog-file-format.md @@ -1,7 +1,6 @@ --- id: adrs-adr002 title: ADR002: Default Software Catalog File Format -sidebar_label: ADR002 description: Architecture Decision Record (ADR) log on Default Software Catalog File Format --- diff --git a/docs/architecture-decisions/adr003-avoid-default-exports.md b/docs/architecture-decisions/adr003-avoid-default-exports.md index 9d1f9c83a4..8634a19632 100644 --- a/docs/architecture-decisions/adr003-avoid-default-exports.md +++ b/docs/architecture-decisions/adr003-avoid-default-exports.md @@ -1,7 +1,6 @@ --- id: adrs-adr003 title: ADR003: Avoid Default Exports and Prefer Named Exports -sidebar_label: ADR003 description: Architecture Decision Record (ADR) log on Avoid Default Exports and Prefer Named Exports --- diff --git a/docs/architecture-decisions/adr004-module-export-structure.md b/docs/architecture-decisions/adr004-module-export-structure.md index 3dbda3f8b9..12408abac1 100644 --- a/docs/architecture-decisions/adr004-module-export-structure.md +++ b/docs/architecture-decisions/adr004-module-export-structure.md @@ -1,7 +1,6 @@ --- id: adrs-adr004 title: ADR004: Module Export Structure -sidebar_label: ADR004 description: Architecture Decision Record (ADR) log on Module Export Structure --- diff --git a/docs/architecture-decisions/adr005-catalog-core-entities.md b/docs/architecture-decisions/adr005-catalog-core-entities.md index ae615f647d..f91698c5ff 100644 --- a/docs/architecture-decisions/adr005-catalog-core-entities.md +++ b/docs/architecture-decisions/adr005-catalog-core-entities.md @@ -1,7 +1,6 @@ --- id: adrs-adr005 title: ADR005: Catalog Core Entities -sidebar_label: ADR005 description: Architecture Decision Record (ADR) log on Catalog Core Entities --- diff --git a/docs/architecture-decisions/adr006-avoid-react-fc.md b/docs/architecture-decisions/adr006-avoid-react-fc.md index 696cdcf537..bea36712d7 100644 --- a/docs/architecture-decisions/adr006-avoid-react-fc.md +++ b/docs/architecture-decisions/adr006-avoid-react-fc.md @@ -1,7 +1,6 @@ --- id: adrs-adr006 title: ADR006: Avoid React.FC and React.SFC -sidebar_label: ADR006 description: Architecture Decision Record (ADR) log on Avoid React.FC and React.SFC --- diff --git a/docs/architecture-decisions/adr007-use-msw-to-mock-service-requests.md b/docs/architecture-decisions/adr007-use-msw-to-mock-service-requests.md index d91af19068..6b18f67d40 100644 --- a/docs/architecture-decisions/adr007-use-msw-to-mock-service-requests.md +++ b/docs/architecture-decisions/adr007-use-msw-to-mock-service-requests.md @@ -1,7 +1,6 @@ --- id: adrs-adr007 title: ADR007: Use MSW to mock http requests -sidebar_label: ADR007 description: Architecture Decision Record (ADR) log on Use MSW to mock http requests --- diff --git a/docs/architecture-decisions/adr008-default-catalog-file-name.md b/docs/architecture-decisions/adr008-default-catalog-file-name.md index 01afefd690..c57d10c0b1 100644 --- a/docs/architecture-decisions/adr008-default-catalog-file-name.md +++ b/docs/architecture-decisions/adr008-default-catalog-file-name.md @@ -1,7 +1,6 @@ --- id: adrs-adr008 title: ADR008: Default Catalog File Name -sidebar_label: ADR008 description: Architecture Decision Record (ADR) log on Default Catalog File Name --- From fd83d501db3530039ab5c494f92797cca8fe7a18 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Thu, 17 Sep 2020 13:36:12 +0200 Subject: [PATCH 10/22] Remove unnecessary boolean conversion --- .../core/src/layout/Sidebar/Settings/FeatureFlagsItem.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/core/src/layout/Sidebar/Settings/FeatureFlagsItem.tsx b/packages/core/src/layout/Sidebar/Settings/FeatureFlagsItem.tsx index d7da3afbe6..21074a5153 100644 --- a/packages/core/src/layout/Sidebar/Settings/FeatureFlagsItem.tsx +++ b/packages/core/src/layout/Sidebar/Settings/FeatureFlagsItem.tsx @@ -53,11 +53,7 @@ export const FlagItem = ({ featureFlag, api }: Props) => { selected={enabled} onChange={toggleFlag} > - + From b67b2b6c0a4e543956ae75213ee9953145b34634 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Thu, 17 Sep 2020 13:37:29 +0200 Subject: [PATCH 11/22] Remove commented out code --- packages/core/src/layout/Sidebar/Settings/index.ts | 1 - packages/core/src/layout/Sidebar/index.ts | 4 ---- 2 files changed, 5 deletions(-) diff --git a/packages/core/src/layout/Sidebar/Settings/index.ts b/packages/core/src/layout/Sidebar/Settings/index.ts index 5479236a3b..7abf061620 100644 --- a/packages/core/src/layout/Sidebar/Settings/index.ts +++ b/packages/core/src/layout/Sidebar/Settings/index.ts @@ -17,5 +17,4 @@ export { ProviderSettingsItem } from './ProviderSettingsItem'; export { OAuthProviderSettings } from './OAuthProviderSettings'; export { OIDCProviderSettings } from './OIDCProviderSettings'; -// export { UserProfile } from './UserProfile'; export { SidebarUserSettings } from './UserSettings'; diff --git a/packages/core/src/layout/Sidebar/index.ts b/packages/core/src/layout/Sidebar/index.ts index fb9a457dea..a644c06e14 100644 --- a/packages/core/src/layout/Sidebar/index.ts +++ b/packages/core/src/layout/Sidebar/index.ts @@ -25,15 +25,11 @@ export { SidebarSpacer, } from './Items'; export { IntroCard, SidebarIntro } from './Intro'; -// export { SidebarPinButton } from './PinButton'; export { SIDEBAR_INTRO_LOCAL_STORAGE, SidebarContext, sidebarConfig, } from './config'; export type { SidebarContextType } from './config'; -// export { SidebarThemeToggle } from './SidebarThemeToggle'; -// export { SidebarUserSettings } from './UserSettings'; export { DefaultProviderSettings } from './DefaultProviderSettings'; export * from './Settings'; -// export { SidebarUserSettings } from './UserSettings'; From 3664ef13c2752ca8c11cf388da633bb8fbd42e3f Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Thu, 17 Sep 2020 13:53:57 +0200 Subject: [PATCH 12/22] Pass props to lists and control rendering from outside wrapper --- .../Sidebar/Settings/AuthProviderList.tsx | 22 ++++-------- .../Sidebar/Settings/FeatureFlagsItem.tsx | 20 ++++++++--- .../Sidebar/Settings/FeatureFlagsList.tsx | 36 ++++++------------- .../Sidebar/Settings/SettingsDialog.tsx | 25 +++++++++++-- 4 files changed, 54 insertions(+), 49 deletions(-) diff --git a/packages/core/src/layout/Sidebar/Settings/AuthProviderList.tsx b/packages/core/src/layout/Sidebar/Settings/AuthProviderList.tsx index 50832da681..34cf6fb837 100644 --- a/packages/core/src/layout/Sidebar/Settings/AuthProviderList.tsx +++ b/packages/core/src/layout/Sidebar/Settings/AuthProviderList.tsx @@ -17,23 +17,13 @@ import React from 'react'; import List from '@material-ui/core/List'; import ListSubheader from '@material-ui/core/ListSubheader'; -import { Divider } from '@material-ui/core'; type Props = { - providerSettings?: React.ReactNode; + providerSettings: React.ReactNode; }; -export const AuthProvidersList = ({ providerSettings }: Props) => { - if (!providerSettings) { - return null; - } - - return ( - <> - - Available Auth Providers}> - {providerSettings} - - - ); -}; +export const AuthProvidersList = ({ providerSettings }: Props) => ( + Available Auth Providers}> + {providerSettings} + +); diff --git a/packages/core/src/layout/Sidebar/Settings/FeatureFlagsItem.tsx b/packages/core/src/layout/Sidebar/Settings/FeatureFlagsItem.tsx index 21074a5153..3e40ce69cc 100644 --- a/packages/core/src/layout/Sidebar/Settings/FeatureFlagsItem.tsx +++ b/packages/core/src/layout/Sidebar/Settings/FeatureFlagsItem.tsx @@ -15,7 +15,11 @@ */ import React from 'react'; -import { FeatureFlagName, FeatureFlagsApi } from '@backstage/core-api'; +import { + FeatureFlagName, + useApi, + featureFlagsApiRef, +} from '@backstage/core-api'; import { ListItem, ListItemSecondaryAction, @@ -25,12 +29,18 @@ import { import CheckIcon from '@material-ui/icons/CheckCircle'; import { ToggleButton } from '@material-ui/lab'; -type Props = { - featureFlag: { name: FeatureFlagName; pluginId: string }; - api: FeatureFlagsApi; +export type Item = { + name: FeatureFlagName; + pluginId: string; }; -export const FlagItem = ({ featureFlag, api }: Props) => { +type Props = { + featureFlag: Item; +}; + +export const FlagItem = ({ featureFlag }: Props) => { + const api = useApi(featureFlagsApiRef); + const [enabled, setEnabled] = React.useState( Boolean(api.getFlags().get(featureFlag.name)), ); diff --git a/packages/core/src/layout/Sidebar/Settings/FeatureFlagsList.tsx b/packages/core/src/layout/Sidebar/Settings/FeatureFlagsList.tsx index d74a501c13..5687446511 100644 --- a/packages/core/src/layout/Sidebar/Settings/FeatureFlagsList.tsx +++ b/packages/core/src/layout/Sidebar/Settings/FeatureFlagsList.tsx @@ -17,30 +17,16 @@ import React from 'react'; import List from '@material-ui/core/List'; import ListSubheader from '@material-ui/core/ListSubheader'; -import { useApi, featureFlagsApiRef } from '@backstage/core-api'; -import { FlagItem } from './FeatureFlagsItem'; -import { Divider } from '@material-ui/core'; +import { FlagItem, Item } from './FeatureFlagsItem'; -export const FeatureFlagsList = () => { - const featureFlagsApi = useApi(featureFlagsApiRef); - const featureFlags = featureFlagsApi.getRegisteredFlags(); - - if (featureFlags.length === 0) { - return null; - } - - return ( - <> - - Feature Flags}> - {featureFlags.map(featureFlag => ( - - ))} - - - ); +type Props = { + featureFlags: Item[]; }; + +export const FeatureFlagsList = ({ featureFlags }: Props) => ( + Feature Flags}> + {featureFlags.map(featureFlag => ( + + ))} + +); diff --git a/packages/core/src/layout/Sidebar/Settings/SettingsDialog.tsx b/packages/core/src/layout/Sidebar/Settings/SettingsDialog.tsx index 9c452982e3..71c4899862 100644 --- a/packages/core/src/layout/Sidebar/Settings/SettingsDialog.tsx +++ b/packages/core/src/layout/Sidebar/Settings/SettingsDialog.tsx @@ -15,13 +15,20 @@ */ import React from 'react'; -import { Card, CardContent, CardHeader, makeStyles } from '@material-ui/core'; +import { + Card, + CardContent, + CardHeader, + makeStyles, + Divider, +} from '@material-ui/core'; import { AppSettingsList } from './AppSettingsList'; import { AuthProvidersList } from './AuthProviderList'; import { FeatureFlagsList } from './FeatureFlagsList'; import { SignInAvatar } from './SignInAvatar'; import { UserSettingsMenu } from './UserSettingsMenu'; import { useUserProfile } from './useUserProfileInfo'; +import { useApi, featureFlagsApiRef } from '@backstage/core-api'; const useStyles = makeStyles({ root: { @@ -36,6 +43,8 @@ type Props = { export const SettingsDialog = ({ providerSettings }: Props) => { const classes = useStyles(); const { profile, displayName } = useUserProfile(); + const featureFlagsApi = useApi(featureFlagsApiRef); + const featureFlags = featureFlagsApi.getRegisteredFlags(); return ( @@ -47,8 +56,18 @@ export const SettingsDialog = ({ providerSettings }: Props) => { /> - - + {providerSettings && ( + <> + + + + )} + {featureFlags.length > 0 && ( + <> + + + + )} ); From 1cb1919f0c922d8d1e2675cb768fff16bfd1937a Mon Sep 17 00:00:00 2001 From: David Tuite Date: Tue, 15 Sep 2020 10:49:27 +0100 Subject: [PATCH 13/22] Send component description to GitHub We might as well set the description on the GitHub repo while we're creating it. --- .../src/scaffolder/stages/publish/github.test.ts | 2 ++ .../scaffolder-backend/src/scaffolder/stages/publish/github.ts | 3 +++ 2 files changed, 5 insertions(+) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts index c06a553771..e68b0d0447 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts @@ -147,6 +147,7 @@ describe('GitHub Publisher', () => { storePath: 'blam/test', owner: 'bob', access: 'bob', + description: 'description', }, directory: '/tmp/test', }); @@ -154,6 +155,7 @@ describe('GitHub Publisher', () => { expect( mockGithubClient.repos.createForAuthenticatedUser, ).toHaveBeenCalledWith({ + description: 'description', name: 'test', private: false, }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts index 1d5c9e1947..d3aafb1e07 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts @@ -61,6 +61,7 @@ export class GithubPublisher implements PublisherBase { values: RequiredTemplateValues & Record, ) { const [owner, name] = values.storePath.split('/'); + const description = values.description as string; const user = await this.client.users.getByUsername({ username: owner }); @@ -71,10 +72,12 @@ export class GithubPublisher implements PublisherBase { org: owner, private: this.repoVisibility !== 'public', visibility: this.repoVisibility, + description, }) : this.client.repos.createForAuthenticatedUser({ name, private: this.repoVisibility === 'private', + description, }); const { data } = await repoCreationPromise; From 2e35f300d0263ff22694ea21e512be75d0a7262c Mon Sep 17 00:00:00 2001 From: David Tuite Date: Tue, 15 Sep 2020 10:56:42 +0100 Subject: [PATCH 14/22] Mark docs template description as required In my testing, attempts to create a docs site with no description would fail with the error: ``` Unable to create file 'component-info.yaml' Error message: 'collections.OrderedDict object' has no attribute 'description' ``` Better to mark it required than let the user hit the error. --- .../sample-templates/docs-template/template.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml b/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml index b4bd45d163..31b57135ad 100644 --- a/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml +++ b/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml @@ -17,6 +17,7 @@ spec: schema: required: - component_id + - description properties: component_id: title: Name From 8e64ca250c276493fad9817ca36e6844e81f9630 Mon Sep 17 00:00:00 2001 From: Andrew Thauer <6507159+andrewthauer@users.noreply.github.com> Date: Thu, 17 Sep 2020 08:44:27 -0400 Subject: [PATCH 15/22] feat: add api-docs config ref for widgets --- .../catalog/EntityPageApi/EntityPageApi.tsx | 2 +- .../ApiDefinitionCard/ApiDefinitionCard.tsx | 28 +++++++---------- .../src/components/ApiDefinitionCard/index.ts | 6 +++- plugins/api-docs/src/components/index.ts | 6 +++- plugins/api-docs/src/config.ts | 30 +++++++++++++++++++ plugins/api-docs/src/plugin.ts | 19 +++++++++++- plugins/api-docs/src/routes.ts | 2 ++ 7 files changed, 71 insertions(+), 22 deletions(-) create mode 100644 plugins/api-docs/src/config.ts diff --git a/plugins/api-docs/src/catalog/EntityPageApi/EntityPageApi.tsx b/plugins/api-docs/src/catalog/EntityPageApi/EntityPageApi.tsx index 5383fc0570..e2148184b7 100644 --- a/plugins/api-docs/src/catalog/EntityPageApi/EntityPageApi.tsx +++ b/plugins/api-docs/src/catalog/EntityPageApi/EntityPageApi.tsx @@ -16,8 +16,8 @@ import { ComponentEntity, Entity } from '@backstage/catalog-model'; import { Progress } from '@backstage/core'; -import React from 'react'; import { Grid } from '@material-ui/core'; +import React from 'react'; import { ApiDefinitionCard, useComponentApiEntities, diff --git a/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.tsx b/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.tsx index 055363ae1f..49b8a69318 100644 --- a/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.tsx +++ b/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.tsx @@ -15,15 +15,16 @@ */ import { ApiEntity } from '@backstage/catalog-model'; -import { CardTab, TabbedCard } from '@backstage/core'; +import { CardTab, useApi, TabbedCard } from '@backstage/core'; import { Alert } from '@material-ui/lab'; import React from 'react'; +import { apiDocsConfigRef } from '../../config'; import { PlainApiDefinitionWidget } from '../PlainApiDefinitionWidget'; import { OpenApiDefinitionWidget } from '../OpenApiDefinitionWidget'; import { AsyncApiDefinitionWidget } from '../AsyncApiDefinitionWidget'; import { GraphQlDefinitionWidget } from '../GraphQlDefinitionWidget'; -type ApiDefinitionWidget = { +export type ApiDefinitionWidget = { type: string; title: string; component: (definition: string) => React.ReactElement; @@ -61,34 +62,25 @@ export function defaultDefinitionWidgets(): ApiDefinitionWidget[] { type Props = { apiEntity?: ApiEntity; - definitionWidgets?: ApiDefinitionWidget[]; }; -const defaultProps = { - definitionWidgets: defaultDefinitionWidgets(), -}; - -export const ApiDefinitionCard = (props: Props) => { - const { apiEntity, definitionWidgets } = { - ...defaultProps, - ...props, - }; +export const ApiDefinitionCard = ({ apiEntity }: Props) => { + const config = useApi(apiDocsConfigRef); + const { getApiDefinitionWidget } = config; if (!apiEntity) { return Could not fetch the API; } - const definitionWidget = definitionWidgets.find( - d => d.type === apiEntity.spec.type, - ); + const definitionWidget = getApiDefinitionWidget(apiEntity); if (definitionWidget) { return ( - + {definitionWidget.component(apiEntity.spec.definition)} - + { title={apiEntity.metadata.name} children={[ // Has to be an array, otherwise typescript doesn't like that this has only a single child - + ({ + id: 'plugin.api-docs.config', + description: 'Used to configure api-docs widgets', +}); + +export interface ApiDocsConfig { + getApiDefinitionWidget: ( + apiEntity: ApiEntity, + ) => ApiDefinitionWidget | undefined; +} diff --git a/plugins/api-docs/src/plugin.ts b/plugins/api-docs/src/plugin.ts index bd9c968d5b..3fcbe8a1f4 100644 --- a/plugins/api-docs/src/plugin.ts +++ b/plugins/api-docs/src/plugin.ts @@ -14,13 +14,30 @@ * limitations under the License. */ -import { createPlugin } from '@backstage/core'; +import { ApiEntity } from '@backstage/catalog-model'; +import { createApiFactory, createPlugin } from '@backstage/core'; import { ApiExplorerPage } from './components/ApiExplorerPage/ApiExplorerPage'; +import { defaultDefinitionWidgets } from './components/ApiDefinitionCard'; import { ApiEntityPage } from './components/ApiEntityPage/ApiEntityPage'; import { entityRoute, rootRoute } from './routes'; +import { apiDocsConfigRef } from './config'; export const plugin = createPlugin({ id: 'api-docs', + apis: [ + createApiFactory({ + api: apiDocsConfigRef, + deps: {}, + factory: () => { + const definitionWidgets = defaultDefinitionWidgets(); + return { + getApiDefinitionWidget: (apiEntity: ApiEntity) => { + return definitionWidgets.find(d => d.type === apiEntity.spec.type); + }, + }; + }, + }), + ], register({ router }) { router.addRoute(rootRoute, ApiExplorerPage); router.addRoute(entityRoute, ApiEntityPage); diff --git a/plugins/api-docs/src/routes.ts b/plugins/api-docs/src/routes.ts index eea911dd62..d2d8ef10c5 100644 --- a/plugins/api-docs/src/routes.ts +++ b/plugins/api-docs/src/routes.ts @@ -23,11 +23,13 @@ export const rootRoute = createRouteRef({ path: '/api-docs', title: 'APIs', }); + export const entityRoute = createRouteRef({ icon: NoIcon, path: '/api-docs/:optionalNamespaceAndName/', title: 'API', }); + export const catalogRoute = createRouteRef({ icon: NoIcon, path: '', From d8d074c1b4b8ce927529259f0e612186d3bea22f Mon Sep 17 00:00:00 2001 From: Tim Jacomb Date: Thu, 17 Sep 2020 13:48:47 +0100 Subject: [PATCH 16/22] scaffolder: Fix backend fails to start --- packages/backend/src/plugins/scaffolder.ts | 71 +++++++++++++++------- 1 file changed, 50 insertions(+), 21 deletions(-) diff --git a/packages/backend/src/plugins/scaffolder.ts b/packages/backend/src/plugins/scaffolder.ts index c4b7e22142..2793a38459 100644 --- a/packages/backend/src/plugins/scaffolder.ts +++ b/packages/backend/src/plugins/scaffolder.ts @@ -55,31 +55,60 @@ export default async function createPlugin({ const publishers = new Publishers(); - const githubToken = config.getString('scaffolder.github.token'); - const repoVisibility = config.getString( - 'scaffolder.github.visibility', - ) as RepoVisilityOptions; + const githubConfig = config.getOptionalConfig('scaffolder.github'); - const githubClient = new Octokit({ auth: githubToken }); - const githubPublisher = new GithubPublisher({ - client: githubClient, - token: githubToken, - repoVisibility, - }); - publishers.register('file', githubPublisher); - publishers.register('github', githubPublisher); + if (githubConfig) { + try { + const repoVisibility = githubConfig.getString( + 'visibility', + ) as RepoVisilityOptions; + + const githubToken = githubConfig.getString('token'); + const githubClient = new Octokit({ auth: githubToken }); + const githubPublisher = new GithubPublisher({ + client: githubClient, + token: githubToken, + repoVisibility, + }); + publishers.register('file', githubPublisher); + publishers.register('github', githubPublisher); + } catch (e) { + const providerName = 'github'; + if (process.env.NODE_ENV !== 'development') { + throw new Error( + `Failed to initialize ${providerName} scaffolding provider, ${e.message}`, + ); + } + + logger.warn( + `Skipping ${providerName} scaffolding provider, ${e.message}`, + ); + } + } const gitLabConfig = config.getOptionalConfig('scaffolder.gitlab.api'); - if (gitLabConfig) { - const gitLabToken = gitLabConfig.getString('token'); - const gitLabClient = new Gitlab({ - host: gitLabConfig.getOptionalString('baseUrl'), - token: gitLabToken, - }); - const gitLabPublisher = new GitlabPublisher(gitLabClient, gitLabToken); - publishers.register('gitlab', gitLabPublisher); - publishers.register('gitlab/api', gitLabPublisher); + try { + const gitLabToken = gitLabConfig.getString('token'); + const gitLabClient = new Gitlab({ + host: gitLabConfig.getOptionalString('baseUrl'), + token: gitLabToken, + }); + const gitLabPublisher = new GitlabPublisher(gitLabClient, gitLabToken); + publishers.register('gitlab', gitLabPublisher); + publishers.register('gitlab/api', gitLabPublisher); + } catch (e) { + const providerName = 'gitlab'; + if (process.env.NODE_ENV !== 'development') { + throw new Error( + `Failed to initialize ${providerName} scaffolding provider, ${e.message}`, + ); + } + + logger.warn( + `Skipping ${providerName} scaffolding provider, ${e.message}`, + ); + } } const dockerClient = new Docker(); From 2331e79897eb60dc355257f7c52cddb02d74b03e Mon Sep 17 00:00:00 2001 From: Iain Billett Date: Thu, 17 Sep 2020 14:18:13 +0100 Subject: [PATCH 17/22] Test for docker before attempting to run a container (#2497) Test for docker using ping() before attempting any docker operations. If this fails we know docker is unavailable and we can fail with an more descriptive error. --- .../techdocs/stages/generate/helpers.test.ts | 38 +++++++++++++++++++ .../src/techdocs/stages/generate/helpers.ts | 8 ++++ 2 files changed, 46 insertions(+) diff --git a/plugins/techdocs-backend/src/techdocs/stages/generate/helpers.test.ts b/plugins/techdocs-backend/src/techdocs/stages/generate/helpers.test.ts index 799e79ef67..283560f0ce 100644 --- a/plugins/techdocs-backend/src/techdocs/stages/generate/helpers.test.ts +++ b/plugins/techdocs-backend/src/techdocs/stages/generate/helpers.test.ts @@ -51,6 +51,10 @@ describe('helpers', () => { jest .spyOn(mockDocker, 'run') .mockResolvedValue([{ Error: null, StatusCode: 0 }]); + + jest + .spyOn(mockDocker, 'ping') + .mockResolvedValue(Buffer.from('OK', 'utf-8')); }); const imageName = 'spotify/techdocs'; @@ -99,5 +103,39 @@ describe('helpers', () => { }, ); }); + + it('should ping docker to test availability', async () => { + await runDockerContainer({ + imageName, + args, + docsDir, + resultDir, + dockerClient: mockDocker, + }); + + expect(mockDocker.ping).toHaveBeenCalled(); + }); + + describe('where docker is unavailable', () => { + const dockerError = 'a docker error'; + + beforeEach(() => { + jest.spyOn(mockDocker, 'ping').mockImplementationOnce(() => { + throw new Error(dockerError); + }); + }); + + it('should throw with a descriptive error message including the docker error message', async () => { + await expect( + runDockerContainer({ + imageName, + args, + docsDir, + resultDir, + dockerClient: mockDocker, + }), + ).rejects.toThrow(new RegExp(`.+: ${dockerError}`)); + }); + }); }); }); diff --git a/plugins/techdocs-backend/src/techdocs/stages/generate/helpers.ts b/plugins/techdocs-backend/src/techdocs/stages/generate/helpers.ts index 88667f9903..9f33823444 100644 --- a/plugins/techdocs-backend/src/techdocs/stages/generate/helpers.ts +++ b/plugins/techdocs-backend/src/techdocs/stages/generate/helpers.ts @@ -47,6 +47,14 @@ export async function runDockerContainer({ dockerClient, createOptions, }: RunDockerContainerOptions) { + try { + await dockerClient.ping(); + } catch (e) { + throw new Error( + `This operation requires Docker. Docker does not appear to be available. Docker.ping() failed with: ${e.message}`, + ); + } + await new Promise((resolve, reject) => { dockerClient.pull(imageName, {}, (err, stream) => { if (err) return reject(err); From 21a366077a0c80c64234c137f0c236d4d30614de Mon Sep 17 00:00:00 2001 From: Perry Manuk Date: Thu, 17 Sep 2020 15:26:39 +0200 Subject: [PATCH 18/22] Techdocs without docker-in-docker (#2438) We're deploying backstage to GKE and have had issues getting techdocs volume mount logic to work because of mixing paths between the host and the container running the backend. So we added a condition where if mkdocs is found in the backend container it will use that to generate the docs rather than spinning it up in a docker container. --- plugins/techdocs-backend/package.json | 1 + .../src/techdocs/stages/generate/helpers.ts | 46 +++++++++++++++++++ .../src/techdocs/stages/generate/techdocs.ts | 38 ++++++++++----- 3 files changed, 73 insertions(+), 12 deletions(-) diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index 476b321e1e..b9c2953d28 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -25,6 +25,7 @@ "@backstage/config": "^0.1.1-alpha.21", "@types/dockerode": "^2.5.34", "@types/express": "^4.17.6", + "command-exists-promise": "^2.0.2", "default-branch": "^1.0.8", "dockerode": "^3.2.1", "express": "^4.17.1", diff --git a/plugins/techdocs-backend/src/techdocs/stages/generate/helpers.ts b/plugins/techdocs-backend/src/techdocs/stages/generate/helpers.ts index 9f33823444..d68bb74aa6 100644 --- a/plugins/techdocs-backend/src/techdocs/stages/generate/helpers.ts +++ b/plugins/techdocs-backend/src/techdocs/stages/generate/helpers.ts @@ -18,6 +18,7 @@ import { Entity } from '@backstage/catalog-model'; import { Writable, PassThrough } from 'stream'; import Docker from 'dockerode'; import { SupportedGeneratorKey } from './types'; +import { spawn } from 'child_process'; // TODO: Implement proper support for more generators. export function getGeneratorKey(entity: Entity): SupportedGeneratorKey { @@ -38,6 +39,13 @@ type RunDockerContainerOptions = { createOptions?: Docker.ContainerCreateOptions; }; +export type RunCommandOptions = { + command: string; + args: string[]; + options: object; + logStream?: Writable; +}; + export async function runDockerContainer({ imageName, args, @@ -96,3 +104,41 @@ export async function runDockerContainer({ return { error, statusCode }; } + +/** + * + * @param options the options object + * @param options.command the command to run + * @param options.args the arguments to pass the command + * @param options.options options used in spawn + * @param options.logStream the log streamer to capture log messages + */ +export const runCommand = async ({ + command, + args, + options, + logStream = new PassThrough(), +}: RunCommandOptions) => { + await new Promise((resolve, reject) => { + const process = spawn(command, args, options); + + process.stdout.on('data', stream => { + logStream.write(stream); + }); + + process.stderr.on('data', stream => { + logStream.write(stream); + }); + + process.on('error', error => { + return reject(error); + }); + + process.on('close', code => { + if (code !== 0) { + return reject(`Command ${command} failed, exit code: ${code}`); + } + return resolve(); + }); + }); +}; diff --git a/plugins/techdocs-backend/src/techdocs/stages/generate/techdocs.ts b/plugins/techdocs-backend/src/techdocs/stages/generate/techdocs.ts index 40cd7590b6..cb89b42e38 100644 --- a/plugins/techdocs-backend/src/techdocs/stages/generate/techdocs.ts +++ b/plugins/techdocs-backend/src/techdocs/stages/generate/techdocs.ts @@ -24,7 +24,9 @@ import { GeneratorRunOptions, GeneratorRunResult, } from './types'; -import { runDockerContainer } from './helpers'; +import { runDockerContainer, runCommand } from './helpers'; + +const commandExists = require('command-exists-promise'); export class TechdocsGenerator implements GeneratorBase { private readonly logger: Logger; @@ -46,17 +48,29 @@ export class TechdocsGenerator implements GeneratorBase { ); try { - await runDockerContainer({ - imageName: 'spotify/techdocs', - args: ['build', '-d', '/result'], - logStream, - docsDir: directory, - resultDir, - dockerClient, - }); - this.logger.info( - `[TechDocs]: Successfully generated docs from ${directory} into ${resultDir}`, - ); + const mkdocsInstalled = await commandExists('mkdocs'); + if (mkdocsInstalled) { + await runCommand({ + command: 'mkdocs', + args: ['build', '-d', resultDir, '-v'], + options: { + cwd: directory, + }, + logStream, + }); + } else { + await runDockerContainer({ + imageName: 'spotify/techdocs', + args: ['build', '-d', '/result'], + logStream, + docsDir: directory, + resultDir, + dockerClient, + }); + this.logger.info( + `[TechDocs]: Successfully generated docs from ${directory} into ${resultDir}`, + ); + } } catch (error) { this.logger.debug( `[TechDocs]: Failed to generate docs from ${directory} into ${resultDir}`, From 51a9b70e3aadd66f68c05598ca596480d801a702 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 17 Sep 2020 19:17:31 +0200 Subject: [PATCH 19/22] e2e-test: ignore unhandled errors from jsdom --- packages/e2e-test/src/e2e-test.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/e2e-test/src/e2e-test.ts b/packages/e2e-test/src/e2e-test.ts index 83f92ea360..146a3ec644 100644 --- a/packages/e2e-test/src/e2e-test.ts +++ b/packages/e2e-test/src/e2e-test.ts @@ -403,5 +403,15 @@ async function testBackendStart(appDir: string, isPostgres: boolean) { } } -process.on('unhandledRejection', handleError); +process.on('unhandledRejection', (error: Error) => { + // Try to avoid exiting if the unhandled error is coming from jsdom, i.e. zombie. + // Those are typically errors on the page that should be benign, at least in the + // context of this test. We have other ways of asserting that the page is being + // rendered correctly. + if (error?.stack?.includes('node_modules/jsdom/lib')) { + console.log(`Ignored error inside jsdom, ${error}`); + } else { + handleError(error); + } +}); main().catch(handleError); From 975b97daf21fc1ad5cc0af8ab4c4c75366afbcc8 Mon Sep 17 00:00:00 2001 From: Jesse Alan Johnson Date: Thu, 17 Sep 2020 14:41:13 -0400 Subject: [PATCH 20/22] new tutorials (#2435) * new tutorial * prettier formatting * Update quickstart.md * prettier write * refined quickstart tutorial * Create QuickStart-app-plugin * Rename QuickStart-app-plugin to quickStart-app-plugin * Update and rename quickStart-app-plugin to quickstart-app-plugin * Adding second tutorial with prettier * removing rogue file * updating where to go link * adding sidebars entry for new tutorials --- docs/tutorials/quickstart-app-auth.md | 199 ++++++++++ docs/tutorials/quickstart-app-plugin.md | 487 ++++++++++++++++++++++++ microsite/sidebars.json | 6 +- 3 files changed, 691 insertions(+), 1 deletion(-) create mode 100644 docs/tutorials/quickstart-app-auth.md create mode 100644 docs/tutorials/quickstart-app-plugin.md diff --git a/docs/tutorials/quickstart-app-auth.md b/docs/tutorials/quickstart-app-auth.md new file mode 100644 index 0000000000..fea1d7085a --- /dev/null +++ b/docs/tutorials/quickstart-app-auth.md @@ -0,0 +1,199 @@ +--- +id: quickstart-app-auth +title: Monorepo App Setup With Authentication +--- + +###### September 15th 2020 - @backstage/create-app - v0.1.1-alpha.21 + +
+ +> This document takes you through setting up a backstage app that runs in your +> own environment. It starts with a skeleton install and verifying of the +> monorepo's functionality. Next, GitHub authentication is added and tested. +> +> This document assumes you have NodeJS 12 active along with Yarn. Please note, +> that at the time of this writing, the current version is 0.1.1-alpha.21. This +> guide can still be used with future versions, just, verify as you go. If you +> run into issues, you can compare your setup with mine here > +> [simple-backstage-app](https://github.com/johnson-jesse/simple-backstage-app). + +# The Skeleton Application + +From the terminal: + +1. Create a (monorepo) application: `npx @backstage/create-app` +1. Enter an `id` for your new app like `mybiz-backstage` I went with + `simple-backstage-app` +1. Choose `SQLite` as your database. This is the quickest way to get started as + PostgreSQL requires additional setup not covered here. +1. Start your backend: `yarn --cwd packages/backend start` + +```zsh +# You should see positive verbiage in your terminal output +2020-09-11T22:20:26.712Z backstage info Listening on :7000 +``` + +5. Finally, start the frontend. Open a new terminal window and from the root of + your project, run: `yarn start` + +```zsh +# You should see positive verbiage in your terminal output +ℹ 「wds」: Project is running at http://localhost:3000/ +``` + +Once the app compiles, a browser window should have popped with your stand alone +application loaded at `localhost:3000`. This could take a couple minutes. + +```zsh +# You should see positive verbiage in your terminal output +ℹℹ 「wdm」: Compiled successfully. +``` + +Since there is no auth currently configured, you are automatically entered as a +guest. Let's fix that now and add auth. + +# The Auth Configuration + +1. Open `app-config.yaml` and change it as follows + +_from:_ + +```yaml +auth: + providers: {} +``` + +_to:_ + +```yaml +auth: + providers: + github: + development: + clientId: + $secret: + env: AUTH_GITHUB_CLIENT_ID + clientSecret: + $secret: + env: AUTH_GITHUB_CLIENT_SECRET + ## uncomment the following three lines if using enterprise + # enterpriseInstanceUrl: + # $secret: + # env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL +``` + +2. Set environment variables in whatever fashion is easiest for you. I chose to + add mine to my `.zshrc` profile. + +```zsh +# For macOS Catalina & Z Shell +# ------ simple-backstage-app GitHub +export AUTH_GITHUB_CLIENT_ID=xxx +export AUTH_GITHUB_CLIENT_SECRET=xxx +# export AUTH_GITHUB_ENTERPRISE_INSTANCE_URL=https://github.{MY_BIZ}.com +``` + +3. And of course I need to source that file. + +```zsh +# Loading the new variables +% source ~/.zshrc + +# Any other currently opened terminals need to be restarted to pick up the new values +# verify your setup by running env +% env +# should output something like +> ... +> AUTH_GITHUB_CLIENT_ID=xxx +> AUTH_GITHUB_CLIENT_SECRET=xxx +> ... +``` + +4. The values to replace `xxx` above come from your oauth app setup. + +``` +> Log into http://github.com +> Navigate to (Settings > Developer Settings > OAuth Apps > New OAuth App)[https://github.com/settings/applications/new] +> Set Homepage URL = http://localhost:3000 +> Set Callback URL = http://localhost:7000/auth/github +> Click [Register application] +> On the next page, copy and paste your new Client ID and Client Secret to the environment variables above, `AUTH_GITHUB_CLIENT_ID` & `AUTH_GITHUB_CLIENT_SECRET` +> Don't forget to `source` that profile file again if necessary. +``` + +5. Open and change _root > packages > app > src >_`App.tsx` as follows + +```tsx +// Add the following imports to the existing list from core +import { githubAuthApiRef, SignInPage } from '@backstage/core'; +``` + +6. In the same file, change the createApp function as follows + +```tsx +const app = createApp({ + apis, + plugins: Object.values(plugins), + components: { + SignInPage: props => { + return ( + + ); + }, + }, +}); +``` + +6. Open and change _root > packages > app > src >_ `apis.ts` as follows + +```ts +// Add the following imports to the existing list from core +import { githubAuthApiRef, GithubAuth } from '@backstage/core'; +``` + +7. In the same file, change the builder block for oauthRequestApiRef as follows + +_from:_ + +```ts +builder.add(oauthRequestApiRef, new OAuthRequestManager()); +``` + +_to:_ + +```ts +const oauthRequestApi = builder.add( + oauthRequestApiRef, + new OAuthRequestManager(), +); + +builder.add( + githubAuthApiRef, + GithubAuth.create({ + discoveryApi, + oauthRequestApi, + }), +); +``` + +> Start the backend and frontend as before. When the browser loads, you should +> be presented with a login page for GitHub. Login as usual with your GitHub +> account. If this is your first time, you will be asked to authorize and then +> are redirected to the catalog page if all is well. + +# Where to go from here + +> You're probably eager to write your first custom plugin. Follow this next +> tutorial for an in-depth look at a custom GitHub repository browser plugin. +> [Adding Custom Plugin to Existing Monorepo App](quickstart-app-plugin.md). diff --git a/docs/tutorials/quickstart-app-plugin.md b/docs/tutorials/quickstart-app-plugin.md new file mode 100644 index 0000000000..28efb2a007 --- /dev/null +++ b/docs/tutorials/quickstart-app-plugin.md @@ -0,0 +1,487 @@ +--- +id: quickstart-app-plugin +title: Adding Custom Plugin to Existing Monorepo App +--- + +###### September 15th 2020 - v0.1.1-alpha.21 + +
+ +> This document takes you through setting up a new plugin for your existing +> monorepo with a _GitHub provider already setup_. If you don't have either of +> those, you can clone +> [simple-backstage-app](https://github.com/johnson-jesse/simple-backstage-app) +> which this document builds on. +> +> This document does not cover authoring a plugin for sharing with the Backstage +> community. That will have to be a later discussion. +> +> We start with a skeleton plugin install. And after verifying its +> functionality, extend the Sidebar to make our life easy. Finally, we add +> custom code to display GitHub repository information. +> +> This document assumes you have NodeJS 12 active along with Yarn. Please note, +> that at the time of this writing, the current version is 0.1.1-alpha.21. This +> guide can still be used with future versions, just, verify as you go. If you +> run into issues, you can compare your setup with mine here > +> [simple-backstage-app-plugin](https://github.com/johnson-jesse/simple-backstage-app-plugin). + +# The Skeleton Plugin + +1. Start by using the built in creator. From the terminal and root of your + project run: `yarn create-plugin` +1. Enter a plugin ID. I used `github-playground` +1. When the process finishes, let's start the backend: + `yarn --cwd packages/backend start` +1. If you see errors starting, refer to + [Auth Configuration](https://github.com/johnson-jesse/simple-backstage-app/blob/master/README.md#the-auth-configuration) + for more information on environment variables. +1. And now the frontend, from a new terminal window and the root of your + project: `yarn start` +1. As usual, a browser window should popup loading the App. +1. Now manually navigate to our plugin page from your browser: + `http://localhost:3000/github-playground` +1. You should see successful verbiage for this endpoint, + `Welcome to github-playground!` + +# The Shortcut + +Let's add a shortcut. + +1. Open and modify `root: packages > app > src > sidebar.tsx` with the + following: + +```tsx +import GitHubIcon from '@material-ui/icons/GitHub'; +... + +``` + +Simple! The App will reload with your changes automatically. You should now see +a github icon displayed in the sidebar. Clicking that will link to our new +plugin. And now, the API fun begins. + +# The Identity + +Our first modification will be to extract information from the Identity API. + +1. Start by opening + `root: plugins > github-playground > src > components > ExampleComponent > ExampleComponent.tsx` +1. Add two new imports + +```tsx +// Add identityApiRef to the list of imported from core +import { identityApiRef } from '@backstage/core'; +import { useApi } from '@backstage/core-api'; +``` + +3. Adjust the ExampleComponent from inline to block + +_from inline:_ + +```tsx +const ExampleComponent: FC<{}> = () => ( ... ) +``` + +_to block:_ + +```tsx +const ExampleComponent: FC<{}> = () => { + + return ( + ... + ) +} +``` + +4. Now add our hook and const data before the return statement + +```tsx +// our API hook +const identityApi = useApi(identityApiRef); + +// data to use +const userId = identityApi.getUserId(); +const profile = identityApi.getProfile(); +``` + +5. Finally, update the InfoCard's jsx to use our new data + +```tsx + + + {`${profile.displayName} | ${profile.email}`} + + +``` + +If everything is saved, you should see your name, id, and email on the +github-playground page. Our data accessed is synchronous. So we just grab and +go. + +6. Here is the entire file for reference +
Complete ExampleComponent.tsx +

+ +```tsx +import React, { FC } from 'react'; +import { Typography, Grid } from '@material-ui/core'; +import { + InfoCard, + Header, + Page, + pageTheme, + Content, + ContentHeader, + HeaderLabel, + SupportButton, + identityApiRef, +} from '@backstage/core'; +import { useApi } from '@backstage/core-api'; +import ExampleFetchComponent from '../ExampleFetchComponent'; + +const ExampleComponent: FC<{}> = () => { + const identityApi = useApi(identityApiRef); + const userId = identityApi.getUserId(); + const profile = identityApi.getProfile(); + + return ( + +

+ + +
+ + + A description of your plugin goes here. + + + + + + {`${profile.displayName} | ${profile.email}`} + + + + + + + + + + ); +}; + +export default ExampleComponent; +``` + +

+
+ +# The Wipe + +The last file we will touch is ExampleFetchComponent. Because of the number of +changes, let's start by wiping this component clean. + +1. Start by opening + `root: plugins > github-playground > src > components > ExampleFetchComponent > ExampleFetchComponent.tsx` +1. Replace everyting in the file with the following: + +```tsx +import React, { FC } from 'react'; +import { useAsync } from 'react-use'; +import Alert from '@material-ui/lab/Alert'; +import { + Table, + TableColumn, + Progress, + githubAuthApiRef, +} from '@backstage/core'; +import { useApi } from '@backstage/core-api'; +import { graphql } from '@octokit/graphql'; + +const ExampleFetchComponent: FC<{}> = () => { + return
Nothing to see yet
; +}; + +export default ExampleFetchComponent; +``` + +3. Save that and ensure you see no errors. Comment out the unused imports if + your linter gets in the way. + +###### We will add a lot to this file for the sake of ease. Please don't do this in productional code! + +# The Graph Model + +GitHub has a graphql API available for interacting. Let's start by adding our +basic repository query + +1. Add the query const statement outside ExampleFetchComponent + +```tsx +const query = `{ + viewer { + repositories(first: 100) { + totalCount + nodes { + name + createdAt + description + diskUsage + isFork + } + pageInfo { + endCursor + hasNextPage + } + } + } +}`; +``` + +2. Using this structure as a guide, we will break our query into type parts +3. Add the following outside of ExampleFetchComponent + +```tsx +type Node = { + name: string; + createdAt: string; + description: string; + diskUsage: number; + isFork: boolean; +}; + +type Viewer = { + repositories: { + totalCount: number; + nodes: Node[]; + pageInfo: { + endCursor: string; + hasNextPage: boolean; + }; + }; +}; +``` + +# The Tabel Model + +Using Backstage's own component library, let's define a custom table. This +component will get used if we have data to display. + +1. Add the following outside of ExampleFetchComponent + +```tsx +type DenseTableProps = { + viewer: Viewer; +}; + +export const DenseTable: FC = ({ viewer }) => { + const columns: TableColumn[] = [ + { title: 'Name', field: 'name' }, + { title: 'Created', field: 'createdAt' }, + { title: 'Description', field: 'description' }, + { title: 'Disk Usage', field: 'diskUsage' }, + { title: 'Fork', field: 'isFork' }, + ]; + + return ( + + ); +}; +``` + +# The Fetch + +We're ready to flush out our fetch component + +1. Add our api hook inside ExampleFetchComponent + +```tsx +const auth = useApi(githubAuthApiRef); +``` + +2. The access token we need to make our GitHub request and the request itself is + obtained in an asynchronous manner. +3. Add the useAsync block inside the ExampleFetchComponent + +```tsx +const { value, loading, error } = useAsync(async (): Promise => { + const token = await auth.getAccessToken(); + + const gqlEndpoint = graphql.defaults({ + // Uncomment baseUrl if using enterprise + // baseUrl: 'https://github.MY-BIZ.com/api', + headers: { + authorization: `token ${token}`, + }, + }); + const { viewer } = await gqlEndpoint(query); + return viewer; +}, []); +``` + +4. The resolved data is conventiently destructured with value containing our + Viewer type. loading as a boolean, self explainatory. And error which is + present only if necessary. So let's use those as the first 3 of 4 multi + return statements. +5. Add the _if return_ blocks below our async block + +```tsx +if (loading) return ; +if (error) return {error.message}; +if (value && value.repositories) return ; +``` + +6. The third line here utilizes our custom table accepting our Viewer type. +7. Finally, we add our _else return_ block to catch any other scenarios. + +```tsx +return ( +
+); +``` + +8. After saving that, and given we don't have any errors, you should see a table + with basic information on your repositories. +9. Here is the entire file for reference +
Complete ExampleFetchComponent.tsx +

+ +```tsx +import React, { FC } from 'react'; +import { useAsync } from 'react-use'; +import Alert from '@material-ui/lab/Alert'; +import { + Table, + TableColumn, + Progress, + githubAuthApiRef, +} from '@backstage/core'; +import { useApi } from '@backstage/core-api'; +import { graphql } from '@octokit/graphql'; + +const query = `{ +viewer { + repositories(first: 100) { + totalCount + nodes { + name + createdAt + description + diskUsage + isFork + } + pageInfo { + endCursor + hasNextPage + } + } +} +}`; + +type Node = { + name: string; + createdAt: string; + description: string; + diskUsage: number; + isFork: boolean; +}; + +type Viewer = { + repositories: { + totalCount: number; + nodes: Node[]; + pageInfo: { + endCursor: string; + hasNextPage: boolean; + }; + }; +}; + +type DenseTableProps = { + viewer: Viewer; +}; + +export const DenseTable: FC = ({ viewer }) => { + const columns: TableColumn[] = [ + { title: 'Name', field: 'name' }, + { title: 'Created', field: 'createdAt' }, + { title: 'Description', field: 'description' }, + { title: 'Disk Usage', field: 'diskUsage' }, + { title: 'Fork', field: 'isFork' }, + ]; + + return ( +

+ ); +}; + +const ExampleFetchComponent: FC<{}> = () => { + const auth = useApi(githubAuthApiRef); + + const { value, loading, error } = useAsync(async (): Promise => { + const token = await auth.getAccessToken(); + + const gqlEndpoint = graphql.defaults({ + // Uncomment baseUrl if using enterprise + // baseUrl: 'https://github.MY-BIZ.com/api', + headers: { + authorization: `token ${token}`, + }, + }); + const { viewer } = await gqlEndpoint(query); + return viewer; + }, []); + + if (loading) return ; + if (error) return {error.message}; + if (value && value.repositories) return ; + + return ( +
+ ); +}; + +export default ExampleFetchComponent; +``` + +

+ + +10. We finished! If there are no errors, you should see your own GitHub + repoistory information displayed in a basic table. If you run into issues, + you can compare the repo that backs this documdnt, + [simple-backstage-app-plugin](https://github.com/johnson-jesse/simple-backstage-app-plugin) + +# Where to go from here + +> Break apart ExampleFetchComponent into smaller logical parts contained in +> their own files. Rename your components to something other than ExampleXxx. +> +> You might be real proud of a plugin you develop. Follow this next tutorial for +> an in-depth look at publishing and including that for the entire Backstage +> community. [TODO](#). diff --git a/microsite/sidebars.json b/microsite/sidebars.json index f7aa0630ef..aac524c538 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -143,7 +143,11 @@ "ids": ["api/backend"] } ], - "Tutorials": ["tutorials/journey"], + "Tutorials": [ + "tutorials/journey", + "tutorials/quickstart-app-auth", + "tutorials/quickstart-app-plugin" + ], "Architecture Decision Records (ADRs)": [ "architecture-decisions/adrs-overview", "architecture-decisions/adrs-adr001", From 20353279d785d32cbfc1fb6df362541479dce0d3 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Thu, 17 Sep 2020 15:54:33 +0200 Subject: [PATCH 21/22] feat: use title for API type in table Now that the API definition types are available via an API, we can resolve the remaining TODO comment here. --- .../ApiExplorerPage/ApiExplorerPage.test.tsx | 8 +++++ .../ApiExplorerTable.test.tsx | 35 +++++++++++++------ .../ApiExplorerTable/ApiExplorerTable.tsx | 20 ++++++++--- 3 files changed, 48 insertions(+), 15 deletions(-) diff --git a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.test.tsx b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.test.tsx index 639f1e3d9c..e386f281e8 100644 --- a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.test.tsx +++ b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.test.tsx @@ -20,6 +20,7 @@ import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog'; import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils'; import { render } from '@testing-library/react'; import React from 'react'; +import { apiDocsConfigRef } from '../../config'; import { ApiExplorerPage } from './ApiExplorerPage'; describe('ApiCatalogPage', () => { @@ -32,6 +33,7 @@ describe('ApiCatalogPage', () => { metadata: { name: 'Entity1', }, + spec: { type: 'openapi' }, }, { apiVersion: 'backstage.io/v1alpha1', @@ -39,12 +41,17 @@ describe('ApiCatalogPage', () => { metadata: { name: 'Entity2', }, + spec: { type: 'openapi' }, }, ] as Entity[]), getLocationByEntity: () => Promise.resolve({ id: 'id', type: 'github', target: 'url' }), }; + const apiDocsConfig = { + getApiDefinitionWidget: () => undefined, + }; + const renderWrapped = (children: React.ReactNode) => render( wrapInTestApp( @@ -52,6 +59,7 @@ describe('ApiCatalogPage', () => { apis={ApiRegistry.from([ [catalogApiRef, catalogApi], [storageApiRef, MockStorageApi.create()], + [apiDocsConfigRef, apiDocsConfig], ])} > {children} diff --git a/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.test.tsx b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.test.tsx index 9cd7b01945..c679c2201f 100644 --- a/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.test.tsx +++ b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.test.tsx @@ -15,9 +15,11 @@ */ import { Entity } from '@backstage/catalog-model'; +import { ApiProvider, ApiRegistry } from '@backstage/core'; import { wrapInTestApp } from '@backstage/test-utils'; import { render } from '@testing-library/react'; import * as React from 'react'; +import { apiDocsConfigRef } from '../../config'; import { ApiExplorerTable } from './ApiExplorerTable'; const entites: Entity[] = [ @@ -25,29 +27,38 @@ const entites: Entity[] = [ apiVersion: 'backstage.io/v1alpha1', kind: 'API', metadata: { name: 'api1' }, + spec: { type: 'openapi' }, }, { apiVersion: 'backstage.io/v1alpha1', kind: 'API', metadata: { name: 'api2' }, + spec: { type: 'openapi' }, }, { apiVersion: 'backstage.io/v1alpha1', kind: 'API', metadata: { name: 'api3' }, + spec: { type: 'grpc' }, }, ]; +const apiRegistry = ApiRegistry.with(apiDocsConfigRef, { + getApiDefinitionWidget: () => undefined, +}); + describe('ApiCatalogTable component', () => { it('should render error message when error is passed in props', async () => { const rendered = render( wrapInTestApp( - , + + + , ), ); const errorMessage = await rendered.findByText( @@ -59,11 +70,13 @@ describe('ApiCatalogTable component', () => { it('should display entity names when loading has finished and no error occurred', async () => { const rendered = render( wrapInTestApp( - , + + + , ), ); expect(rendered.getByText(/APIs \(3\)/)).toBeInTheDocument(); diff --git a/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx index fd1e9c3518..485fcc216d 100644 --- a/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx +++ b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx @@ -14,14 +14,23 @@ * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; -import { Table, TableColumn } from '@backstage/core'; -import { Link, Chip } from '@material-ui/core'; +import { ApiEntityV1alpha1, Entity } from '@backstage/catalog-model'; +import { Table, TableColumn, useApi } from '@backstage/core'; +import { Chip, Link } from '@material-ui/core'; import { Alert } from '@material-ui/lab'; import React from 'react'; import { generatePath, Link as RouterLink } from 'react-router-dom'; +import { apiDocsConfigRef } from '../../config'; import { entityRoute } from '../../routes'; +const ApiTypeTitle = ({ apiEntity }: { apiEntity: ApiEntityV1alpha1 }) => { + const config = useApi(apiDocsConfigRef); + const definition = config.getApiDefinitionWidget(apiEntity); + const type = definition ? definition.title : apiEntity.spec.type; + + return {type}; +}; + const columns: TableColumn[] = [ { title: 'Name', @@ -54,8 +63,11 @@ const columns: TableColumn[] = [ field: 'spec.lifecycle', }, { - title: 'Type', // TODO: Resolve the type display name using the API from https://github.com/spotify/backstage/pull/2451 + title: 'Type', field: 'spec.type', + render: (entity: Entity) => ( + + ), }, { title: 'Description', From 6341af5b31467a564778fb217b967208872f0f10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20=C3=85lund?= Date: Fri, 18 Sep 2020 11:24:51 +0200 Subject: [PATCH 22/22] [microsite] Add CNCF logo (#2511) --- microsite/pages/en/index.js | 13 +++++++++++++ microsite/static/css/custom.css | 11 +++++++++++ microsite/static/img/cncf-color.svg | 1 + microsite/static/img/cncf-white.svg | 1 + 4 files changed, 26 insertions(+) create mode 100644 microsite/static/img/cncf-color.svg create mode 100644 microsite/static/img/cncf-white.svg diff --git a/microsite/pages/en/index.js b/microsite/pages/en/index.js index f595032bd0..37298ae8e2 100644 --- a/microsite/pages/en/index.js +++ b/microsite/pages/en/index.js @@ -462,6 +462,19 @@ class Index extends React.Component { Contribute + + + + + Backstage is a{' '} + + Cloud Native Computing Foundation + {' '} + sandbox project + +
+ + ); } diff --git a/microsite/static/css/custom.css b/microsite/static/css/custom.css index 111065a5d5..00a102ba00 100644 --- a/microsite/static/css/custom.css +++ b/microsite/static/css/custom.css @@ -1070,3 +1070,14 @@ code { margin: auto; } } + +.cncf-block { + text-align: center; +} + +.cncf-logo { + background: center no-repeat url(../img/cncf-white.svg); + width: 100%; + height: 100px; + margin-bottom: 40px; +} diff --git a/microsite/static/img/cncf-color.svg b/microsite/static/img/cncf-color.svg new file mode 100644 index 0000000000..12f7d3e48a --- /dev/null +++ b/microsite/static/img/cncf-color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/microsite/static/img/cncf-white.svg b/microsite/static/img/cncf-white.svg new file mode 100644 index 0000000000..d94aaf3249 --- /dev/null +++ b/microsite/static/img/cncf-white.svg @@ -0,0 +1 @@ + \ No newline at end of file