Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/branding-slots.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@cobaltcore-dev/aurora": minor
---

Add branding slots, appName prop, and pageFooter slot to AuroraApp.

- `slots.logo`: consumers can now supply a custom logo component rendered in the page header, replacing the default Aurora SVG
- `slots.pageFooter`: consumers can now supply a custom footer component rendered at the bottom of the page, replacing the default empty footer
- `appName`: string prop that replaces the hardcoded "Aurora" text in the header breadcrumb and logo title
1 change: 1 addition & 0 deletions apps/dashboard/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ DEFAULT_ENDPOINT_INTERFACE="public"
PORT="4001"
# Prefix VITE_ so Vite exposes this to the browser via import.meta.env
VITE_BFF_ENDPOINT="/polaris-bff"
VITE_APP_TITLE="Aurora"
CEPH_S3_ENDPOINT="https://rgw.example.com"
# Ceph region identifier for AWS Signature V4 signing and LocationConstraint
# SAP Converged Cloud examples:
Expand Down
Binary file added apps/dashboard/src/client/assets/favicon.ico
Binary file not shown.
2 changes: 1 addition & 1 deletion apps/dashboard/src/client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/x-icon" href="/assets/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Dashboard</title>
<title>%VITE_APP_TITLE%</title>
Comment thread
taymoor89 marked this conversation as resolved.
</head>
<body>
<div id="app"></div>
Expand Down
2 changes: 1 addition & 1 deletion commitlint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const scopes = [
"gardener",
"network",
"template",
"aurora-portal",
"aurora",
"portal",
"aurora-sdk",
"signal-openstack",
Expand Down
150 changes: 73 additions & 77 deletions packages/aurora/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,39 +18,31 @@ npm install react react-dom fastify

Aurora has two entry points: `server` and `client`.

### Server (`src/server/server.ts`)
### Server

```ts
import path from "path"
import { createServer } from "@cobaltcore-dev/aurora/server"

createServer({
viteRoot: path.resolve(__dirname, "../.."), // root of your app (where dist/client lives)
identityEndpoint: process.env.IDENTITY_ENDPOINT, // OpenStack Keystone URL
bffEndpoint: process.env.BFF_ENDPOINT, // tRPC prefix, default "/polaris-bff"
defaultEndpointInterface: process.env.DEFAULT_ENDPOINT_INTERFACE, // "public" | "internal"
proxyUrl: process.env.GLOBAL_AGENT_HTTP_PROXY, // optional HTTP proxy (dev only)
cephRegion: process.env.CEPH_REGION, // Ceph/S3 region for object storage
imageMetadataExcludedProperties: process.env.IMAGE_METADATA_EXCLUDED_PROPERTIES, // comma-separated
insecureCookies: process.env.INSECURE_COOKIES === "true", // disable Secure flag locally
}).then((server) => server.listen({ host: "0.0.0.0", port: 4000 }))
const server = await createServer({
identityEndpoint: "https://keystone.example.com/v3/",
policyDir: path.resolve(__dirname, "../policies"),
})

server.listen({ host: "0.0.0.0", port: 4000 })
Comment thread
taymoor89 marked this conversation as resolved.
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.

### Client (`src/client/App.tsx`)
### Client

```tsx
import { AuroraApp } from "@cobaltcore-dev/aurora/client"
import "@cobaltcore-dev/aurora/client/style.css" // include once at your app root

export function App() {
return (
<AuroraApp
bffEndpoint="/polaris-bff" // must match the server's bffEndpoint
theme="theme-light" // "theme-light" | "theme-dark"
onThemeChange={(theme) => {
// called when user toggles — persist however you like
localStorage.setItem("theme", theme)
}}
bffEndpoint="/polaris-bff"
theme="theme-light"
onThemeChange={(theme) => localStorage.setItem("theme", theme)}
/>
)
}
Expand All @@ -60,80 +52,84 @@ export function App() {

### `createServer(config)`

| Option | Type | Default | Description |
| --------------------------------- | --------- | -------------------------- | ---------------------------------------------------------------- |
| `identityEndpoint` | `string` | — | OpenStack Keystone v3 URL (required) |
| `bffEndpoint` | `string` | `"/polaris-bff"` | URL prefix for all tRPC routes |
| `viteRoot` | `string` | `__dirname/../../` | Directory that contains `dist/client/` in production |
| `defaultEndpointInterface` | `string` | `"public"` | OpenStack service catalog interface |
| `proxyUrl` | `string` | — | HTTP proxy for OpenStack calls (dev only, ignored in production) |
| `cephRegion` | `string` | — | Ceph RGW region for S3 operations |
| `imageMetadataExcludedProperties` | `string` | — | Comma-separated image metadata keys to hide in the UI |
| `cookieName` | `string` | `"dashboard-session-auth"` | Override the session cookie name |
| `crossDomainCookie` | `boolean` | `true` | Share cookie across subdomains |
| `insecureCookies` | `boolean` | `false` | Disable `Secure` flag — only for HTTP-only local dev |
| Option | Type | Required | Default | Description |
| --------------------------------- | --------- | -------- | -------------------------- | ---------------------------------------------------------------- |
| `identityEndpoint` | `string` | yes | — | OpenStack Keystone v3 URL |
| `policyDir` | `string` | yes | — | Absolute path to a directory of OpenStack policy YAML files |
| `bffEndpoint` | `string` | no | `"/polaris-bff"` | URL prefix for all tRPC routes |
| `viteRoot` | `string` | no | — | Directory that contains `dist/client/` in production |
| `defaultEndpointInterface` | `string` | no | `"public"` | OpenStack service catalog interface |
Comment thread
taymoor89 marked this conversation as resolved.
| `proxyUrl` | `string` | no | — | HTTP proxy for OpenStack calls (dev only, ignored in production) |
| `cephRegion` | `string` | no | — | Ceph RGW region for S3 operations |
| `imageMetadataExcludedProperties` | `string` | no | — | Comma-separated image metadata keys to hide in the UI |
| `cookieName` | `string` | no | `"dashboard-session-auth"` | Override the session cookie name |
| `crossDomainCookie` | `boolean` | no | `true` | Share cookie across subdomains |
| `insecureCookies` | `boolean` | no | `false` | Disable `Secure` flag — only for HTTP-only local dev |

### `<AuroraApp />`

| Prop | Type | Default | Description |
| --------------- | ------------------------------- | ---------------- | -------------------------------------- |
| `bffEndpoint` | `string` | `"/polaris-bff"` | Must match the server's `bffEndpoint` |
| `theme` | `"theme-light" \| "theme-dark"` | `"theme-light"` | Initial theme |
| `onThemeChange` | `(theme) => void` | — | Called when the user toggles the theme |
| Prop | Type | Default | Description |
| --------------- | ------------------------------- | ---------------- | -------------------------------------------------- |
| `bffEndpoint` | `string` | `"/polaris-bff"` | Must match the server's `bffEndpoint` |
| `theme` | `"theme-light" \| "theme-dark"` | `"theme-light"` | Initial theme |
| `onThemeChange` | `(theme) => void` | — | Called when the user toggles the theme |
| `appName` | `string` | `"Aurora"` | App name shown in the header breadcrumb and logo |
| `slots` | `Slots` | — | Optional UI extension points — see [Slots](#slots) |

## Environment variables
## Slots

Your app reads these from `.env` and passes them to `createServer()`. Aurora itself never reads environment variables.
Slots let you inject your own React components into specific locations inside `AuroraApp` without forking the package. Each slot receives an `auroraContext` object with a `client` (tRPC client) for making API calls.

```env
IDENTITY_ENDPOINT="https://keystone.example.com/v3/"
DEFAULT_ENDPOINT_INTERFACE="public"
BFF_ENDPOINT="/polaris-bff"
PORT="4000"
```tsx
import type { SlotProps } from "@cobaltcore-dev/aurora/client"

# Object storage (optional)
CEPH_REGION="ceph-objectstore-st1-region-1"
function MyLogo(_props: SlotProps) {
return <img src="/my-logo.svg" alt="My App" style={{ height: "1.5rem" }} />
}

# Local dev only
INSECURE_COOKIES=true
# GLOBAL_AGENT_HTTP_PROXY=http://localhost:8888
```
function MyBanner({ auroraContext }: SlotProps) {
// auroraContext.client gives you access to the tRPC API
return <div>Custom sidebar content</div>
}

The `VITE_BFF_ENDPOINT` variable (prefixed with `VITE_`) is read by Vite and passed to the client:
function MyFooter(_props: SlotProps) {
return (
<footer>
<a href="/imprint">Imprint</a> · <a href="/privacy">Privacy</a>
</footer>
)
}

```env
VITE_BFF_ENDPOINT="/polaris-bff"
;<AuroraApp
slots={{
logo: MyLogo,
sideNavBanner: MyBanner,
pageFooter: MyFooter,
}}
/>
```

```tsx
<AuroraApp bffEndpoint={import.meta.env.VITE_BFF_ENDPOINT} />
```
### Available slots

## Vite config
| Slot | Location | Renders in shadow DOM |
| --------------- | ----------------------------------------------- | --------------------- |
| `logo` | Page header, replacing the default Aurora logo | No |
| `sideNavBanner` | Bottom of the project side navigation | Yes |
| `pageFooter` | Page footer, replacing the default empty footer | No |

In development, point Vite at your own server plugin. In production no extra plugins are needed — aurora ships pre-built CSS and assets.
**Shadow DOM isolation:** Slots rendered in a shadow DOM cannot inherit styles from the host page. If your slot component uses a CSS framework, inject the styles inline:

```js
// vite.config.mjs
import { defineConfig } from "vite"
import react from "@vitejs/plugin-react-swc"
import viteFastify from "@fastify/vite/plugin"

export default defineConfig(({ mode }) => ({
root: "./src/client",
build: { outDir: "../../dist/client" },
plugins: [mode !== "production" && viteFastify(), react()],
}))
```

## Running the app

```bash
# Development (hot-reload)
tsx watch --env-file=.env src/server/server.ts
```tsx
import styles from "my-lib/styles.css?inline"

# Production
NODE_ENV=production tsx --env-file=.env src/server/server.ts
function MyBanner(_props: SlotProps) {
return (
<>
<style>{styles}</style>
<div>...</div>
</>
)
}
```

## License
Expand Down
5 changes: 5 additions & 0 deletions packages/aurora/src/client/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type AppProps = {
bffEndpoint?: string
onThemeChange?: (theme: "theme-dark" | "theme-light") => void
slots?: Slots
appName?: string
}

// Additional navigation items can be added here and will be passed to the layout via context
Expand Down Expand Up @@ -92,6 +93,7 @@ const App = (props: AppProps) => {
navItems={navItems}
handleThemeToggle={handleThemeToggle}
slots={props.slots}
appName={props.appName}
/>
</AuthProvider>
</QueryClientProvider>
Expand All @@ -107,11 +109,13 @@ function AppInner({
navItems,
handleThemeToggle,
slots,
appName,
}: {
router: ReturnType<typeof createAuroraRouter>
navItems: NavigationItem[]
handleThemeToggle: (theme: string) => void
slots?: Slots
appName?: string
}) {
const auth = useAuth()

Expand All @@ -122,6 +126,7 @@ function AppInner({
navItems,
handleThemeToggle,
slots,
appName,
}

return <RouterProvider router={router} context={routerContext} />
Expand Down
15 changes: 15 additions & 0 deletions packages/aurora/src/client/AuroraApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,36 @@ import type { FC } from "react"
import type { TrpcClient } from "./trpcClient"
import App from "./App"

/** Context object passed to every slot component. */
export type SlotProps = {
auroraContext: {
/** tRPC client for making API calls to the Aurora BFF. */
client: TrpcClient
}
}

/** Named extension points where consumers can inject their own React components. */
export type Slots = {
/** Replaces the default Aurora logo in the page header. Renders outside shadow DOM — inherits header styles. */
logo?: FC<SlotProps>
/** Rendered at the bottom of the project side navigation. Renders inside a shadow DOM. */
sideNavBanner?: FC<SlotProps>
/** Replaces the default page footer. Renders outside shadow DOM — inherits page styles. */
pageFooter?: FC<SlotProps>
}

/** Props for the top-level `<AuroraApp />` component. */
export type AuroraAppProps = {
/** Initial theme. Defaults to `"theme-light"`. */
theme?: "theme-dark" | "theme-light"
/** URL prefix for all tRPC routes. Must match the server's `bffEndpoint`. Defaults to `"/polaris-bff"`. */
bffEndpoint?: string
/** Called when the user toggles the theme. Use this to persist the selection. */
onThemeChange?: (theme: "theme-dark" | "theme-light") => void
/** Optional UI extension points. */
slots?: Slots
/** App name shown in the header breadcrumb and as the default logo title. Defaults to `"Aurora"`. */
appName?: string
}

export const AuroraApp: FC<AuroraAppProps> = App
9 changes: 9 additions & 0 deletions packages/aurora/src/client/components/Slot.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,13 @@ describe("Slot", () => {
})
expect(HTMLElement.prototype.attachShadow).toHaveBeenCalledWith({ mode: "open" })
})

it("renders the component without a shadow root when useShadowDOM is false", async () => {
let container!: HTMLElement
await act(async () => {
;({ container } = render(<Slot component={TestWidget} useShadowDOM={false} />))
})
expect(container.querySelector("[data-testid='test-widget']")).toBeTruthy()
expect(HTMLElement.prototype.attachShadow).not.toHaveBeenCalled()
})
})
20 changes: 14 additions & 6 deletions packages/aurora/src/client/components/Slot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,22 @@ function SlotShadowRoot({ children }: { children: ReactNode }) {
)
}

export function Slot({ component: Component }: { component: FC<SlotProps> }) {
export function Slot({
component: Component,
useShadowDOM = true,
}: {
component: FC<SlotProps>
useShadowDOM?: boolean
}) {
const { trpcClient } = useRouteContext({ strict: false })

if (!trpcClient) return null

return (
<SlotShadowRoot>
<Component auroraContext={{ client: trpcClient }} />
</SlotShadowRoot>
)
const content = <Component auroraContext={{ client: trpcClient }} />

if (!useShadowDOM) {
return content
}
Comment thread
taymoor89 marked this conversation as resolved.

return <SlotShadowRoot>{content}</SlotShadowRoot>
}
Loading
Loading