diff --git a/app-config.yaml b/app-config.yaml index d524041ef..cd0f653e0 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -14,6 +14,8 @@ app: links: - url: https://backstage.io/blog/ title: Backstage Blog + experimental: + packages: all organization: name: Backstage diff --git a/package.json b/package.json index 0b195754a..b83496952 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "scripts": { "start": "backstage-cli repo start", "start:app-migrated": "backstage-cli repo start app-migrated backend", + "start:app-next": "backstage-cli repo start app-next backend", "start:otel-prerequisites": "docker-compose -f ./open-telemetry/docker-compose.yaml up", "build:backend": "yarn workspace backend build", "build:all": "backstage-cli repo build --all", diff --git a/packages/app-next/.eslintrc.js b/packages/app-next/.eslintrc.js new file mode 100644 index 000000000..e2a53a6ad --- /dev/null +++ b/packages/app-next/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/app-next/config.d.ts b/packages/app-next/config.d.ts new file mode 100644 index 000000000..a92c543d4 --- /dev/null +++ b/packages/app-next/config.d.ts @@ -0,0 +1,14 @@ +export interface Config { + /** + * @visibility frontend + */ + notificationsTester?: { + /** + * Flag to enable or disable the tester + * Default is enabled + * + * @visibility frontend + */ + enabled: boolean; + }; +} diff --git a/packages/app-next/e2e-tests/app.test.ts b/packages/app-next/e2e-tests/app.test.ts new file mode 100644 index 000000000..f0124e6c0 --- /dev/null +++ b/packages/app-next/e2e-tests/app.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2023 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 { test, expect } from '@playwright/test'; + +test('App should render the welcome page', async ({ page }) => { + await page.goto('/'); + await expect(page.getByText('My Company Catalog')).toBeVisible(); +}); diff --git a/packages/app-next/package.json b/packages/app-next/package.json new file mode 100644 index 000000000..7c932bab9 --- /dev/null +++ b/packages/app-next/package.json @@ -0,0 +1,84 @@ +{ + "name": "app-next", + "version": "0.0.0", + "private": true, + "backstage": { + "role": "frontend" + }, + "bundled": true, + "dependencies": { + "@backstage-community/plugin-badges": "^0.9.0", + "@backstage-community/plugin-cost-insights": "^0.15.1", + "@backstage-community/plugin-explore": "^0.9.0", + "@backstage-community/plugin-github-actions": "^0.11.0", + "@backstage-community/plugin-graphiql": "^0.4.1", + "@backstage-community/plugin-tech-radar": "^1.6.0", + "@backstage-community/plugin-todo": "^0.9.0", + "@backstage/canon": "backstage:^", + "@backstage/cli": "backstage:^", + "@backstage/core-app-api": "backstage:^", + "@backstage/core-compat-api": "backstage:^", + "@backstage/core-components": "backstage:^", + "@backstage/core-plugin-api": "backstage:^", + "@backstage/frontend-defaults": "backstage:^", + "@backstage/frontend-plugin-api": "backstage:^", + "@backstage/integration-react": "backstage:^", + "@backstage/plugin-api-docs": "backstage:^", + "@backstage/plugin-catalog": "backstage:^", + "@backstage/plugin-catalog-graph": "backstage:^", + "@backstage/plugin-catalog-react": "backstage:^", + "@backstage/plugin-home": "backstage:^", + "@backstage/plugin-kubernetes": "backstage:^", + "@backstage/plugin-notifications": "backstage:^", + "@backstage/plugin-org": "backstage:^", + "@backstage/plugin-scaffolder": "backstage:^", + "@backstage/plugin-search": "backstage:^", + "@backstage/plugin-search-react": "backstage:^", + "@backstage/plugin-signals": "backstage:^", + "@backstage/plugin-techdocs": "backstage:^", + "@backstage/plugin-techdocs-module-addons-contrib": "backstage:^", + "@backstage/plugin-techdocs-react": "backstage:^", + "@backstage/plugin-user-settings": "backstage:^", + "@backstage/theme": "backstage:^", + "@material-ui/core": "^4.11.0", + "@material-ui/icons": "^4.9.1", + "backstage-plugin-techdocs-addon-mermaid": "^0.21.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router": "^6.3.0", + "react-router-dom": "^6.3.0" + }, + "devDependencies": { + "@playwright/test": "^1.32.3", + "@testing-library/dom": "^10.1.0", + "@testing-library/jest-dom": "^6.0.0", + "@testing-library/react": "^16.0.0", + "@types/d3": "^7.4.3", + "@types/node": "^22.0.0", + "@types/react-dom": "*" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "test": "backstage-cli package test", + "lint": "backstage-cli package lint", + "clean": "backstage-cli package clean" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "files": [ + "dist", + "config.d.ts" + ], + "configSchema": "config.d.ts" +} diff --git a/packages/app-next/public/android-chrome-192x192.png b/packages/app-next/public/android-chrome-192x192.png new file mode 100644 index 000000000..eec0ae25b Binary files /dev/null and b/packages/app-next/public/android-chrome-192x192.png differ diff --git a/packages/app-next/public/apple-touch-icon.png b/packages/app-next/public/apple-touch-icon.png new file mode 100644 index 000000000..3158830ac Binary files /dev/null and b/packages/app-next/public/apple-touch-icon.png differ diff --git a/packages/app-next/public/favicon-16x16.png b/packages/app-next/public/favicon-16x16.png new file mode 100644 index 000000000..58cf61a35 Binary files /dev/null and b/packages/app-next/public/favicon-16x16.png differ diff --git a/packages/app-next/public/favicon-32x32.png b/packages/app-next/public/favicon-32x32.png new file mode 100644 index 000000000..c0915ece7 Binary files /dev/null and b/packages/app-next/public/favicon-32x32.png differ diff --git a/packages/app-next/public/favicon.ico b/packages/app-next/public/favicon.ico new file mode 100644 index 000000000..5e45e5dfb Binary files /dev/null and b/packages/app-next/public/favicon.ico differ diff --git a/packages/app-next/public/google-cloud.png b/packages/app-next/public/google-cloud.png new file mode 100644 index 000000000..09b3c6ea4 Binary files /dev/null and b/packages/app-next/public/google-cloud.png differ diff --git a/packages/app-next/public/graphiql.png b/packages/app-next/public/graphiql.png new file mode 100644 index 000000000..3a2b884c3 Binary files /dev/null and b/packages/app-next/public/graphiql.png differ diff --git a/packages/app-next/public/index.html b/packages/app-next/public/index.html new file mode 100644 index 000000000..76b78b0a3 --- /dev/null +++ b/packages/app-next/public/index.html @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + <%= config.getString('app.title') %> + + + +
+ + + diff --git a/packages/app-next/public/manifest.json b/packages/app-next/public/manifest.json new file mode 100644 index 000000000..4a7c1b4ec --- /dev/null +++ b/packages/app-next/public/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "Backstage", + "name": "Backstage", + "icons": [ + { + "src": "favicon.ico", + "sizes": "48x48", + "type": "image/png" + } + ], + "start_url": "./index.html", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/packages/app-next/public/robots.txt b/packages/app-next/public/robots.txt new file mode 100644 index 000000000..b21f0887a --- /dev/null +++ b/packages/app-next/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: / diff --git a/packages/app-next/public/safari-pinned-tab.svg b/packages/app-next/public/safari-pinned-tab.svg new file mode 100644 index 000000000..0f500b300 --- /dev/null +++ b/packages/app-next/public/safari-pinned-tab.svg @@ -0,0 +1 @@ +Created by potrace 1.11, written by Peter Selinger 2001-2013 \ No newline at end of file diff --git a/packages/app-next/public/tech-radar.png b/packages/app-next/public/tech-radar.png new file mode 100644 index 000000000..a68b502fd Binary files /dev/null and b/packages/app-next/public/tech-radar.png differ diff --git a/packages/app-next/src/App.test.tsx b/packages/app-next/src/App.test.tsx new file mode 100644 index 000000000..639036375 --- /dev/null +++ b/packages/app-next/src/App.test.tsx @@ -0,0 +1,27 @@ +import { render, waitFor } from '@testing-library/react'; +import app from './App'; + +describe('App', () => { + it('should render', async () => { + process.env = { + NODE_ENV: 'test', + APP_CONFIG: [ + { + data: { + app: { + title: 'Test', + support: { url: 'http://localhost:7007/support' }, + }, + backend: { baseUrl: 'http://localhost:7007' }, + }, + context: 'test', + }, + ] as any, + }; + + const rendered = render(app); + await waitFor(() => { + expect(rendered.baseElement).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx new file mode 100644 index 000000000..45d690557 --- /dev/null +++ b/packages/app-next/src/App.tsx @@ -0,0 +1,256 @@ +import { badgesPlugin } from './plugins'; + +import { ProxiedSignInPage } from '@backstage/core-components'; +import { FlatRoutes } from '@backstage/core-app-api'; +import { CatalogIndexPage, catalogPlugin } from '@backstage/plugin-catalog'; +import { default as alphaCatalogPlugin } from '@backstage/plugin-catalog/alpha'; +import { catalogGraphPlugin } from '@backstage/plugin-catalog-graph'; +import { + CostInsightsLabelDataflowInstructionsPage, + CostInsightsPage, + CostInsightsProjectGrowthInstructionsPage, +} from '@backstage-community/plugin-cost-insights'; +import { ExplorePage } from '@backstage-community/plugin-explore'; +import { Navigate, Route } from 'react-router'; +import { + TechDocsIndexPage, + TechDocsReaderPage, + techdocsPlugin, +} from '@backstage/plugin-techdocs'; +import { UnifiedThemeProvider, themes } from '@backstage/theme'; + +import { GraphiQLPage } from '@backstage-community/plugin-graphiql'; + +import { + SettingsLayout, + UserSettingsPage, +} from '@backstage/plugin-user-settings'; +import { apertureTheme } from './theme/aperture'; +import { apis } from './apis'; + +import { orgPlugin } from '@backstage/plugin-org'; + +import { CssBaseline } from '@material-ui/core'; +import { HomepageCompositionRoot, VisitListener } from '@backstage/plugin-home'; +import { CustomizableHomePage } from './components/home/CustomizableHomePage'; +import { scaffolderPlugin } from '@backstage/plugin-scaffolder'; +import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; +import { + ExpandableNavigation, + LightBox, + ReportIssue, + TextSize, +} from '@backstage/plugin-techdocs-module-addons-contrib'; +import { Mermaid } from 'backstage-plugin-techdocs-addon-mermaid'; +import { SignalsDisplay } from '@backstage/plugin-signals'; +import { NotificationSettings } from './components/settings/NotificationSettings'; + +// New Frontend System Imports +import { createApp } from '@backstage/frontend-defaults'; +import { + compatWrapper, + convertLegacyApp, + convertLegacyAppOptions, + convertLegacyRouteRef, + convertLegacyRouteRefs, +} from '@backstage/core-compat-api'; +import { + AppRootElementBlueprint, + createFrontendModule, + SignInPageBlueprint, + ThemeBlueprint, +} from '@backstage/frontend-plugin-api'; +import { rootNav } from './components/Root'; +import { + EntityKindPicker, + EntityTypePicker, + UserListPicker, + EntityOwnerPicker, + EntityLifecyclePicker, + EntityTagPicker, + EntityProcessingStatusPicker, + EntityNamespacePicker, +} from '@backstage/plugin-catalog-react'; + +const routes = ( + + } /> + }> + + + } /> + } + /> + } + /> + } /> + } + > + + + + + + + + + + } /> + } /> + + }> + + + + + +); + +const legacyFeatures = convertLegacyApp(routes); + +const signalsDisplayExtension = AppRootElementBlueprint.make({ + name: 'signals-display-extension', + params: { + element: compatWrapper(), + }, +}); + +const visitListenerExtension = AppRootElementBlueprint.make({ + name: 'visit-listener-extension', + params: { + element: , + }, +}); + +const optionsModule = convertLegacyAppOptions({ + // TODO:(awanlin) the badges plugin doesn't support the new frontend system yet + plugins: [badgesPlugin], +}); + +const proxiedSignInPage = SignInPageBlueprint.make({ + params: { + loader: async () => props => ( + + ), + }, +}); + +const lightThemeExtension = ThemeBlueprint.make({ + name: 'light', + params: { + theme: { + id: 'light', + title: 'Light', + variant: 'light', + Provider: ({ children }) => ( + + ), + }, + }, +}); + +const darkThemeExtension = ThemeBlueprint.make({ + name: 'dark', + params: { + theme: { + id: 'dark', + title: 'Dark', + variant: 'dark', + Provider: ({ children }) => ( + + ), + }, + }, +}); + +const apertureThemeExtension = ThemeBlueprint.make({ + name: 'aperture', + params: { + theme: { + id: 'aperture', + title: 'Aperture', + variant: 'light', + Provider: ({ children }) => ( + + + {children} + + ), + }, + }, +}); + +const catalogPluginOverride = alphaCatalogPlugin.withOverrides({ + extensions: [ + alphaCatalogPlugin.getExtension('page:catalog').override({ + params: { + loader: async () => + compatWrapper( + + + + + + + + + + + } + />, + ), + }, + }), + ], +}); + +const app = createApp({ + features: [ + optionsModule, + ...legacyFeatures, + createFrontendModule({ + pluginId: 'app', + extensions: [ + ...apis, + proxiedSignInPage, + lightThemeExtension, + darkThemeExtension, + apertureThemeExtension, + rootNav, + signalsDisplayExtension, + visitListenerExtension, + ], + }), + catalogPluginOverride, + ], + bindRoutes({ bind }) { + bind(convertLegacyRouteRefs(catalogPlugin.externalRoutes), { + createComponent: convertLegacyRouteRef(scaffolderPlugin.routes.root), + viewTechDoc: convertLegacyRouteRef(techdocsPlugin.routes.docRoot), + createFromTemplate: convertLegacyRouteRef( + scaffolderPlugin.routes.selectedTemplate, + ), + }); + bind(convertLegacyRouteRefs(scaffolderPlugin.externalRoutes), { + viewTechDoc: convertLegacyRouteRef(techdocsPlugin.routes.docRoot), + }); + bind(convertLegacyRouteRefs(catalogGraphPlugin.externalRoutes), { + catalogEntity: convertLegacyRouteRef(catalogPlugin.routes.catalogEntity), + }); + bind(convertLegacyRouteRefs(orgPlugin.externalRoutes), { + catalogIndex: convertLegacyRouteRef(catalogPlugin.routes.catalogIndex), + }); + }, +}); + +export default app.createRoot(); diff --git a/packages/app-next/src/apis.ts b/packages/app-next/src/apis.ts new file mode 100644 index 000000000..853d671d7 --- /dev/null +++ b/packages/app-next/src/apis.ts @@ -0,0 +1,140 @@ +import { + graphQlBrowseApiRef, + GraphQLEndpoints, +} from '@backstage-community/plugin-graphiql'; +import { + costInsightsApiRef, + ExampleCostInsightsClient, +} from '@backstage-community/plugin-cost-insights'; +import { + ScmAuth, + ScmIntegrationsApi, + scmIntegrationsApiRef, +} from '@backstage/integration-react'; + +import { + createApiFactory, + githubAuthApiRef, + discoveryApiRef, + oauthRequestApiRef, + errorApiRef, + configApiRef, + identityApiRef, +} from '@backstage/core-plugin-api'; + +import { GithubAuth } from '@backstage/core-app-api'; +import { visitsApiRef, VisitsWebStorageApi } from '@backstage/plugin-home'; + +// New Frontend System imports +import { ApiBlueprint } from '@backstage/frontend-plugin-api'; + +const scmIntegrationsApi = ApiBlueprint.make({ + name: 'scm-integrations', + params: { + factory: createApiFactory({ + api: scmIntegrationsApiRef, + deps: { configApi: configApiRef }, + factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi), + }), + }, +}); + +const scmAuthApi = ApiBlueprint.make({ + name: 'scm-auth', + params: { + factory: ScmAuth.createDefaultApiFactory(), + }, +}); + +const githubAuthApi = ApiBlueprint.make({ + name: 'github-auth', + params: { + factory: createApiFactory({ + api: githubAuthApiRef, + deps: { + configApi: configApiRef, + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + GithubAuth.create({ + discoveryApi, + oauthRequestApi, + defaultScopes: ['read:user'], + environment: configApi.getString('auth.environment'), + }), + }), + }, +}); + +const graphQlBrowseApi = ApiBlueprint.make({ + name: 'graphql-browse', + params: { + factory: createApiFactory({ + api: graphQlBrowseApiRef, + deps: { + errorApi: errorApiRef, + graphGithubAuthApi: githubAuthApiRef, + discoveryApi: discoveryApiRef, + }, + factory: ({ errorApi, graphGithubAuthApi, discoveryApi }) => + GraphQLEndpoints.from([ + GraphQLEndpoints.create({ + id: 'backstage', + title: 'GraphQL Backend', + url: discoveryApi.getBaseUrl('graphql'), + }), + GraphQLEndpoints.github({ + id: 'github', + title: 'GitHub', + errorApi, + githubAuthApi: graphGithubAuthApi, + }), + GraphQLEndpoints.create({ + id: 'gitlab', + title: 'GitLab', + url: 'https://gitlab.com/api/graphql', + }), + GraphQLEndpoints.create({ + id: 'swapi', + title: 'SWAPI', + url: 'https://swapi-graphql.netlify.app/.netlify/functions/index', + }), + ]), + }), + }, +}); + +const costInsightsApi = ApiBlueprint.make({ + name: 'cost-insights', + params: { + factory: createApiFactory( + costInsightsApiRef, + new ExampleCostInsightsClient(), + ), + }, +}); + +const visitsApi = ApiBlueprint.make({ + name: 'visits', + params: { + factory: createApiFactory({ + api: visitsApiRef, + deps: { + identityApi: identityApiRef, + errorApi: errorApiRef, + }, + factory: ({ identityApi, errorApi }) => + VisitsWebStorageApi.create({ identityApi, errorApi }), + }), + }, +}); + +export const apis = [ + scmIntegrationsApi, + scmAuthApi, + githubAuthApi, + graphQlBrowseApi, + costInsightsApi, + visitsApi, +]; diff --git a/packages/app-next/src/components/Root/ApertureLogoFull.tsx b/packages/app-next/src/components/Root/ApertureLogoFull.tsx new file mode 100644 index 000000000..eb09a575f --- /dev/null +++ b/packages/app-next/src/components/Root/ApertureLogoFull.tsx @@ -0,0 +1,79 @@ +import { makeStyles } from '@material-ui/core'; + +const useStyles = makeStyles({ + svg: { + fill: '#0099ff', + width: 'auto', + height: 40, + }, +}); + +export const ApertureLogoFull = () => { + const classes = useStyles(); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/app-next/src/components/Root/ApertureLogoIcon.tsx b/packages/app-next/src/components/Root/ApertureLogoIcon.tsx new file mode 100644 index 000000000..f91114847 --- /dev/null +++ b/packages/app-next/src/components/Root/ApertureLogoIcon.tsx @@ -0,0 +1,40 @@ +import { makeStyles } from '@material-ui/core'; + +const useStyles = makeStyles({ + svg: { + fill: '#0099ff', + width: 'auto', + height: 40, + }, +}); + +export const ApertureLogoIcon = () => { + const classes = useStyles(); + + return ( + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/app-next/src/components/Root/LogoFull.tsx b/packages/app-next/src/components/Root/LogoFull.tsx new file mode 100644 index 000000000..4508fc450 --- /dev/null +++ b/packages/app-next/src/components/Root/LogoFull.tsx @@ -0,0 +1,45 @@ +/* + * 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 { makeStyles } from '@material-ui/core'; + +const useStyles = makeStyles({ + svg: { + width: 'auto', + height: 30, + }, + path: { + fill: '#7df3e1', + }, +}); +const LogoFull = () => { + const classes = useStyles(); + + return ( + + + + ); +}; + +export default LogoFull; diff --git a/packages/app-next/src/components/Root/LogoIcon.tsx b/packages/app-next/src/components/Root/LogoIcon.tsx new file mode 100644 index 000000000..02455eb6d --- /dev/null +++ b/packages/app-next/src/components/Root/LogoIcon.tsx @@ -0,0 +1,46 @@ +/* + * 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 { makeStyles } from '@material-ui/core'; + +const useStyles = makeStyles({ + svg: { + width: 'auto', + height: 28, + }, + path: { + fill: '#7df3e1', + }, +}); + +const LogoIcon = () => { + const classes = useStyles(); + + return ( + + + + ); +}; + +export default LogoIcon; diff --git a/packages/app-next/src/components/Root/Root.tsx b/packages/app-next/src/components/Root/Root.tsx new file mode 100644 index 000000000..f1852ecb8 --- /dev/null +++ b/packages/app-next/src/components/Root/Root.tsx @@ -0,0 +1,136 @@ +import { Link, Theme, makeStyles } from '@material-ui/core'; +import HomeIcon from '@material-ui/icons/Home'; +import ExtensionIcon from '@material-ui/icons/Extension'; +import MapIcon from '@material-ui/icons/MyLocation'; +import LayersIcon from '@material-ui/icons/Layers'; +import LibraryBooks from '@material-ui/icons/LibraryBooks'; +import CreateComponentIcon from '@material-ui/icons/AddCircleOutline'; +import MoneyIcon from '@material-ui/icons/MonetizationOn'; +import LogoFull from './LogoFull'; +import LogoIcon from './LogoIcon'; +import { NavLink } from 'react-router-dom'; +import { GraphiQLIcon } from '@backstage-community/plugin-graphiql'; +import { + Settings as SidebarSettings, + UserSettingsSignInAvatar, +} from '@backstage/plugin-user-settings'; +import { SidebarSearchModal } from '@backstage/plugin-search'; +import { + Sidebar, + sidebarConfig, + SidebarItem, + SidebarDivider, + SidebarSpace, + SidebarGroup, + useSidebarOpenState, +} from '@backstage/core-components'; +import MenuIcon from '@material-ui/icons/Menu'; +import SearchIcon from '@material-ui/icons/Search'; +import { appThemeApiRef, useApi } from '@backstage/core-plugin-api'; +import { ApertureLogoFull } from './ApertureLogoFull'; +import { ApertureLogoIcon } from './ApertureLogoIcon'; +import CategoryIcon from '@material-ui/icons/Category'; +import { MyGroupsSidebarItem } from '@backstage/plugin-org'; +import GroupIcon from '@material-ui/icons/People'; +import { NotificationsSidebarItem } from '@backstage/plugin-notifications'; +import { compatWrapper } from '@backstage/core-compat-api'; +import { + coreExtensionData, + createExtension, +} from '@backstage/frontend-plugin-api'; + +const useSidebarLogoStyles = makeStyles({ + root: { + width: sidebarConfig.drawerWidthClosed, + height: 3 * sidebarConfig.logoHeight, + display: 'flex', + flexFlow: 'row nowrap', + alignItems: 'center', + marginBottom: -14, + }, + link: props => ({ + width: sidebarConfig.drawerWidthClosed, + marginLeft: props.themeId === 'aperture' ? 15 : 24, + }), +}); + +const SidebarLogo = () => { + const { isOpen } = useSidebarOpenState(); + + const appThemeApi = useApi(appThemeApiRef); + const themeId = appThemeApi.getActiveThemeId(); + const classes = useSidebarLogoStyles({ themeId: themeId! }); + + const fullLogo = themeId === 'aperture' ? : ; + const iconLogo = themeId === 'aperture' ? : ; + + return ( +
+ + {isOpen ? fullLogo : iconLogo} + +
+ ); +}; + +export const rootNav = createExtension({ + name: 'nav', + attachTo: { id: 'app/layout', input: 'nav' }, + output: [coreExtensionData.reactElement], + factory() { + return [ + coreExtensionData.reactElement( + compatWrapper( + + + } to="/search"> + + + + }> + + + + + + + + + + + + + + + + + } + to="/settings" + > + + + , + ), + ), + ]; + }, +}); diff --git a/packages/app-next/src/components/Root/index.ts b/packages/app-next/src/components/Root/index.ts new file mode 100644 index 000000000..81685adca --- /dev/null +++ b/packages/app-next/src/components/Root/index.ts @@ -0,0 +1 @@ +export { rootNav } from './Root'; diff --git a/packages/app-next/src/components/catalog/EntityPage.tsx b/packages/app-next/src/components/catalog/EntityPage.tsx new file mode 100644 index 000000000..1258a24d8 --- /dev/null +++ b/packages/app-next/src/components/catalog/EntityPage.tsx @@ -0,0 +1,441 @@ +/* + * 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 { useMemo, useState } from 'react'; +import { Button, Grid } from '@material-ui/core'; +import BadgeIcon from '@material-ui/icons/CallToAction'; +import { + EntityApiDefinitionCard, + EntityConsumedApisCard, + EntityConsumingComponentsCard, + EntityHasApisCard, + EntityProvidedApisCard, + EntityProvidingComponentsCard, +} from '@backstage/plugin-api-docs'; +import { EntityBadgesDialog } from '@backstage-community/plugin-badges'; +import { + EntityAboutCard, + EntityDependsOnComponentsCard, + EntityDependsOnResourcesCard, + EntityHasComponentsCard, + EntityHasResourcesCard, + EntityHasSubcomponentsCard, + EntityHasSystemsCard, + EntityLayout, + EntityLinksCard, + EntityOrphanWarning, + EntityProcessingErrorsPanel, + EntityRelationWarning, + EntitySwitch, + hasCatalogProcessingErrors, + hasRelationWarnings, + isComponentType, + isKind, + isOrphan, +} from '@backstage/plugin-catalog'; +import { + EntityGithubActionsContent, + EntityRecentGithubActionsRunsCard, + isGithubActionsAvailable, +} from '@backstage-community/plugin-github-actions'; +import { + EntityGroupProfileCard, + EntityMembersListCard, + EntityOwnershipCard, + EntityUserProfileCard, +} from '@backstage/plugin-org'; +import { EntityTechdocsContent } from '@backstage/plugin-techdocs'; +import { EntityTodoContent } from '@backstage-community/plugin-todo'; +import { EmptyState } from '@backstage/core-components'; +import { EntityCatalogGraphCard } from '@backstage/plugin-catalog-graph'; +import { + EntityKubernetesContent, + isKubernetesAvailable, +} from '@backstage/plugin-kubernetes'; +import { + ExpandableNavigation, + LightBox, + ReportIssue, + TextSize, +} from '@backstage/plugin-techdocs-module-addons-contrib'; +import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; +import { Mermaid } from 'backstage-plugin-techdocs-addon-mermaid'; + +const EntityLayoutWrapper = (props: { children?: React.ReactNode }) => { + const [badgesDialogOpen, setBadgesDialogOpen] = useState(false); + + const extraMenuItems = useMemo(() => { + return [ + { + title: 'Badges', + Icon: BadgeIcon, + onClick: () => setBadgesDialogOpen(true), + }, + ]; + }, []); + + return ( + <> + + {props.children} + + setBadgesDialogOpen(false)} + /> + + ); +}; + +const entityWarningContent = ( + <> + + + + + + + + + + + + + + + + + + + + + + + + +); + +const cicdContent = ( + + + + + + + + Read more + + } + /> + + +); + +const cicdCard = ( + + + + + + + +); + +const techdocsContentWithAddons = ( + + + + + + + + + +); + +const overviewContent = ( + + {entityWarningContent} + + + + + + + + + + + + + {cicdCard} + + + + + +); + +const serviceEntityPage = ( + + + {overviewContent} + + + + + + + + {cicdContent} + + + + + + + + + + + + + + + + + + + + + + + + + + {techdocsContentWithAddons} + + + + + + +); + +const websiteEntityPage = ( + + + {overviewContent} + + + + + + + + {cicdContent} + + + + + + + + + + + + + + + {techdocsContentWithAddons} + + + + + + +); + +const defaultEntityPage = ( + + + {overviewContent} + + + + {techdocsContentWithAddons} + + + + + + +); + +const componentPage = ( + + + {serviceEntityPage} + + + + {websiteEntityPage} + + + {defaultEntityPage} + +); + +const apiPage = ( + + + + {entityWarningContent} + + + + + + + + + + + + + + + + + + + + + + + + + +); + +const userPage = ( + + + + {entityWarningContent} + + + + + + + + + +); + +const groupPage = ( + + + + {entityWarningContent} + + + + + + + + + + + + + + + +); + +const systemPage = ( + + + + {entityWarningContent} + + + + + + + + + + + + + + + + + + +); + +const domainPage = ( + + + + {entityWarningContent} + + + + + + + + + +); + +export const entityPage = ( + + + + + + + + + {defaultEntityPage} + +); diff --git a/packages/app-next/src/components/home/CustomizableHomePage.tsx b/packages/app-next/src/components/home/CustomizableHomePage.tsx new file mode 100644 index 000000000..122c07f08 --- /dev/null +++ b/packages/app-next/src/components/home/CustomizableHomePage.tsx @@ -0,0 +1,79 @@ +import { Page, Content } from '@backstage/core-components'; +import { + HomePageCompanyLogo, + TemplateBackstageLogo, + HomePageStarredEntities, + HomePageToolkit, + CustomHomepageGrid, + HomePageRandomJoke, + HomePageTopVisited, + HomePageRecentlyVisited, +} from '@backstage/plugin-home'; +import { HomePageSearchBar } from '@backstage/plugin-search'; +import { Grid } from '@material-ui/core'; + +import { tools, useLogoStyles } from './shared'; + +const defaultConfig = [ + { + component: 'HomePageSearchBar', + x: 0, + y: 0, + width: 24, + height: 2, + }, + { + component: 'HomePageRecentlyVisited', + x: 0, + y: 1, + width: 5, + height: 4, + }, + { + component: 'HomePageTopVisited', + x: 5, + y: 1, + width: 5, + height: 4, + }, + { + component: 'HomePageStarredEntities', + x: 0, + y: 2, + width: 6, + height: 4, + }, + { + component: 'HomePageToolkit', + x: 6, + y: 6, + width: 4, + height: 4, + }, +]; + +export const CustomizableHomePage = () => { + const { svg, path, container } = useLogoStyles(); + + return ( + + + + } + /> + + + + + + + + + + + + + ); +}; diff --git a/packages/app-next/src/components/home/shared.tsx b/packages/app-next/src/components/home/shared.tsx new file mode 100644 index 000000000..f182c939b --- /dev/null +++ b/packages/app-next/src/components/home/shared.tsx @@ -0,0 +1,43 @@ +import { TemplateBackstageLogoIcon } from '@backstage/plugin-home'; +import { makeStyles } from '@material-ui/core/styles'; + +export const useLogoStyles = makeStyles(theme => ({ + container: { + margin: theme.spacing(5, 0), + }, + svg: { + width: 'auto', + height: 100, + }, + path: { + fill: '#7df3e1', + }, +})); + +export const tools = [ + { + url: 'https://backstage.io/docs', + label: 'Docs', + icon: , + }, + { + url: 'https://github.com/backstage/backstage', + label: 'GitHub', + icon: , + }, + { + url: 'https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md', + label: 'Contributing', + icon: , + }, + { + url: 'https://backstage.io/plugins', + label: 'Plugins Directory', + icon: , + }, + { + url: 'https://github.com/backstage/backstage/issues/new/choose', + label: 'Submit New Issue', + icon: , + }, +]; diff --git a/packages/app-next/src/components/search/SearchPage.tsx b/packages/app-next/src/components/search/SearchPage.tsx new file mode 100644 index 000000000..f4aa8cf48 --- /dev/null +++ b/packages/app-next/src/components/search/SearchPage.tsx @@ -0,0 +1,169 @@ +import { + CatalogIcon, + Content, + DocsIcon, + Header, + Page, +} from '@backstage/core-components'; +import { CatalogSearchResultListItem } from '@backstage/plugin-catalog'; +import { SearchType } from '@backstage/plugin-search'; +import { + DefaultResultListItem, + SearchBar, + SearchFilter, + SearchResult, + SearchResultPager, + useSearch, +} from '@backstage/plugin-search-react'; +import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs'; +import { Grid, List, makeStyles, Paper, Theme } from '@material-ui/core'; + +import { ToolSearchResultListItem } from '@backstage-community/plugin-explore'; +import { useApi } from '@backstage/core-plugin-api'; +import { + catalogApiRef, + CATALOG_FILTER_EXISTS, +} from '@backstage/plugin-catalog-react'; +import BuildIcon from '@material-ui/icons/Build'; + +const useStyles = makeStyles((theme: Theme) => ({ + bar: { + padding: theme.spacing(1, 0), + }, + filters: { + padding: theme.spacing(2), + marginTop: theme.spacing(2), + }, + filter: { + '& + &': { + marginTop: theme.spacing(2.5), + }, + }, +})); + +const SearchPage = () => { + const classes = useStyles(); + const { types } = useSearch(); + const catalogApi = useApi(catalogApiRef); + return ( + +
+ + + + + + + , + }, + { + value: 'techdocs', + name: 'Documentation', + icon: , + }, + { + value: 'tools', + name: 'Tools', + icon: , + }, + ]} + /> + + {types.includes('techdocs') && ( + { + // Return a list of entities which are documented. + const { items } = await catalogApi.getEntities({ + fields: ['metadata.name'], + filter: { + 'metadata.annotations.backstage.io/techdocs-ref': + CATALOG_FILTER_EXISTS, + }, + }); + + const names = items.map(entity => entity.metadata.name); + names.sort(); + return names; + }} + /> + )} + + + + + + + {({ results }) => ( + + {results.map(({ type, document, highlight, rank }) => { + switch (type) { + case 'software-catalog': + return ( + + ); + case 'techdocs': + return ( + + ); + case 'tools': + return ( + + ); + default: + return ( + + ); + } + })} + + )} + + + + + + + ); +}; + +export const searchPage = ; diff --git a/packages/app-next/src/components/settings/NotificationSettings.tsx b/packages/app-next/src/components/settings/NotificationSettings.tsx new file mode 100644 index 000000000..b240893da --- /dev/null +++ b/packages/app-next/src/components/settings/NotificationSettings.tsx @@ -0,0 +1,66 @@ +import { Box, Button, Grid, Typography } from '@material-ui/core'; +import { + configApiRef, + discoveryApiRef, + errorApiRef, + fetchApiRef, + useApi, +} from '@backstage/core-plugin-api'; + +import { InfoCard } from '@backstage/core-components'; + +import { UserNotificationSettingsCard } from '@backstage/plugin-notifications'; + +export const NotificationSettings = () => { + const config = useApi(configApiRef); + const fetchApi = useApi(fetchApiRef); + const discovery = useApi(discoveryApiRef); + const errorApi = useApi(errorApiRef); + + const isEnabled = + config.getOptionalBoolean('notificationsTester.enabled') ?? true; + + const handleNotifyClick = async () => { + const notificationTesterUrl = await discovery.getBaseUrl( + 'notifications-tester', + ); + const response = await fetchApi.fetch(`${notificationTesterUrl}/test`, { + method: 'POST', + }); + if (!response.ok) { + errorApi.post( + new Error(`Failed to send notification: ${response.status}`), + ); + } + }; + + return ( + + + + + {isEnabled && ( + + + + + + Note: this card is not part of the default Notifications Setting + and was added to be able to try out the Notification system for + this Demo site. + + + + + )} + + ); +}; diff --git a/packages/app-next/src/index.tsx b/packages/app-next/src/index.tsx new file mode 100644 index 000000000..2e4bb6bd6 --- /dev/null +++ b/packages/app-next/src/index.tsx @@ -0,0 +1,5 @@ +import ReactDOM from 'react-dom/client'; +import app from './App'; +import '@backstage/canon/css/styles.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render(app); diff --git a/packages/app-next/src/plugins.ts b/packages/app-next/src/plugins.ts new file mode 100644 index 000000000..552c32a1f --- /dev/null +++ b/packages/app-next/src/plugins.ts @@ -0,0 +1 @@ +export { badgesPlugin } from '@backstage-community/plugin-badges'; diff --git a/packages/app-next/src/setupTests.ts b/packages/app-next/src/setupTests.ts new file mode 100644 index 000000000..7b0828bfa --- /dev/null +++ b/packages/app-next/src/setupTests.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom'; diff --git a/packages/app-next/src/theme/aperture.ts b/packages/app-next/src/theme/aperture.ts new file mode 100644 index 000000000..95304b5f7 --- /dev/null +++ b/packages/app-next/src/theme/aperture.ts @@ -0,0 +1,295 @@ +import { + createBaseThemeOptions, + pageTheme as defaultPageThemes, + PageTheme, + palettes, + createUnifiedTheme, +} from '@backstage/theme'; + +import { alpha } from '@material-ui/core/styles'; + +const pageThemesFontColorOverride: Record = {}; +Object.keys(defaultPageThemes).map(key => { + pageThemesFontColorOverride[key] = { + ...defaultPageThemes[key], + fontColor: '#172B4D', + }; +}); + +export const apertureTheme = createUnifiedTheme({ + ...createBaseThemeOptions({ + palette: { + ...palettes.light, + primary: { + main: '#0052CC', + light: '#4C9AFF', + dark: '#172B4D', + }, + secondary: { + main: '#FF5630', + light: '#FFAB00', + dark: '#6554C0', + }, + grey: { + 50: '#C1C7D0', + 100: '#7A869A', + 200: '#6B778C', + 300: '#5E6C84', + 400: '#505F79', + 500: '#42526E', + 600: '#344563', + 700: '#253858', + 800: '#172B4D', + 900: '#091E42', + }, + error: { + main: '#FF5630', + light: '#FF8F73', + dark: '#DE350B', + }, + warning: { + main: '#FFAB00', + light: '#FFE380', + dark: '#FF8B00', + }, + success: { + main: '#36B37E', + light: '#79F2C0', + dark: '#006644', + }, + info: { + main: '#0065FF', + light: '#4C9AFF', + dark: '#0747A6', + }, + navigation: { + ...palettes.light.navigation, + background: '#172B4D', + color: '#FFFFFF', + indicator: '#2684FF', + navItem: { + hoverBackground: 'rgba(116,118,121,0.6)', + }, + }, + text: { + primary: '#172B48', + }, + background: { + default: '#FFFFFF', + }, + }, + }), + typography: { + htmlFontSize: 16, + fontFamily: 'Roboto, sans-serif', + h1: { + fontSize: 54, + fontWeight: 700, + marginBottom: 10, + }, + h2: { + fontSize: 40, + fontWeight: 700, + marginBottom: 8, + }, + h3: { + fontSize: 32, + fontWeight: 700, + marginBottom: 6, + }, + h4: { + fontWeight: 700, + fontSize: 28, + marginBottom: 6, + }, + h5: { + fontWeight: 700, + fontSize: 24, + marginBottom: 4, + }, + h6: { + fontWeight: 700, + fontSize: 20, + marginBottom: 2, + }, + }, + pageTheme: pageThemesFontColorOverride, + defaultPageTheme: 'home', + components: { + BackstageHeader: { + styleOverrides: { + header: ({ theme }) => ({ + backgroundImage: 'unset', + boxShadow: 'unset', + paddingBottom: theme.spacing(1), + }), + title: ({ theme }) => ({ + color: theme.page.fontColor, + fontWeight: 900, + }), + subtitle: ({ theme }) => ({ + color: alpha(theme.page.fontColor, 0.8), + }), + type: ({ theme }) => ({ + color: alpha(theme.page.fontColor, 0.8), + }), + }, + }, + BackstageHeaderTabs: { + styleOverrides: { + defaultTab: { + fontSize: 'inherit', + textTransform: 'none', + }, + }, + }, + BackstageOpenedDropdown: { + styleOverrides: { + icon: { + '& path': { + fill: '#FFFFFF', + }, + }, + }, + }, + BackstageTable: { + styleOverrides: { + root: { + '&> :first-child': { + borderBottom: '1px solid #D5D5D5', + boxShadow: 'none', + }, + '& th': { + borderTop: 'none', + textTransform: 'none !important', + }, + }, + }, + }, + CatalogReactUserListPicker: { + styleOverrides: { + title: { + textTransform: 'none', + }, + }, + }, + MuiAlert: { + styleOverrides: { + root: { + borderRadius: 0, + }, + standardError: ({ theme }) => ({ + color: '#FFFFFF', + backgroundColor: theme.palette.error.dark, + '& $icon': { + color: '#FFFFFF', + }, + }), + standardInfo: ({ theme }) => ({ + color: '#FFFFFF', + backgroundColor: theme.palette.primary.dark, + '& $icon': { + color: '#FFFFFF', + }, + }), + standardSuccess: ({ theme }) => ({ + color: '#FFFFFF', + backgroundColor: theme.palette.success.dark, + '& $icon': { + color: '#FFFFFF', + }, + }), + standardWarning: ({ theme }) => ({ + color: theme.palette.grey[700], + backgroundColor: theme.palette.secondary.light, + '& $icon': { + color: theme.palette.grey[700], + }, + }), + }, + }, + MuiAutocomplete: { + styleOverrides: { + root: { + '&[aria-expanded=true]': { + backgroundColor: '#26385A', + color: '#FFFFFF', + }, + '&[aria-expanded=true] path': { + fill: '#FFFFFF', + }, + }, + }, + }, + MuiBackdrop: { + styleOverrides: { + root: { + backgroundColor: 'rgba(9,30,69,0.54)', + }, + }, + }, + MuiButton: { + styleOverrides: { + root: { + borderRadius: 3, + textTransform: 'none', + }, + contained: { + boxShadow: 'none', + }, + }, + }, + MuiChip: { + styleOverrides: { + root: ({ theme }) => ({ + borderRadius: 3, + backgroundColor: theme.palette.grey[50], + color: theme.palette.primary.dark, + margin: 4, + }), + }, + }, + MuiSelect: { + styleOverrides: { + select: { + '&[aria-expanded]': { + backgroundColor: '#26385A', + color: '#FFFFFF', + }, + }, + }, + }, + MuiSwitch: { + styleOverrides: { + root: { + padding: 10, + }, + switchBase: { + padding: 12, + }, + thumb: { + backgroundColor: '#FFFFFF', + height: 14, + width: 14, + }, + track: { + borderRadius: 9, + }, + }, + }, + MuiTabs: { + styleOverrides: { + indicator: { + transition: 'none', + }, + }, + }, + MuiTypography: { + styleOverrides: { + button: { + textTransform: 'none', + }, + }, + }, + }, +}); diff --git a/yarn.lock b/yarn.lock index 502abd4a2..de5d349df 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18546,6 +18546,60 @@ __metadata: languageName: unknown linkType: soft +"app-next@workspace:packages/app-next": + version: 0.0.0-use.local + resolution: "app-next@workspace:packages/app-next" + dependencies: + "@backstage-community/plugin-badges": "npm:^0.9.0" + "@backstage-community/plugin-cost-insights": "npm:^0.15.1" + "@backstage-community/plugin-explore": "npm:^0.9.0" + "@backstage-community/plugin-github-actions": "npm:^0.11.0" + "@backstage-community/plugin-graphiql": "npm:^0.4.1" + "@backstage-community/plugin-tech-radar": "npm:^1.6.0" + "@backstage-community/plugin-todo": "npm:^0.9.0" + "@backstage/canon": "backstage:^" + "@backstage/cli": "backstage:^" + "@backstage/core-app-api": "backstage:^" + "@backstage/core-compat-api": "backstage:^" + "@backstage/core-components": "backstage:^" + "@backstage/core-plugin-api": "backstage:^" + "@backstage/frontend-defaults": "backstage:^" + "@backstage/frontend-plugin-api": "backstage:^" + "@backstage/integration-react": "backstage:^" + "@backstage/plugin-api-docs": "backstage:^" + "@backstage/plugin-catalog": "backstage:^" + "@backstage/plugin-catalog-graph": "backstage:^" + "@backstage/plugin-catalog-react": "backstage:^" + "@backstage/plugin-home": "backstage:^" + "@backstage/plugin-kubernetes": "backstage:^" + "@backstage/plugin-notifications": "backstage:^" + "@backstage/plugin-org": "backstage:^" + "@backstage/plugin-scaffolder": "backstage:^" + "@backstage/plugin-search": "backstage:^" + "@backstage/plugin-search-react": "backstage:^" + "@backstage/plugin-signals": "backstage:^" + "@backstage/plugin-techdocs": "backstage:^" + "@backstage/plugin-techdocs-module-addons-contrib": "backstage:^" + "@backstage/plugin-techdocs-react": "backstage:^" + "@backstage/plugin-user-settings": "backstage:^" + "@backstage/theme": "backstage:^" + "@material-ui/core": "npm:^4.11.0" + "@material-ui/icons": "npm:^4.9.1" + "@playwright/test": "npm:^1.32.3" + "@testing-library/dom": "npm:^10.1.0" + "@testing-library/jest-dom": "npm:^6.0.0" + "@testing-library/react": "npm:^16.0.0" + "@types/d3": "npm:^7.4.3" + "@types/node": "npm:^22.0.0" + "@types/react-dom": "npm:*" + backstage-plugin-techdocs-addon-mermaid: "npm:^0.21.0" + react: "npm:^18.2.0" + react-dom: "npm:^18.2.0" + react-router: "npm:^6.3.0" + react-router-dom: "npm:^6.3.0" + languageName: unknown + linkType: soft + "app@npm:^0.0.0, app@workspace:packages/app": version: 0.0.0-use.local resolution: "app@workspace:packages/app"