From 2b7d3455b54c54f63145d7478576f650ebc1bab3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20Lindstr=C3=B6m?= Date: Tue, 17 Aug 2021 14:44:30 +0200 Subject: [PATCH] Composable Homepage (#6486) feat: implement homepage plugin --- .changeset/blue-tables-sparkle.md | 9 ++ .github/CODEOWNERS | 1 + packages/app/package.json | 1 + packages/app/src/App.tsx | 7 + packages/app/src/components/home/HomePage.tsx | 72 ++++++++++ plugins/home/.eslintrc.js | 3 + plugins/home/README.md | 95 +++++++++++++ plugins/home/api-report.md | 130 ++++++++++++++++++ plugins/home/dev/index.tsx | 27 ++++ plugins/home/package.json | 51 +++++++ .../componentRenderers/ComponentAccordion.tsx | 102 ++++++++++++++ .../ComponentTabs/ComponentTab.tsx | 36 +++++ .../ComponentTabs/ComponentTabs.tsx | 53 +++++++ .../componentRenderers/ComponentTabs/index.ts | 18 +++ plugins/home/src/componentRenderers/index.ts | 18 +++ .../components/HomepageCompositionRoot.tsx | 33 +++++ plugins/home/src/components/SettingsModal.tsx | 48 +++++++ plugins/home/src/components/index.ts | 18 +++ plugins/home/src/extensions.tsx | 123 +++++++++++++++++ .../homePageComponents/RandomJoke/Actions.tsx | 29 ++++ .../homePageComponents/RandomJoke/Content.tsx | 31 +++++ .../homePageComponents/RandomJoke/Context.tsx | 99 +++++++++++++ .../RandomJoke/Settings.tsx | 48 +++++++ .../homePageComponents/RandomJoke/index.ts | 20 +++ plugins/home/src/index.ts | 26 ++++ plugins/home/src/plugin.test.ts | 22 +++ plugins/home/src/plugin.ts | 68 +++++++++ plugins/home/src/routes.ts | 20 +++ plugins/home/src/setupTests.ts | 17 +++ 29 files changed, 1225 insertions(+) create mode 100644 .changeset/blue-tables-sparkle.md create mode 100644 packages/app/src/components/home/HomePage.tsx create mode 100644 plugins/home/.eslintrc.js create mode 100644 plugins/home/README.md create mode 100644 plugins/home/api-report.md create mode 100644 plugins/home/dev/index.tsx create mode 100644 plugins/home/package.json create mode 100644 plugins/home/src/componentRenderers/ComponentAccordion.tsx create mode 100644 plugins/home/src/componentRenderers/ComponentTabs/ComponentTab.tsx create mode 100644 plugins/home/src/componentRenderers/ComponentTabs/ComponentTabs.tsx create mode 100644 plugins/home/src/componentRenderers/ComponentTabs/index.ts create mode 100644 plugins/home/src/componentRenderers/index.ts create mode 100644 plugins/home/src/components/HomepageCompositionRoot.tsx create mode 100644 plugins/home/src/components/SettingsModal.tsx create mode 100644 plugins/home/src/components/index.ts create mode 100644 plugins/home/src/extensions.tsx create mode 100644 plugins/home/src/homePageComponents/RandomJoke/Actions.tsx create mode 100644 plugins/home/src/homePageComponents/RandomJoke/Content.tsx create mode 100644 plugins/home/src/homePageComponents/RandomJoke/Context.tsx create mode 100644 plugins/home/src/homePageComponents/RandomJoke/Settings.tsx create mode 100644 plugins/home/src/homePageComponents/RandomJoke/index.ts create mode 100644 plugins/home/src/index.ts create mode 100644 plugins/home/src/plugin.test.ts create mode 100644 plugins/home/src/plugin.ts create mode 100644 plugins/home/src/routes.ts create mode 100644 plugins/home/src/setupTests.ts diff --git a/.changeset/blue-tables-sparkle.md b/.changeset/blue-tables-sparkle.md new file mode 100644 index 0000000000..1656375267 --- /dev/null +++ b/.changeset/blue-tables-sparkle.md @@ -0,0 +1,9 @@ +--- +'@backstage/plugin-home': minor +--- + +Create the `Home` plugin which exports some basic functionality that's used to compose a homepage. An example of a composed homepage is added to the example app. + +This change also introduces the `createCardExtension` which creates a lazy loaded card that is intended to be used for homepage components. + +Adoption of this homepage requires setup similar to what can be found in the example app. diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index fda500ed02..781feb1f04 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -17,6 +17,7 @@ /plugins/techdocs @backstage/techdocs-core /plugins/techdocs-backend @backstage/techdocs-core /plugins/ilert @yacut +/plugins/home @backstage/techdocs-core /packages/search-common @backstage/techdocs-core /packages/techdocs-common @backstage/techdocs-core /.changeset/cost-insights-* @backstage/silver-lining diff --git a/packages/app/package.json b/packages/app/package.json index dcf14c88c1..1778857cfb 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -23,6 +23,7 @@ "@backstage/plugin-gcp-projects": "^0.3.2", "@backstage/plugin-github-actions": "^0.4.16", "@backstage/plugin-graphiql": "^0.2.14", + "@backstage/plugin-home": "^0.1.1", "@backstage/plugin-jenkins": "^0.5.3", "@backstage/plugin-kafka": "^0.2.13", "@backstage/plugin-kubernetes": "^0.4.11", diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index f0ee099924..a73039fed2 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -20,6 +20,7 @@ import { OAuthRequestDialog, SignInPage, } from '@backstage/core-components'; +import { HomepageCompositionRoot } from '@backstage/plugin-home'; import { apiDocsPlugin, ApiExplorerPage } from '@backstage/plugin-api-docs'; import { CatalogEntityPage, @@ -63,6 +64,8 @@ import { Root } from './components/Root'; import { entityPage } from './components/catalog/EntityPage'; import { searchPage } from './components/search/SearchPage'; import { LowerCaseValuePickerFieldExtension } from './components/scaffolder/customScaffolderExtensions'; +import { HomePage } from './components/home/HomePage'; + import { providers } from './identityProviders'; import * as plugins from './plugins'; @@ -108,6 +111,10 @@ const AppRouter = app.getRouter(); const routes = ( + {/* TODO(rubenl): Move this to / once its more mature and components exist */} + }> + + } /> ( + + + + + + + + + + + ( + + ), + }, + { + label: 'Any', + Component: () => ( + + ), + }, + ]} + /> + + +); diff --git a/plugins/home/.eslintrc.js b/plugins/home/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/home/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/home/README.md b/plugins/home/README.md new file mode 100644 index 0000000000..a09c85775b --- /dev/null +++ b/plugins/home/README.md @@ -0,0 +1,95 @@ +# Home + +Development is ongoing. You can follow the progress and contribute at the Backstage [Home Project Board](https://github.com/backstage/backstage/projects/7) or reach out to us in the [`#support` Discord channel](https://discord.com/channels/687207715902193673/687235481154617364). + +## Overview + +The Home plugin introduces a system for composing a Home Page for Backstage in order to surface relevant info and provide convenient shortcuts for common tasks. It's designed with composability in mind with an open ecosystem that allows anyone to contribute with any component, to be included in any Home Page. + +For App Integrators, the system is designed to be composable to give total freedom in designing a Home Page that suits the needs of the organization. From the perspective of a Component Developer who wishes to contribute with building blocks to be included in Home Pages, there's a convenient interface for bundling the different parts and exporting them with both error boundary and lazy loading handled under the surface. + +## Getting started + +If you have a standalone app (you didn't clone this repo), then do + +```bash +# From the Backstage repository root +cd packages/app +yarn add @backstage/plugin-home +``` + +### Setting up the Home Page + +1. Create a Home Page Component that will be used for composition. + +`packages/app/src/components/home/HomePage.tsx` + +```tsx +import React from 'react'; + +export const HomePage = () => { + return { + /* TODO: Compose a Home Page here */ + }; +}; +``` + +2. Add a route where the homepage will live, presumably `/`. + +`packages/app/src/App.tsx` + +```tsx +import { HomepageCompositionRoot } from '@backstage/plugin-home'; +import { HomePage } from './components/home/HomePage'; + +// ... +}> + +; +// ... +``` + +### Creating Components + +The Home Page can be composed with regular React components, so there's no magic in creating components to be used for composition 🪄 🎩 . However, in order to assure that your component fits into a diverse set of Home Pages, there's an extension creator for this purpose, that creates a Card-based layout, for consistency between components (read more about extensions [here](https://backstage.io/docs/plugins/composability#extensions)). The extension creator requires two fields: `title` and `components`. The `components` field is expected to be an asynchronous import that should at least contain a `Content` field. Additionally, you can optionally provide `settings`, `actions` and `contextProvider` as well. These parts will be combined to create a card, where the `content`, `actions` and `settings` will be wrapped within the `contextProvider` in order to be able to access to context and effectively communicate with one another. + +Finally, the `createCardExtension` also accepts a generic, such that Component Developers can indicate to App Integrators what custom props their component will accept, such as the example below where the default category of the random jokes can be set. + +```tsx +export const RandomJokeHomePageComponent = homePlugin.provide( + createCardExtension<{ defaultCategory?: 'programming' | 'any' }>({ + title: 'Random Joke', + components: () => import('./homePageComponents/RandomJoke'), + }), +); +``` + +In summary: it is not necessary to use the `createCardExtension` extension creator to register a home page component, although it is convenient since it provides error boundary and lazy loading, and it also may hook into other functionality in the future. + +### Composing a Home Page + +Composing a Home Page is no different from creating a regular React Component, i.e. the App Integrator is free to include whatever content they like. However, there are components developed with the Home Page in mind, as described in the previous section. If created by the `createCardExtension` extension creator, they are rendered like so + +```tsx +import React from 'react' +import Grid from '@material-ui/core/Grid' +import { RandomJokeHomePageComponent } from '@backstage/plugin-home'; + +export const HomePage = () => { + return ( + + + + + + ) +} +``` + +Additionally, the App Integrator is provided an escape hatch in case the way the card is rendered does not fit their requirements. They may optionally pass the `Renderer`-prop, which will receive the `title`, `content` and optionally `actions`, `settings` and `contextProvider`, if they exist for the component. This allows the App Integrator to render the content in any way they want. + +## Contributing + +We believe that people have great ideas for what makes a useful Home Page, and we want to make it easy for every to benefit from the effort you put in to create something cool for the Home Page. Therefore, a great way of contributing is by simply creating more Home Page Components, than can then be used by everyone when composing their own Home Page. If they are tightly coupled to an existing plugin, it is recommended to allow them to live within that plugin, for convenience and to limit complex dependencies. On the other hand, if there's no clear plugin that the component is based on, it's also fine to contribute them into the [home plugin](/plugins/home/src/homePageComponents) + +Additionally, the API is at a very early state, so contributing with additional use cases may expose weaknesses in the current solution that we may iterate on, to provide more flexibility and ease of use for those who wish to develop components for the Home Page. diff --git a/plugins/home/api-report.md b/plugins/home/api-report.md new file mode 100644 index 0000000000..a2350b803b --- /dev/null +++ b/plugins/home/api-report.md @@ -0,0 +1,130 @@ +## API Report File for "@backstage/plugin-home" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { Extension } from '@backstage/core-plugin-api'; +import { ReactNode } from 'react'; +import { RouteRef } from '@backstage/core-plugin-api'; + +// Warning: (ae-missing-release-tag) "ComponentAccordion" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const ComponentAccordion: ({ + title, + Content, + Actions, + Settings, + ContextProvider, + ...childProps +}: { + title: string; + Content: () => JSX.Element; + Actions?: (() => JSX.Element) | undefined; + Settings?: (() => JSX.Element) | undefined; + ContextProvider?: ((props: any) => JSX.Element) | undefined; +}) => JSX.Element; + +// Warning: (ae-missing-release-tag) "ComponentTab" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const ComponentTab: ({ + title, + Content, + ContextProvider, + ...childProps +}: { + title: string; + Content: () => JSX.Element; + ContextProvider?: ((props: any) => JSX.Element) | undefined; +}) => JSX.Element; + +// Warning: (ae-missing-release-tag) "ComponentTabs" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const ComponentTabs: ({ + title, + tabs, +}: { + title: string; + tabs: { + label: string; + Component: () => JSX.Element; + }[]; +}) => JSX.Element; + +// Warning: (ae-forgotten-export) The symbol "ComponentRenderer" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "createCardExtension" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function createCardExtension({ + title, + components, +}: { + title: string; + components: () => Promise; +}): Extension< + ({ + Renderer, + title: overrideTitle, + ...childProps + }: ComponentRenderer & { + title?: string; + } & T) => JSX.Element +>; + +// Warning: (ae-missing-release-tag) "HomepageCompositionRoot" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const HomepageCompositionRoot: (props: { + title?: string | undefined; + children?: ReactNode; +}) => JSX.Element; + +// Warning: (ae-missing-release-tag) "homePlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const homePlugin: BackstagePlugin< + { + root: RouteRef; + }, + {} +>; + +// Warning: (ae-missing-release-tag) "RandomJokeHomePageComponent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const RandomJokeHomePageComponent: ({ + Renderer, + title: overrideTitle, + ...childProps +}: ComponentRenderer & { + title?: string | undefined; +} & { + defaultCategory?: 'any' | 'programming' | undefined; +}) => JSX.Element; + +// Warning: (ae-missing-release-tag) "SettingsModal" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const SettingsModal: ({ + open, + close, + componentName, + children, +}: { + open: boolean; + close: Function; + componentName: string; + children: JSX.Element; +}) => JSX.Element; + +// Warnings were encountered during analysis: +// +// src/extensions.d.ts:16:5 - (ae-forgotten-export) The symbol "ComponentParts" needs to be exported by the entry point index.d.ts + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/home/dev/index.tsx b/plugins/home/dev/index.tsx new file mode 100644 index 0000000000..802203e9f4 --- /dev/null +++ b/plugins/home/dev/index.tsx @@ -0,0 +1,27 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 { createDevApp } from '@backstage/dev-utils'; +import { homePlugin, HomepageCompositionRoot } from '../src/plugin'; + +createDevApp() + .registerPlugin(homePlugin) + .addPage({ + element: , + title: 'Root Page', + path: '/', + }) + .render(); diff --git a/plugins/home/package.json b/plugins/home/package.json new file mode 100644 index 0000000000..b2c1741728 --- /dev/null +++ b/plugins/home/package.json @@ -0,0 +1,51 @@ +{ + "name": "@backstage/plugin-home", + "version": "0.1.1", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "build": "backstage-cli plugin:build", + "start": "backstage-cli plugin:serve", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "diff": "backstage-cli plugin:diff", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", + "@backstage/theme": "^0.2.9", + "@material-ui/core": "^4.12.2", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.45", + "@types/react": "^16.9", + "react": "^16.13.1", + "react-dom": "^16.13.1", + "react-router": "^6.0.0-beta.0", + "react-use": "^17.2.4" + }, + "devDependencies": { + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^11.2.5", + "@testing-library/user-event": "^13.1.8", + "@types/jest": "^26.0.7", + "@types/node": "^14.14.32", + "cross-fetch": "^3.0.6", + "msw": "^0.29.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/home/src/componentRenderers/ComponentAccordion.tsx b/plugins/home/src/componentRenderers/ComponentAccordion.tsx new file mode 100644 index 0000000000..5cf54d7a0c --- /dev/null +++ b/plugins/home/src/componentRenderers/ComponentAccordion.tsx @@ -0,0 +1,102 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 { + Accordion, + AccordionDetails, + AccordionSummary, + Typography, + IconButton, + Theme, +} from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import SettingsIcon from '@material-ui/icons/Settings'; + +import { SettingsModal } from '../components'; + +const useStyles = makeStyles((theme: Theme) => ({ + settingsIconButton: { + padding: theme.spacing(0, 1, 0, 0), + }, +})); + +export const ComponentAccordion = ({ + title, + Content, + Actions, + Settings, + ContextProvider, + ...childProps +}: { + title: string; + Content: () => JSX.Element; + Actions?: () => JSX.Element; + Settings?: () => JSX.Element; + ContextProvider?: (props: any) => JSX.Element; +}) => { + const classes = useStyles(); + const [settingsIsExpanded, setSettingsIsExpanded] = React.useState(false); + const [isExpanded, setIsExpanded] = React.useState(false); + + const handleOpenSettings = (e: any) => { + e.stopPropagation(); + setSettingsIsExpanded(prevState => !prevState); + }; + + const innerContent = ( + <> + {Settings && ( + setSettingsIsExpanded(false)} + componentName={title} + > + + + )} + setIsExpanded(expanded)} + > + }> + {Settings && ( + + + + )} + {title} + + +
+ + {Actions && } +
+
+
+ + ); + + return ContextProvider ? ( + {innerContent} + ) : ( + innerContent + ); +}; diff --git a/plugins/home/src/componentRenderers/ComponentTabs/ComponentTab.tsx b/plugins/home/src/componentRenderers/ComponentTabs/ComponentTab.tsx new file mode 100644 index 0000000000..2b3e4e746c --- /dev/null +++ b/plugins/home/src/componentRenderers/ComponentTabs/ComponentTab.tsx @@ -0,0 +1,36 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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'; + +export const ComponentTab = ({ + title, + Content, + ContextProvider, + ...childProps +}: { + title: string; + Content: () => JSX.Element; + ContextProvider?: (props: any) => JSX.Element; +}) => { + return ContextProvider ? ( + + + + ) : ( + + ); +}; diff --git a/plugins/home/src/componentRenderers/ComponentTabs/ComponentTabs.tsx b/plugins/home/src/componentRenderers/ComponentTabs/ComponentTabs.tsx new file mode 100644 index 0000000000..f404a80980 --- /dev/null +++ b/plugins/home/src/componentRenderers/ComponentTabs/ComponentTabs.tsx @@ -0,0 +1,53 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { Tabs, Tab } from '@material-ui/core'; +import { InfoCard } from '@backstage/core-components'; + +type TabType = { + label: string; + Component: () => JSX.Element; +}; + +export const ComponentTabs = ({ + title, + tabs, +}: { + title: string; + tabs: TabType[]; +}) => { + const [value, setValue] = React.useState(0); + + const handleChange = (_event: any, newValue: number) => { + setValue(newValue); + }; + + return ( + + + {tabs.map(t => ( + + ))} + + {tabs.map(({ Component }, idx) => ( +
+ +
+ ))} +
+ ); +}; diff --git a/plugins/home/src/componentRenderers/ComponentTabs/index.ts b/plugins/home/src/componentRenderers/ComponentTabs/index.ts new file mode 100644 index 0000000000..672d6e8385 --- /dev/null +++ b/plugins/home/src/componentRenderers/ComponentTabs/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { ComponentTabs } from './ComponentTabs'; +export { ComponentTab } from './ComponentTab'; diff --git a/plugins/home/src/componentRenderers/index.ts b/plugins/home/src/componentRenderers/index.ts new file mode 100644 index 0000000000..5c5d3a457a --- /dev/null +++ b/plugins/home/src/componentRenderers/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { ComponentAccordion } from './ComponentAccordion'; +export { ComponentTabs, ComponentTab } from './ComponentTabs'; diff --git a/plugins/home/src/components/HomepageCompositionRoot.tsx b/plugins/home/src/components/HomepageCompositionRoot.tsx new file mode 100644 index 0000000000..92232fb387 --- /dev/null +++ b/plugins/home/src/components/HomepageCompositionRoot.tsx @@ -0,0 +1,33 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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, { ReactNode } from 'react'; +import { useOutlet } from 'react-router'; +import { Content, Header, Page } from '@backstage/core-components'; + +export const HomepageCompositionRoot = (props: { + title?: string; + children?: ReactNode; +}) => { + const outlet = useOutlet(); + const children = props.children ?? outlet; + return ( + +
+ {children} + + ); +}; diff --git a/plugins/home/src/components/SettingsModal.tsx b/plugins/home/src/components/SettingsModal.tsx new file mode 100644 index 0000000000..25566bccdf --- /dev/null +++ b/plugins/home/src/components/SettingsModal.tsx @@ -0,0 +1,48 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, +} from '@material-ui/core'; + +export const SettingsModal = ({ + open, + close, + componentName, + children, +}: { + open: boolean; + close: Function; + componentName: string; + children: JSX.Element; +}) => { + return ( + close()}> + Settings - {componentName} + {children} + + + + + ); +}; diff --git a/plugins/home/src/components/index.ts b/plugins/home/src/components/index.ts new file mode 100644 index 0000000000..aae0cbe26d --- /dev/null +++ b/plugins/home/src/components/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { HomepageCompositionRoot } from './HomepageCompositionRoot'; +export { SettingsModal } from './SettingsModal'; diff --git a/plugins/home/src/extensions.tsx b/plugins/home/src/extensions.tsx new file mode 100644 index 0000000000..93fe4ceb4f --- /dev/null +++ b/plugins/home/src/extensions.tsx @@ -0,0 +1,123 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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, { Suspense } from 'react'; +import { IconButton } from '@material-ui/core'; +import SettingsIcon from '@material-ui/icons/Settings'; +import { InfoCard } from '@backstage/core-components'; +import { SettingsModal } from './components'; +import { createReactExtension, useApp } from '@backstage/core-plugin-api'; + +export type ComponentRenderer = { + Renderer?: (props: RendererProps) => JSX.Element; +}; + +type ComponentParts = { + Content: () => JSX.Element; + Actions?: () => JSX.Element; + Settings?: () => JSX.Element; + ContextProvider?: (props: any) => JSX.Element; +}; + +type RendererProps = { title: string } & ComponentParts; + +export function createCardExtension({ + title, + components, +}: { + title: string; + components: () => Promise; +}) { + return createReactExtension({ + component: { + lazy: () => + components().then(({ Content, Actions, Settings, ContextProvider }) => { + const CardExtension = ({ + Renderer, + title: overrideTitle, + ...childProps + }: ComponentRenderer & { title?: string } & T) => { + const app = useApp(); + const { Progress } = app.getComponents(); + const [settingsOpen, setSettingsOpen] = React.useState(false); + + if (Renderer) { + return ( + }> + + + ); + } + + const cardProps = { + title: overrideTitle ?? title, + ...(Settings + ? { + action: ( + setSettingsOpen(true)}> + Settings + + ), + } + : {}), + ...(Actions + ? { + actions: , + } + : {}), + }; + + const innerContent = ( + + {Settings && ( + setSettingsOpen(false)} + > + + + )} + + + ); + + return ( + }> + {ContextProvider ? ( + + {innerContent} + + ) : ( + innerContent + )} + + ); + }; + return CardExtension; + }), + }, + }); +} diff --git a/plugins/home/src/homePageComponents/RandomJoke/Actions.tsx b/plugins/home/src/homePageComponents/RandomJoke/Actions.tsx new file mode 100644 index 0000000000..cebb0400ab --- /dev/null +++ b/plugins/home/src/homePageComponents/RandomJoke/Actions.tsx @@ -0,0 +1,29 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; + +import { Button } from '@material-ui/core'; +import { useRandomJoke } from './Context'; + +export const Actions = () => { + const { rerollJoke } = useRandomJoke(); + return ( + + ); +}; diff --git a/plugins/home/src/homePageComponents/RandomJoke/Content.tsx b/plugins/home/src/homePageComponents/RandomJoke/Content.tsx new file mode 100644 index 0000000000..231e32356a --- /dev/null +++ b/plugins/home/src/homePageComponents/RandomJoke/Content.tsx @@ -0,0 +1,31 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 { useRandomJoke } from './Context'; + +export const Content = () => { + const { joke, loading } = useRandomJoke(); + + if (loading) return

Loading...

; + + return ( +
+

{joke.setup}

+

{joke.punchline}

+
+ ); +}; diff --git a/plugins/home/src/homePageComponents/RandomJoke/Context.tsx b/plugins/home/src/homePageComponents/RandomJoke/Context.tsx new file mode 100644 index 0000000000..5c2790d8a5 --- /dev/null +++ b/plugins/home/src/homePageComponents/RandomJoke/Context.tsx @@ -0,0 +1,99 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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, { createContext } from 'react'; + +export type JokeType = 'any' | 'programming'; + +type Joke = { + setup: string; + punchline: string; +}; + +type RandomJokeContextValue = { + loading: boolean; + joke: Joke; + type: JokeType; + rerollJoke: Function; + handleChangeType: Function; +}; + +const Context = createContext(undefined); + +const getNewJoke = (type: string): Promise => + fetch( + `https://official-joke-api.appspot.com/jokes${ + type !== 'any' ? `/${type}` : '' + }/random`, + ) + .then(res => res.json()) + .then(data => (Array.isArray(data) ? data[0] : data)); + +export const ContextProvider = ({ + children, + defaultCategory, +}: { + children: JSX.Element; + defaultCategory?: JokeType; +}) => { + const [loading, setLoading] = React.useState(true); + const [joke, setJoke] = React.useState({ + setup: '', + punchline: '', + }); + const [type, setType] = React.useState( + defaultCategory || ('programming' as JokeType), + ); + + const rerollJoke = React.useCallback(() => { + setLoading(true); + getNewJoke(type).then(newJoke => setJoke(newJoke)); + }, [type]); + + const handleChangeType = (newType: JokeType) => { + setType(newType); + }; + + React.useEffect(() => { + setLoading(false); + }, [joke]); + + React.useEffect(() => { + rerollJoke(); + }, [rerollJoke]); + + const value: RandomJokeContextValue = { + loading, + joke, + type, + rerollJoke, + handleChangeType, + }; + + return {children}; +}; + +export const useRandomJoke = () => { + const value = React.useContext(Context); + + if (value === undefined) { + throw new Error('useRandomJoke must be used within a RandomJokeProvider'); + } + + return value; +}; + +export default Context; diff --git a/plugins/home/src/homePageComponents/RandomJoke/Settings.tsx b/plugins/home/src/homePageComponents/RandomJoke/Settings.tsx new file mode 100644 index 0000000000..ee5de69dc6 --- /dev/null +++ b/plugins/home/src/homePageComponents/RandomJoke/Settings.tsx @@ -0,0 +1,48 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 { + FormControl, + FormLabel, + RadioGroup, + FormControlLabel, + Radio, +} from '@material-ui/core'; +import React from 'react'; +import { useRandomJoke, JokeType } from './Context'; + +export const Settings = () => { + const { type, handleChangeType } = useRandomJoke(); + const JOKE_TYPES: JokeType[] = ['any' as JokeType, 'programming' as JokeType]; + return ( + + Joke Type + handleChangeType(e.target.value)} + > + {JOKE_TYPES.map(t => ( + } + label={`${t.slice(0, 1).toUpperCase()}${t.slice(1)}`} + /> + ))} + + + ); +}; diff --git a/plugins/home/src/homePageComponents/RandomJoke/index.ts b/plugins/home/src/homePageComponents/RandomJoke/index.ts new file mode 100644 index 0000000000..aafbc17a2e --- /dev/null +++ b/plugins/home/src/homePageComponents/RandomJoke/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { Actions } from './Actions'; +export { Content } from './Content'; +export { Settings } from './Settings'; +export { ContextProvider } from './Context'; diff --git a/plugins/home/src/index.ts b/plugins/home/src/index.ts new file mode 100644 index 0000000000..812e1b2495 --- /dev/null +++ b/plugins/home/src/index.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { + homePlugin, + HomepageCompositionRoot, + RandomJokeHomePageComponent, + ComponentAccordion, + ComponentTabs, + ComponentTab, +} from './plugin'; +export { SettingsModal } from './components'; +export { createCardExtension } from './extensions'; diff --git a/plugins/home/src/plugin.test.ts b/plugins/home/src/plugin.test.ts new file mode 100644 index 0000000000..920a9076f4 --- /dev/null +++ b/plugins/home/src/plugin.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 { homePlugin } from './plugin'; + +describe('home', () => { + it('should export plugin', () => { + expect(homePlugin).toBeDefined(); + }); +}); diff --git a/plugins/home/src/plugin.ts b/plugins/home/src/plugin.ts new file mode 100644 index 0000000000..5600d2d315 --- /dev/null +++ b/plugins/home/src/plugin.ts @@ -0,0 +1,68 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 { + createComponentExtension, + createPlugin, + createRoutableExtension, +} from '@backstage/core-plugin-api'; +import { createCardExtension } from './extensions'; + +import { rootRouteRef } from './routes'; + +export const homePlugin = createPlugin({ + id: 'home', + routes: { + root: rootRouteRef, + }, +}); + +export const HomepageCompositionRoot = homePlugin.provide( + createRoutableExtension({ + component: () => + import('./components').then(m => m.HomepageCompositionRoot), + mountPoint: rootRouteRef, + }), +); + +export const ComponentAccordion = homePlugin.provide( + createComponentExtension({ + component: { + lazy: () => + import('./componentRenderers').then(m => m.ComponentAccordion), + }, + }), +); +export const ComponentTabs = homePlugin.provide( + createComponentExtension({ + component: { + lazy: () => import('./componentRenderers').then(m => m.ComponentTabs), + }, + }), +); +export const ComponentTab = homePlugin.provide( + createComponentExtension({ + component: { + lazy: () => import('./componentRenderers').then(m => m.ComponentTab), + }, + }), +); + +export const RandomJokeHomePageComponent = homePlugin.provide( + createCardExtension<{ defaultCategory?: 'any' | 'programming' }>({ + title: 'Random Joke', + components: () => import('./homePageComponents/RandomJoke'), + }), +); diff --git a/plugins/home/src/routes.ts b/plugins/home/src/routes.ts new file mode 100644 index 0000000000..2c641d8433 --- /dev/null +++ b/plugins/home/src/routes.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 { createRouteRef } from '@backstage/core-plugin-api'; + +export const rootRouteRef = createRouteRef({ + title: 'home', +}); diff --git a/plugins/home/src/setupTests.ts b/plugins/home/src/setupTests.ts new file mode 100644 index 0000000000..fc6dbd98f8 --- /dev/null +++ b/plugins/home/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 '@testing-library/jest-dom'; +import 'cross-fetch/polyfill';