diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..9bfcf04 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,101 @@ +# AI Agent Instructions for Backstage Showcase + +## Project Overview + +This is a **Backstage developer portal** — a monorepo with a React frontend (`packages/app`) and Node.js backend (`packages/backend`), plus custom plugins in `plugins/`. It uses the new Backstage backend system with `createBackend()`. + +## Development Commands + +All commands go through **mise** (not yarn directly): + +```bash +mise run install # Install dependencies (runs yarn install) +mise run dev # Start frontend (localhost:3000) + backend (localhost:7007) +mise run lint # Lint and type-check +mise run test # Run tests +mise run build # Build Docker image +mise run up # Start with Traefik (production-like) +``` + +## Architecture + +### Monorepo Structure + +- **`packages/app`** — React frontend using MUI v5 + `@backstage/core-components` +- **`packages/backend`** — Node.js backend using `@backstage/backend-defaults` +- **`plugins/`** — Custom plugins (scoped as `@internal/`): + - `plausible/` — Analytics integration (frontend-plugin) + - `permission-backend-module-default/` — RBAC policy (backend-plugin-module) + +### Backend Plugin Registration + +Backend uses the new system. Add plugins in [packages/backend/src/index.ts](packages/backend/src/index.ts): + +```typescript +const backend = createBackend(); +backend.add(import('@backstage/plugin-catalog-backend')); +backend.add( + import('@internal/backstage-plugin-permission-backend-module-default'), +); +backend.start(); +``` + +### Frontend App Structure + +- Entry: [packages/app/src/App.tsx](packages/app/src/App.tsx) +- Routes use `FlatRoutes` with `` components +- Entity pages: [packages/app/src/components/catalog/EntityPage.tsx](packages/app/src/components/catalog/EntityPage.tsx) +- Sidebar navigation: [packages/app/src/components/Root/Root.tsx](packages/app/src/components/Root/Root.tsx) + +## Theming Pattern + +Custom themes use `createUnifiedTheme()` from `@backstage/theme`: + +- Default theme: [packages/app/src/theme/index.ts](packages/app/src/theme/index.ts) +- Brand themes: [packages/app/src/themes/brandTheme.ts](packages/app/src/themes/brandTheme.ts) + +Themes are registered in App.tsx via the `themes` array with `UnifiedThemeProvider`. + +## Configuration Files + +- `app-config.yaml` — Base config (local development) +- `app-config.local.yaml` — Local overrides (gitignored) +- `app-config.production.yaml` — Production config (uses env vars like `${APP_BASE_URL}`) + +Environment variables are loaded from `.env` via mise. + +## Permission System + +RBAC is implemented in [plugins/permission-backend-module-default/src/module.ts](plugins/permission-backend-module-default/src/module.ts): + +- Groups: `readers` (read-only), `developers` (CRUD), `admins` +- Uses `policyExtensionPoint` to register custom `PermissionPolicy` + +## Software Templates + +Templates live in `examples/templates/`. Each template has: + +- `template.yaml` — Scaffolder definition with parameters and steps +- `template/` — Skeleton files to copy + +Example: [examples/templates/github-blank-repo/template.yaml](examples/templates/github-blank-repo/template.yaml) + +## Entity Catalog + +- Sample entities: `examples/` directory (components, APIs, domains, systems) +- Entity types customized per kind in [EntityPage.tsx](packages/app/src/components/catalog/EntityPage.tsx) +- Uses `EntitySwitch.Case` with predicates like `isKind('component')` + +## Docker & Deployment + +- Multi-stage Dockerfile uses asdf for Node.js/Python +- Production runs: `node . --config ../../app-config.yaml --config ../../app-config.production.yaml` +- Traefik reverse proxy via `compose.yaml` + +## Key Conventions + +1. **Plugin naming**: `@internal/backstage-plugin-{name}` for private plugins +2. **Internal links**: Use `link:../app` in package.json for local dependencies +3. **MUI imports**: Use `@mui/material` (v5), not `@material-ui/core` (v4) +4. **Icons**: Import from `@mui/icons-material` as `IconComponent` for Sidebar +5. **Yarn workspaces**: `packages/*` and `plugins/*` diff --git a/mise.toml b/mise.toml index baf79b2..e41fc63 100644 --- a/mise.toml +++ b/mise.toml @@ -43,7 +43,7 @@ run = './scripts/lint' [tasks.test] description = "Run the test suite" -run = 'yarn test' +run = 'yarn test --watchAll=false' [tasks.build] description = "Build the Backstage Docker image" diff --git a/package.json b/package.json index 9c944de..0d589bf 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,9 @@ "resolutions": { "@types/react": "^18", "@types/react-dom": "^18", + "@backstage/app-defaults": "^1.7.3", + "@backstage/frontend-app-api": "^0.13.3", + "@backstage/core-app-api": "^1.19.3", "isolated-vm": "5.0.4" }, "prettier": "@spotify/prettier-config", diff --git a/packages/app/package.json b/packages/app/package.json index 059855d..bd0136f 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -45,12 +45,15 @@ "@emotion/styled": "^11.11.5", "@fontsource/inter": "^5.0.18", "@internal/backstage-plugin-plausible": "^0.1.0", + "@internal/backstage-theme-github": "link:../backstage-theme-github", "@mui/icons-material": "^5.15.19", "@mui/joy": "^5.0.0-beta.36", "@mui/material": "^5.15.19", "@mui/styles": "^5.18.0", "@mui/utils": "^5.15.14", "@mui/x-charts": "^8.0.0", + "@primer/octicons-react": "^19.21.1", + "@primer/react": "^38.6.2", "history": "^5.0.0", "react": "^18.0.2", "react-dom": "^18.0.2", @@ -87,4 +90,4 @@ "files": [ "dist" ] -} +} \ No newline at end of file diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 51ca1a8..33a184a 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -45,6 +45,11 @@ import { HomePage } from './components/home/HomePage'; import CssBaseline from '@mui/material/CssBaseline'; import { PlausibleAnalytics } from '@internal/backstage-plugin-plausible'; import { githubAuthApiRef, gitlabAuthApiRef } from '@backstage/core-plugin-api'; +import { + brandDarkTheme, + brandLightTheme, +} from '@internal/backstage-theme-github'; +import { PrimerDemoPage } from './components/PrimerDemoPage'; const app = createApp({ apis, @@ -89,9 +94,31 @@ const app = createApp({ ), }, themes: [ + { + id: 'brand-light', + title: 'GitHub Light', + variant: 'light', + Provider: ({ children }) => ( + + + {children} + + ), + }, + { + id: 'brand-dark', + title: 'GitHub Dark', + variant: 'dark', + Provider: ({ children }) => ( + + + {children} + + ), + }, { id: 'default', - title: 'Default', + title: 'Legacy (Original)', variant: 'light', icon: , Provider: ({ children }) => ( @@ -107,6 +134,7 @@ const app = createApp({ const routes = ( } /> + } /> + +
+
+ + +
+

+ Primer BaseStyles + ThemeProvider are scoped to this card only. +

+
+
+ + ); +} diff --git a/packages/app/src/components/PrimerDemoPage.tsx b/packages/app/src/components/PrimerDemoPage.tsx new file mode 100644 index 0000000..0ffc371 --- /dev/null +++ b/packages/app/src/components/PrimerDemoPage.tsx @@ -0,0 +1,18 @@ +import { Content, Header, InfoCard, Page } from '@backstage/core-components'; +import { PrimerDemo } from './PrimerDemo'; + +export function PrimerDemoPage() { + return ( + +
+ + + + + + + ); +} diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index b6c7f49..e6fe8d7 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -1,6 +1,5 @@ import { PropsWithChildren } from 'react'; import { styled } from '@mui/material/styles'; -import Home from '@mui/icons-material/Home'; import LogoFull from './LogoFull'; import LogoIcon from './LogoIcon'; import { @@ -9,6 +8,18 @@ import { } from '@backstage/plugin-user-settings'; import { SidebarSearchModal } from '@backstage/plugin-search'; import { NotificationsSidebarItem } from '@backstage/plugin-notifications'; +import { + HomeIcon, + SearchIcon, + ThreeBarsIcon, + RepoIcon, + PlugIcon, + BookIcon, + RocketIcon, + ChecklistIcon, + PulseIcon, + TelescopeIcon, +} from '@primer/octicons-react'; import { Sidebar, sidebarConfig, @@ -21,15 +32,6 @@ import { Link, SidebarExpandButton, } from '@backstage/core-components'; -import MenuIcon from '@mui/icons-material/Menu'; -import SearchIcon from '@mui/icons-material/Search'; -import AutoAwesomeRoundedIcon from '@mui/icons-material/AutoAwesomeRounded'; -import ExtensionRoundedIcon from '@mui/icons-material/ExtensionRounded'; -import MenuBookRoundedIcon from '@mui/icons-material/MenuBookRounded'; -import LocalLibraryRoundedIcon from '@mui/icons-material/LocalLibraryRounded'; -import TouchAppRoundedIcon from '@mui/icons-material/TouchAppRounded'; -import MonitorHeartRoundedIcon from '@mui/icons-material/MonitorHeartRounded'; -import ScoreRoundedIcon from '@mui/icons-material/ScoreRounded'; import { IconComponent } from '@backstage/core-plugin-api'; export enum LocalStorageKeys { @@ -63,6 +65,15 @@ const SidebarLogo = () => { ); }; +const HomeOcticon: IconComponent = () => ; +const RepoOcticon: IconComponent = () => ; +const PlugOcticon: IconComponent = () => ; +const BookOcticon: IconComponent = () => ; +const RocketOcticon: IconComponent = () => ; +const ChecklistOcticon: IconComponent = () => ; +const PulseOcticon: IconComponent = () => ; +const TelescopeOcticon: IconComponent = () => ; + export const Root = ({ children }: PropsWithChildren<{}>) => { if ( window.localStorage.getItem(LocalStorageKeys.SIDEBAR_PIN_STATE) === null @@ -77,30 +88,22 @@ export const Root = ({ children }: PropsWithChildren<{}>) => { - } to="/search"> + } + to="/search" + > - }> + }> {/* Global nav, not org-specific */} - - - - + + + + @@ -108,21 +111,9 @@ export const Root = ({ children }: PropsWithChildren<{}>) => { - - - + + + { return { width: '50vh', borderRadius: theme.shape.borderRadius, - backgroundColor: '#FFF', + backgroundColor: theme.palette.background.paper, }; }); const Header = styled('div')(({ theme }) => ({ display: 'flex', justifyContent: 'space-between', - backgroundColor: '#000', + backgroundColor: theme.palette.background.default, gap: '10px', padding: '20px 40px', backgroundImage: theme.page.backgroundImage, @@ -151,7 +151,7 @@ export const HomePage = () => { sx={{ display: 'flex', flexDirection: 'column', - color: '#FFF', + color: theme.palette.text.primary, }} > @@ -774,7 +774,7 @@ export const HomePage = () => { display: 'flex', flexDirection: 'column', alignItems: 'center', - borderLeft: '1px solid #e0e0e0', + borderLeft: `1px solid ${theme.palette.divider}`, }} >