@cobaltcore-dev/aurora 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/README.md +59 -9
  2. package/dist/client/App.d.ts +2 -2
  3. package/dist/client/AuroraApp.d.ts +33 -13
  4. package/dist/client/{AuthProvider-BS5OVv8n.mjs → AuthProvider-CVSAwW9f.mjs} +22 -8
  5. package/dist/client/AuthProvider-CVSAwW9f.mjs.map +1 -0
  6. package/dist/client/{ContentHeader-BHMUzd9H.mjs → ContentHeader-CPJbgYWn.mjs} +18 -18
  7. package/dist/client/{ContentHeader-BHMUzd9H.mjs.map → ContentHeader-CPJbgYWn.mjs.map} +1 -1
  8. package/dist/client/DeleteFlavorModal-V44QCJBd.mjs +773 -0
  9. package/dist/client/DeleteFlavorModal-V44QCJBd.mjs.map +1 -0
  10. package/dist/client/DynamicBreadcrumbContext-CVcUQFou.mjs +26 -0
  11. package/dist/client/DynamicBreadcrumbContext-CVcUQFou.mjs.map +1 -0
  12. package/dist/client/{FiltersInput-B8CNBg1L.mjs → FiltersInput-CKI6zWnm.mjs} +18 -18
  13. package/dist/client/{FiltersInput-B8CNBg1L.mjs.map → FiltersInput-CKI6zWnm.mjs.map} +1 -1
  14. package/dist/client/FloatingIpActionModals-CwEC5GR-.mjs +460 -0
  15. package/dist/client/FloatingIpActionModals-CwEC5GR-.mjs.map +1 -0
  16. package/dist/client/ImageToastNotifications-m6c3tVPb.mjs +1152 -0
  17. package/dist/client/ImageToastNotifications-m6c3tVPb.mjs.map +1 -0
  18. package/dist/client/SortInput-4g7nSTHW.mjs +34 -0
  19. package/dist/client/SortInput-4g7nSTHW.mjs.map +1 -0
  20. package/dist/client/{TwoColumnDescriptionList-DiDx3PnH.mjs → TwoColumnDescriptionList-NvgvErua.mjs} +2 -2
  21. package/dist/client/{TwoColumnDescriptionList-DiDx3PnH.mjs.map → TwoColumnDescriptionList-NvgvErua.mjs.map} +1 -1
  22. package/dist/client/_-CAsCx-Ay.mjs +6 -0
  23. package/dist/client/_-CAsCx-Ay.mjs.map +1 -0
  24. package/dist/client/_auth-SrDjHL6j.mjs +12 -0
  25. package/dist/client/_auth-SrDjHL6j.mjs.map +1 -0
  26. package/dist/client/_flavorId-9PcBBW52.mjs +231 -0
  27. package/dist/client/_flavorId-9PcBBW52.mjs.map +1 -0
  28. package/dist/client/{_flavorId-BN_qessZ.mjs → _flavorId-a9Gu66R1.mjs} +3 -9
  29. package/dist/client/_flavorId-a9Gu66R1.mjs.map +1 -0
  30. package/dist/client/_floatingIpId-AE9Fu8l6.mjs +210 -0
  31. package/dist/client/_floatingIpId-AE9Fu8l6.mjs.map +1 -0
  32. package/dist/client/{_floatingIpId-CwDGHkWm.mjs → _floatingIpId-AZ11Ksmu.mjs} +3 -9
  33. package/dist/client/_floatingIpId-AZ11Ksmu.mjs.map +1 -0
  34. package/dist/client/{_imageId-BtJ7GmYB.mjs → _imageId-BtAql2Kr.mjs} +104 -92
  35. package/dist/client/_imageId-BtAql2Kr.mjs.map +1 -0
  36. package/dist/client/_imageId-D76BxvFY.mjs +39 -0
  37. package/dist/client/_imageId-D76BxvFY.mjs.map +1 -0
  38. package/dist/client/{_projectId-Byt3GQDW.mjs → _projectId-7Nl1MQTJ.mjs} +5 -5
  39. package/dist/client/_projectId-7Nl1MQTJ.mjs.map +1 -0
  40. package/dist/client/_projectId-B3wmSala.mjs +281 -0
  41. package/dist/client/_projectId-B3wmSala.mjs.map +1 -0
  42. package/dist/client/_projectId-DPzv0CCp.mjs +77 -0
  43. package/dist/client/_projectId-DPzv0CCp.mjs.map +1 -0
  44. package/dist/client/_projectId-DfeVJy0G.mjs +30 -0
  45. package/dist/client/_projectId-DfeVJy0G.mjs.map +1 -0
  46. package/dist/client/_securityGroupId-C5huJNfO.mjs +1810 -0
  47. package/dist/client/_securityGroupId-C5huJNfO.mjs.map +1 -0
  48. package/dist/client/{_securityGroupId-BcbkRhsN.mjs → _securityGroupId-D85mSigA.mjs} +3 -9
  49. package/dist/client/_securityGroupId-D85mSigA.mjs.map +1 -0
  50. package/dist/client/_serviceType-DS8DxsrP.mjs +20 -0
  51. package/dist/client/_serviceType-DS8DxsrP.mjs.map +1 -0
  52. package/dist/client/_serviceType-M8d9rOT6.mjs +6 -0
  53. package/dist/client/_serviceType-M8d9rOT6.mjs.map +1 -0
  54. package/dist/client/_storageType-BRWWJtow.mjs +14 -0
  55. package/dist/client/_storageType-BRWWJtow.mjs.map +1 -0
  56. package/dist/client/_storageType-CzZ4u9x_.mjs +12 -0
  57. package/dist/client/_storageType-CzZ4u9x_.mjs.map +1 -0
  58. package/dist/client/_storageType-Dy9uCVXr.mjs +845 -0
  59. package/dist/client/_storageType-Dy9uCVXr.mjs.map +1 -0
  60. package/dist/client/_storageType-zeSZe--V.mjs.map +1 -1
  61. package/dist/client/{about-_73VmpTB.mjs → about-BmQTyLWc.mjs} +2 -2
  62. package/dist/client/{about-_73VmpTB.mjs.map → about-BmQTyLWc.mjs.map} +1 -1
  63. package/dist/client/{aurora-EbwGRLLf.mjs → aurora-CIml-f2f.mjs} +2 -2
  64. package/dist/client/{aurora-EbwGRLLf.mjs.map → aurora-CIml-f2f.mjs.map} +1 -1
  65. package/dist/client/{build-C_4vRMCq.mjs → build-WuxXRAqf.mjs} +121 -24
  66. package/dist/client/{build-C_4vRMCq.mjs.map → build-WuxXRAqf.mjs.map} +1 -1
  67. package/dist/client/components/Breadcrumbs.d.ts +1 -0
  68. package/dist/client/components/ListToolbar/SortInput.d.ts +3 -0
  69. package/dist/client/context/BreadcrumbExtensionContext.d.ts +9 -0
  70. package/dist/client/context/DynamicBreadcrumbContext.d.ts +9 -0
  71. package/dist/client/{flavors-CiHCYrtZ.mjs → flavors-B0rbqffG.mjs} +3 -5
  72. package/dist/client/flavors-B0rbqffG.mjs.map +1 -0
  73. package/dist/client/flavors-DWMZ6TuJ.mjs.map +1 -1
  74. package/dist/client/flavors-Rl20bxVU.mjs +628 -0
  75. package/dist/client/flavors-Rl20bxVU.mjs.map +1 -0
  76. package/dist/client/floatingips-Cz9k80cc.mjs +8 -0
  77. package/dist/client/floatingips-Cz9k80cc.mjs.map +1 -0
  78. package/dist/client/floatingips-xbO_mu4f.mjs +576 -0
  79. package/dist/client/floatingips-xbO_mu4f.mjs.map +1 -0
  80. package/dist/client/hooks/useBreadcrumbs.d.ts +16 -0
  81. package/dist/client/hooks/usePushBreadcrumbs.d.ts +10 -0
  82. package/dist/client/hooks/useSetBreadcrumb.d.ts +11 -0
  83. package/dist/client/{hooks-LutR2Yg3.mjs → hooks-D0yKIumu.mjs} +2 -2
  84. package/dist/client/{hooks-LutR2Yg3.mjs.map → hooks-D0yKIumu.mjs.map} +1 -1
  85. package/dist/client/images-BGQv9kKT.mjs +1983 -0
  86. package/dist/client/images-BGQv9kKT.mjs.map +1 -0
  87. package/dist/client/{images-CS4kZQyu.mjs → images-CPmMa4SQ.mjs} +3 -5
  88. package/dist/client/images-CPmMa4SQ.mjs.map +1 -0
  89. package/dist/client/images-NBf2bV43.mjs.map +1 -1
  90. package/dist/client/index.d.ts +7 -3
  91. package/dist/client/index.js +549 -535
  92. package/dist/client/index.js.map +1 -1
  93. package/dist/client/{objects-C3azvW8m.mjs → objects-BE_u5yRt.mjs} +7087 -4211
  94. package/dist/client/objects-BE_u5yRt.mjs.map +1 -0
  95. package/dist/client/objects-BN-59Th0.mjs +66 -0
  96. package/dist/client/objects-BN-59Th0.mjs.map +1 -0
  97. package/dist/client/objects-D9Luf9KB.mjs +66 -0
  98. package/dist/client/objects-D9Luf9KB.mjs.map +1 -0
  99. package/dist/client/projects-yiK0HGSA.mjs.map +1 -1
  100. package/dist/client/routeInfo-DP3v74Wg.mjs +19 -0
  101. package/dist/client/routeInfo-DP3v74Wg.mjs.map +1 -0
  102. package/dist/client/router.d.ts +2 -2
  103. package/dist/client/{routes-CdDm-hhO.mjs → routes-CJj7SR_K.mjs} +2 -2
  104. package/dist/client/routes-CJj7SR_K.mjs.map +1 -0
  105. package/dist/client/routes-xofA3ksU.mjs +110 -0
  106. package/dist/client/routes-xofA3ksU.mjs.map +1 -0
  107. package/dist/client/securitygroups-BjhgLBHm.mjs +620 -0
  108. package/dist/client/securitygroups-BjhgLBHm.mjs.map +1 -0
  109. package/dist/client/securitygroups-CtKne8UQ.mjs +8 -0
  110. package/dist/client/securitygroups-CtKne8UQ.mjs.map +1 -0
  111. package/dist/client/{securitygroups-Byte-iSp.mjs → securitygroups-DEJQBcxm.mjs} +3 -5
  112. package/dist/client/securitygroups-DEJQBcxm.mjs.map +1 -0
  113. package/dist/client/trpcClient.d.ts +135 -3
  114. package/dist/client/useBreadcrumbs-ha3IlaxX.mjs +54 -0
  115. package/dist/client/useBreadcrumbs-ha3IlaxX.mjs.map +1 -0
  116. package/dist/client/useModalTracking-BcOKTYRN.mjs +33 -0
  117. package/dist/client/useModalTracking-BcOKTYRN.mjs.map +1 -0
  118. package/dist/client/{useSecurityGroupPermissions-C29mzb3b.mjs → useSecurityGroupPermissions-BOyLQLdC.mjs} +85 -19
  119. package/dist/client/useSecurityGroupPermissions-BOyLQLdC.mjs.map +1 -0
  120. package/dist/client/useSetBreadcrumb-tn7lBaQV.mjs +19 -0
  121. package/dist/client/useSetBreadcrumb-tn7lBaQV.mjs.map +1 -0
  122. package/dist/client/utils/formatSwiftDate.d.ts +30 -0
  123. package/dist/server/index.d.ts +136 -4
  124. package/dist/server/index.js +845 -260
  125. package/package.json +37 -36
  126. package/dist/client/AuthProvider-BS5OVv8n.mjs.map +0 -1
  127. package/dist/client/DeleteFlavorModal-BP83t9zX.mjs +0 -721
  128. package/dist/client/DeleteFlavorModal-BP83t9zX.mjs.map +0 -1
  129. package/dist/client/FloatingIpActionModals-BraiJWBi.mjs +0 -417
  130. package/dist/client/FloatingIpActionModals-BraiJWBi.mjs.map +0 -1
  131. package/dist/client/ImageToastNotifications-BXMHJaIH.mjs +0 -1131
  132. package/dist/client/ImageToastNotifications-BXMHJaIH.mjs.map +0 -1
  133. package/dist/client/RouteError-hAF8_jbe.mjs +0 -25
  134. package/dist/client/RouteError-hAF8_jbe.mjs.map +0 -1
  135. package/dist/client/SortInput-Cqs2poV3.mjs +0 -34
  136. package/dist/client/SortInput-Cqs2poV3.mjs.map +0 -1
  137. package/dist/client/StatusError-C5KodQOq.mjs +0 -39
  138. package/dist/client/StatusError-C5KodQOq.mjs.map +0 -1
  139. package/dist/client/_auth-DJ9wHZaj.mjs +0 -11
  140. package/dist/client/_auth-DJ9wHZaj.mjs.map +0 -1
  141. package/dist/client/_flavorId-BN_qessZ.mjs.map +0 -1
  142. package/dist/client/_flavorId-CJjEB_Ao.mjs +0 -210
  143. package/dist/client/_flavorId-CJjEB_Ao.mjs.map +0 -1
  144. package/dist/client/_floatingIpId-CwDGHkWm.mjs.map +0 -1
  145. package/dist/client/_floatingIpId-foMIwbE4.mjs +0 -215
  146. package/dist/client/_floatingIpId-foMIwbE4.mjs.map +0 -1
  147. package/dist/client/_imageId-BtJ7GmYB.mjs.map +0 -1
  148. package/dist/client/_projectId-BmtCuzJu.mjs +0 -29
  149. package/dist/client/_projectId-BmtCuzJu.mjs.map +0 -1
  150. package/dist/client/_projectId-Byt3GQDW.mjs.map +0 -1
  151. package/dist/client/_projectId-NYk1IN_a.mjs +0 -291
  152. package/dist/client/_projectId-NYk1IN_a.mjs.map +0 -1
  153. package/dist/client/_projectId-QueiFczy.mjs +0 -46
  154. package/dist/client/_projectId-QueiFczy.mjs.map +0 -1
  155. package/dist/client/_securityGroupId-BcbkRhsN.mjs.map +0 -1
  156. package/dist/client/_securityGroupId-pPtJsCoe.mjs +0 -1797
  157. package/dist/client/_securityGroupId-pPtJsCoe.mjs.map +0 -1
  158. package/dist/client/_storageType-BmqrU06i.mjs +0 -2202
  159. package/dist/client/_storageType-BmqrU06i.mjs.map +0 -1
  160. package/dist/client/components/Error/StatusError.d.ts +0 -11
  161. package/dist/client/components/ProjectView/ProjectInfoBox.d.ts +0 -12
  162. package/dist/client/flavors-CiHCYrtZ.mjs.map +0 -1
  163. package/dist/client/flavors-DY-bDc8l.mjs +0 -650
  164. package/dist/client/flavors-DY-bDc8l.mjs.map +0 -1
  165. package/dist/client/floatingips-Ci4jHkaG.mjs +0 -591
  166. package/dist/client/floatingips-Ci4jHkaG.mjs.map +0 -1
  167. package/dist/client/images-91U7tr9Y2.mjs +0 -1976
  168. package/dist/client/images-91U7tr9Y2.mjs.map +0 -1
  169. package/dist/client/images-CS4kZQyu.mjs.map +0 -1
  170. package/dist/client/objects-C3azvW8m.mjs.map +0 -1
  171. package/dist/client/objects-CM265S6b.mjs +0 -56
  172. package/dist/client/objects-CM265S6b.mjs.map +0 -1
  173. package/dist/client/objects-EE9Cd1wI.mjs +0 -56
  174. package/dist/client/objects-EE9Cd1wI.mjs.map +0 -1
  175. package/dist/client/routeInfo-DXqB2HCf.mjs +0 -32
  176. package/dist/client/routeInfo-DXqB2HCf.mjs.map +0 -1
  177. package/dist/client/routes-CBd8CCuM.mjs +0 -115
  178. package/dist/client/routes-CBd8CCuM.mjs.map +0 -1
  179. package/dist/client/routes-CdDm-hhO.mjs.map +0 -1
  180. package/dist/client/securitygroups-Bd_uV4S9.mjs +0 -595
  181. package/dist/client/securitygroups-Bd_uV4S9.mjs.map +0 -1
  182. package/dist/client/securitygroups-Byte-iSp.mjs.map +0 -1
  183. package/dist/client/useSecurityGroupPermissions-C29mzb3b.mjs.map +0 -1
package/README.md CHANGED
@@ -114,15 +114,16 @@ await server.listen({ host: "0.0.0.0", port: 4000 })
114
114
 
115
115
  ### `<AuroraApp />`
116
116
 
117
- | Prop | Type | Default | Description |
118
- | ----------------- | ------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------- |
119
- | `bffEndpoint` | `string` | `"/polaris-bff"` | Must match the server's `bffEndpoint` |
120
- | `theme` | `"theme-light" \| "theme-dark"` | `"theme-light"` | Initial theme |
121
- | `onThemeChange` | `(theme) => void` | — | Called when the user toggles the theme |
122
- | `appName` | `string` | `"Aurora"` | App name shown in the header breadcrumb and logo |
123
- | `slots` | `Slots` | — | Optional UI extension points — see [Slots](#slots) |
124
- | `onTrackEvent` | `OnTrackEventCallback` | — | Called on user interactions for analytics — see [Analytics](#analytics) |
125
- | `enabledServices` | `string[]` | — | Whitelist of service keys to show. When omitted, all services are shown — see [Enabled services](#enabled-services) |
117
+ | Prop | Type | Default | Description |
118
+ | ------------------- | ------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------- |
119
+ | `bffEndpoint` | `string` | `"/polaris-bff"` | Must match the server's `bffEndpoint` |
120
+ | `theme` | `"theme-light" \| "theme-dark"` | `"theme-light"` | Initial theme |
121
+ | `onThemeChange` | `(theme) => void` | — | Called when the user toggles the theme |
122
+ | `appName` | `string` | `"Aurora"` | App name shown in the header breadcrumb and logo |
123
+ | `slots` | `Slots` | — | Optional UI extension points — see [Slots](#slots) |
124
+ | `onTrackEvent` | `OnTrackEventCallback` | — | Called on user interactions for analytics — see [Analytics](#analytics) |
125
+ | `enabledServices` | `string[]` | — | Whitelist of service keys to show. When omitted, all services are shown — see [Enabled services](#enabled-services) |
126
+ | `serviceExtensions` | `ServiceExtension[]` | — | Consumer-defined services to mount under project scope — see [Service extensions](#service-extensions) |
126
127
 
127
128
  ## Slots
128
129
 
@@ -214,6 +215,55 @@ enabledServices={import.meta.env.VITE_ENABLED_SERVICES?.split(",").map(s => s.tr
214
215
  VITE_ENABLED_SERVICES="ceph-containers,securitygroups"
215
216
  ```
216
217
 
218
+ ## Service extensions
219
+
220
+ Aurora lets you register consumer-owned services (service extensions) that appear alongside the built-in ones in the project navigation. Each extension is tied to an OpenStack catalog entry — Aurora checks whether the service type and name exist in the project's catalog and only shows the service when they do.
221
+
222
+ Pass an array of service descriptors to `AuroraApp`:
223
+
224
+ ```tsx
225
+ import type { ServiceExtension } from "@cobaltcore-dev/aurora/client"
226
+ import { MyServiceApp } from "./myService/MyServiceApp"
227
+
228
+ const serviceExtensions: ServiceExtension[] = [
229
+ {
230
+ serviceType: "my-service", // OpenStack catalog service type
231
+ serviceName: "my-service", // OpenStack catalog service name
232
+ label: "My Service", // Label shown in the side nav and project home
233
+ component: MyServiceApp, // React component that renders the service UI
234
+ },
235
+ ]
236
+
237
+ ;<AuroraApp serviceExtensions={serviceExtensions} />
238
+ ```
239
+
240
+ The `component` receives a `basePath` (the URL prefix Aurora has mounted the service at) and a `context` object of host-provided data (`ServiceExtensionContext`). Today `context` carries the current `projectId`; it is an extensible object, so future host data (e.g. domain, scope) can be added without changing the component signature.
241
+
242
+ Because Aurora mounts your extension at `basePath` and strips it from the sub-router, your routes never see `projectId` as a URL param. Seed `context` into your own router context so any component can read it:
243
+
244
+ ```tsx
245
+ import { RouterProvider, createRouter, useRouteContext } from "@tanstack/react-router"
246
+ import { useMemo } from "react"
247
+ import type { ServiceExtensionProps, ServiceExtensionContext } from "@cobaltcore-dev/aurora/client"
248
+ import { routeTree } from "./routeTree.gen"
249
+
250
+ export function MyServiceApp({ basePath, context }: ServiceExtensionProps) {
251
+ // Seed the host context into the extension's own router context.
252
+ const router = useMemo(() => createRouter({ routeTree, context }), [basePath, context])
253
+ return <RouterProvider basepath={basePath} router={router} />
254
+ }
255
+
256
+ // In your root route, type the context so it flows through:
257
+ // createRootRouteWithContext<ServiceExtensionContext>()({ ... })
258
+ //
259
+ // Then read projectId anywhere in your tree:
260
+ function useProjectId(): string {
261
+ return useRouteContext({ strict: false, select: (c: ServiceExtensionContext) => c.projectId })
262
+ }
263
+ ```
264
+
265
+ Aurora handles routing to your service, breadcrumb rendering for the service level, and showing or hiding the nav entry based on catalog availability. Navigation and breadcrumb details below the service level are the responsibility of the service component.
266
+
217
267
  ## Analytics
218
268
 
219
269
  Aurora provides an optional `onTrackEvent` callback to track user interactions and feature usage. The API is source-agnostic, supporting various event types like navigation, link clicks, modals, and more.
@@ -1,4 +1,4 @@
1
- import { Slots, OnTrackEventCallback, AdditionalProjectService } from './AuroraApp';
1
+ import { Slots, OnTrackEventCallback, ServiceExtension } from './AuroraApp';
2
2
  type AppProps = {
3
3
  theme?: "theme-dark" | "theme-light";
4
4
  bffEndpoint?: string;
@@ -7,7 +7,7 @@ type AppProps = {
7
7
  appName?: string;
8
8
  onTrackEvent?: OnTrackEventCallback;
9
9
  enabledServices?: string[];
10
- additionalProjectServices?: AdditionalProjectService[];
10
+ serviceExtensions?: ServiceExtension[];
11
11
  };
12
12
  declare const App: (props: AppProps) => import("react").JSX.Element;
13
13
  export default App;
@@ -1,5 +1,4 @@
1
1
  import { FC } from 'react';
2
- import { AnyRoute } from '@tanstack/react-router';
3
2
  import { TrpcClient } from './trpcClient';
4
3
  /** Context object passed to every slot component. */
5
4
  export type SlotProps = {
@@ -49,13 +48,32 @@ export type TrackEventPayload = {
49
48
  */
50
49
  export type OnTrackEventCallback = (payload: TrackEventPayload) => void;
51
50
  /**
52
- * An additional project-scoped service a consumer wants to register.
51
+ * Host-provided data handed to a mounted service extension. Extend this as extensions need more
52
+ * from OSS (domainId, scope, a typed tRPC client, ...). Seed it into the extension's own router
53
+ * context so pages can read it via `useRouteContext`.
54
+ */
55
+ export type ServiceExtensionContext = {
56
+ /** The current project ID. */
57
+ projectId: string;
58
+ };
59
+ /**
60
+ * Props passed to a service extension component mounted by OSS.
61
+ * The basePath is the full URL prefix up to and including the service type segment.
62
+ */
63
+ export type ServiceExtensionProps = {
64
+ /** Full URL prefix for this service, e.g. "/projects/abc/services/pca". Pass as `basepath` to RouterProvider. */
65
+ basePath: string;
66
+ /** Host context to seed into the extension's own router context. */
67
+ context: ServiceExtensionContext;
68
+ };
69
+ /**
70
+ * A project-scoped service extension a consumer wants to register.
53
71
  * Aurora checks `serviceIndex[serviceType][serviceName]` against the project's OpenStack catalog.
54
- * When present, the service appears in the "Services" nav section and its routes are activated
55
- * under the OSS-owned `/services` layout. Pass the service's entry route (list/overview) as
56
- * `routes` — wire its full sub-tree via `addChildren` and set `getParentRoute: () => servicesRoute`.
72
+ * When present, the service appears in the "Services" nav section and its component is mounted
73
+ * under `/projects/$projectId/services/$serviceType`. The component receives `basePath` and
74
+ * `context`: use these to set up a standalone RouterProvider for the service.
57
75
  */
58
- export type AdditionalProjectService = {
76
+ export type ServiceExtension = {
59
77
  /** OpenStack catalog service type, e.g. `"pca"`. */
60
78
  serviceType: string;
61
79
  /** OpenStack catalog service name, e.g. `"clavis"`. */
@@ -63,12 +81,14 @@ export type AdditionalProjectService = {
63
81
  /** Nav item and service card label. */
64
82
  label: string;
65
83
  /**
66
- * The pre-assembled entry route for this service (list/overview).
67
- * Set `getParentRoute: () => servicesRoute` (from `@cobaltcore-dev/aurora/client`) and wire
68
- * the full sub-tree via `addChildren` before passing here.
69
- * `routes.fullPath` is used as the nav item navigation target automatically.
84
+ * The React component to render at `/projects/$projectId/services/$serviceType/**`.
85
+ * Receives `basePath` (the URL prefix) and `context` (host data). Create your own RouterProvider
86
+ * inside this component using `basePath` as the `basepath` prop and `context` as the router context.
87
+ *
88
+ * Note: this is a self-contained sub-app. Host slots (see `Slots`) are not injected into it, so the
89
+ * extension owns its own header/actions (e.g. via the re-exported `PageContentHeader` `actions` prop).
70
90
  */
71
- routes: AnyRoute;
91
+ component: FC<ServiceExtensionProps>;
72
92
  };
73
93
  /** Props for the top-level `<AuroraApp />` component. */
74
94
  export type AuroraAppProps = {
@@ -105,7 +125,7 @@ export type AuroraAppProps = {
105
125
  onTrackEvent?: OnTrackEventCallback;
106
126
  /** Whitelist of service keys to show in the side nav and project home page. When omitted, all available services are shown. */
107
127
  enabledServices?: string[];
108
- /** Additional project-scoped services to register. Each is activated when its OpenStack service appears in the project catalog. */
109
- additionalProjectServices?: AdditionalProjectService[];
128
+ /** Service extensions to register. Each is activated when its OpenStack service appears in the project catalog. */
129
+ serviceExtensions?: ServiceExtension[];
110
130
  };
111
131
  export declare const AuroraApp: FC<AuroraAppProps>;
@@ -20,15 +20,22 @@ function o({ children: n }) {
20
20
  }).finally(() => l(!1));
21
21
  }, []), r(() => {
22
22
  if (!o || !f) return;
23
- let e = new Date(f).getTime() - Date.now(), t = () => {
24
- h(), m(!0);
23
+ let t = new Date(f).getTime() - Date.now(), n = async () => {
24
+ l(!0);
25
+ try {
26
+ await e.auth.terminateUserSession.mutate();
27
+ } catch (e) {
28
+ d(e instanceof Error ? e.message : `Session termination failed: ${e}`);
29
+ } finally {
30
+ h(), l(!1), m(!0);
31
+ }
25
32
  };
26
- if (e <= 0) {
27
- t();
33
+ if (t <= 0) {
34
+ n();
28
35
  return;
29
36
  }
30
- let n = setTimeout(t, e);
31
- return () => clearTimeout(n);
37
+ let r = setTimeout(n, t);
38
+ return () => clearTimeout(r);
32
39
  }, [o, f]), /*#__PURE__*/ t(a.Provider, {
33
40
  value: {
34
41
  user: o,
@@ -51,7 +58,14 @@ function o({ children: n }) {
51
58
  }
52
59
  },
53
60
  logout: async () => {
54
- l(!0), await e.auth.terminateUserSession.mutate().catch(() => {}), h(), m(!1), window.location.pathname === "/" && l(!1);
61
+ l(!0), d(null);
62
+ try {
63
+ await e.auth.terminateUserSession.mutate();
64
+ } catch (e) {
65
+ d(e instanceof Error ? e.message : `Logout failed: ${e}`);
66
+ } finally {
67
+ h(), l(!1), m(!1);
68
+ }
55
69
  },
56
70
  expiresAt: f ? new Date(f) : void 0
57
71
  },
@@ -66,4 +80,4 @@ function s() {
66
80
  //#endregion
67
81
  export { s as n, o as t };
68
82
 
69
- //# sourceMappingURL=AuthProvider-BS5OVv8n.mjs.map
83
+ //# sourceMappingURL=AuthProvider-CVSAwW9f.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AuthProvider-CVSAwW9f.mjs","names":["React","useEffect","useState","trpcClient","AuthContext","createContext","AuthProvider","children","user","setUser","isLoading","setIsLoading","error","setError","expiresAt","setExpiresAt","redirectToLogin","saveReturnUrl","window","location","pathname","returnUrl","encodeURIComponent","search","replace","clearLocalSession","auth","getCurrentUserSession","query","then","session","expires_at","catch","err","Error","message","finally","timeUntilExpiry","Date","getTime","now","handleExpiry","terminateUserSession","mutate","timer","setTimeout","clearTimeout","login","domain","password","createUserSession","domainName","success","logout","Provider","value","isAuthenticated","undefined","useAuth","context","useContext"],"sources":["../../src/client/store/AuthProvider.tsx"],"sourcesContent":["import React, { useEffect, useState } from \"react\"\nimport { TokenData } from \"../../server/Authentication/types/models\"\nimport { trpcClient } from \"../trpcClient\"\n\nexport type User = TokenData[\"user\"] | null\n\nexport interface AuthContext {\n isAuthenticated: boolean\n isLoading: boolean\n error: string | null\n login: (credentials: { domain: string; user: string; password: string }) => Promise<{ success: boolean }>\n logout: () => Promise<void>\n user?: User\n expiresAt?: Date\n}\n\nconst AuthContext = React.createContext<AuthContext | null>(null)\n\nexport function AuthProvider({ children }: { children: React.ReactNode }) {\n const [user, setUser] = useState<User | null>(null)\n const [isLoading, setIsLoading] = useState(true)\n const [error, setError] = useState<string | null>(null)\n const [expiresAt, setExpiresAt] = useState<string | null>(null)\n\n // Redirect to login, optionally with return URL\n const redirectToLogin = (saveReturnUrl: boolean = false) => {\n if (window.location.pathname !== \"/\") {\n const returnUrl = saveReturnUrl\n ? `/?redirect=${encodeURIComponent(window.location.pathname + window.location.search)}`\n : \"/\"\n window.location.replace(returnUrl)\n }\n }\n\n // Clear local session state\n const clearLocalSession = () => {\n setUser(null)\n setExpiresAt(null)\n }\n\n // Get current session on mount\n useEffect(() => {\n trpcClient.auth.getCurrentUserSession\n .query()\n .then((session) => {\n if (session) {\n setUser(session.user)\n setExpiresAt(session.expires_at ?? null)\n }\n })\n .catch((err) => {\n setError(err instanceof Error ? err.message : \"Could not load session\")\n })\n .finally(() => setIsLoading(false))\n }, [])\n\n // Auto-logout when session expires\n useEffect(() => {\n if (!user || !expiresAt) return\n\n const timeUntilExpiry = new Date(expiresAt).getTime() - Date.now()\n\n const handleExpiry = async () => {\n setIsLoading(true)\n try {\n await trpcClient.auth.terminateUserSession.mutate()\n } catch (err) {\n // Server invalidation failed - show error\n setError(err instanceof Error ? err.message : `Session termination failed: ${err}`)\n } finally {\n clearLocalSession()\n setIsLoading(false)\n redirectToLogin(true)\n }\n }\n\n if (timeUntilExpiry <= 0) {\n handleExpiry()\n return\n }\n\n const timer = setTimeout(handleExpiry, timeUntilExpiry)\n return () => clearTimeout(timer)\n }, [user, expiresAt])\n\n const login = async ({ domain, user, password }: { domain: string; user: string; password: string }) => {\n setError(null)\n setIsLoading(true)\n try {\n const session = await trpcClient.auth.createUserSession.mutate({ domainName: domain, user, password })\n setUser(session.user)\n setExpiresAt(session.expires_at)\n return { success: true }\n } catch (err) {\n setError(err instanceof Error ? err.message : `Could not create session: ${err}`)\n return { success: false }\n } finally {\n setIsLoading(false)\n }\n }\n\n const logout = async () => {\n setIsLoading(true)\n setError(null)\n try {\n await trpcClient.auth.terminateUserSession.mutate()\n } catch (err) {\n // Server invalidation failed - show error\n setError(err instanceof Error ? err.message : `Logout failed: ${err}`)\n } finally {\n clearLocalSession()\n setIsLoading(false)\n redirectToLogin(false) // Don't save return URL on manual logout\n }\n }\n\n return (\n <AuthContext.Provider\n value={{\n user,\n isAuthenticated: !!user,\n isLoading,\n error,\n login,\n logout,\n expiresAt: expiresAt ? new Date(expiresAt) : undefined,\n }}\n >\n {children}\n </AuthContext.Provider>\n )\n}\n\nexport function useAuth() {\n const context = React.useContext(AuthContext)\n if (!context) {\n throw new Error(\"useAuth must be used within an AuthProvider\")\n }\n return context\n}\n"],"mappings":"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"}
@@ -1,32 +1,32 @@
1
- import { $ as e, B as t, X as n, Y as r, _ as i, d as a, nt as o } from "./build-C_4vRMCq.mjs";
1
+ import { H as e, Q as t, Z as n, _ as r, at as i, d as a, tt as o } from "./build-WuxXRAqf.mjs";
2
2
  import { t as s } from "./Slot-CWb612-_.mjs";
3
- import { t as c } from "./routeInfo-DXqB2HCf.mjs";
3
+ import { t as c } from "./routeInfo-DP3v74Wg.mjs";
4
4
  import { jsx as l, jsxs as u } from "react/jsx-runtime";
5
5
  import { useState as d } from "react";
6
6
  import { useMatches as f, useParams as p, useRouteContext as m } from "@tanstack/react-router";
7
7
  import { Trans as h, useLingui as g } from "@lingui/react";
8
8
  //#region src/client/components/ClipboardText.tsx
9
- var _ = ({ text: i, tooltipContent: o, className: s, truncateAt: c, showTooltip: f = !0, ...p }) => {
9
+ var _ = ({ text: r, tooltipContent: i, className: s, truncateAt: c, showTooltip: f = !0, ...p }) => {
10
10
  let { i18n: m, _: h } = g(), [_, v] = d(!1), [y, b] = d(!1), x = `copyableTooltip inline-flex items-center ${s || ""}`, S = async (e) => {
11
11
  e.preventDefault(), e.stopPropagation();
12
12
  try {
13
- await navigator.clipboard.writeText(i), v(!0), b(!1), setTimeout(() => v(!1), 2e3);
13
+ await navigator.clipboard.writeText(r), v(!0), b(!1), setTimeout(() => v(!1), 2e3);
14
14
  } catch (e) {
15
15
  console.error("Failed to copy text:", e);
16
16
  }
17
- }, C = c && i.length > c ? `${i.slice(0, c)}...` : i, w = () => _ ? o || m._({ id: "u+VWhB" }) : m._({ id: "he3ygx" }), T = _ && f || y && f;
17
+ }, C = c && r.length > c ? `${r.slice(0, c)}...` : r, w = () => _ ? i || m._({ id: "u+VWhB" }) : m._({ id: "he3ygx" }), T = _ && f || y && f;
18
18
  return /*#__PURE__*/ l("div", {
19
19
  ...p,
20
20
  className: x,
21
- children: /*#__PURE__*/ u(r, {
21
+ children: /*#__PURE__*/ u(n, {
22
22
  open: T,
23
- children: [/*#__PURE__*/ l(e, {
23
+ children: [/*#__PURE__*/ l(o, {
24
24
  onClick: S,
25
25
  onMouseEnter: () => !_ && b(!0),
26
26
  onMouseLeave: () => b(!1),
27
27
  "aria-label": m._({
28
28
  id: "Wbg1jv",
29
- values: { text: i }
29
+ values: { text: r }
30
30
  }),
31
31
  className: "cursor-pointer",
32
32
  asChild: !0,
@@ -34,7 +34,7 @@ var _ = ({ text: i, tooltipContent: o, className: s, truncateAt: c, showTooltip:
34
34
  children: /*#__PURE__*/ l("button", {
35
35
  type: "button",
36
36
  className: "group border-none bg-transparent p-0 text-inherit",
37
- children: /*#__PURE__*/ u(n, {
37
+ children: /*#__PURE__*/ u(t, {
38
38
  direction: "horizontal",
39
39
  gap: "1",
40
40
  className: "items-center hover:underline",
@@ -47,13 +47,13 @@ var _ = ({ text: i, tooltipContent: o, className: s, truncateAt: c, showTooltip:
47
47
  })]
48
48
  })
49
49
  })
50
- }), /*#__PURE__*/ l(t, { children: w() })]
50
+ }), /*#__PURE__*/ l(e, { children: w() })]
51
51
  })
52
52
  });
53
53
  };
54
54
  //#endregion
55
55
  //#region src/client/components/ContentHeader/ContentHeader.tsx
56
- function v({ title: e, projectId: t, description: n, actions: r, badges: a }) {
56
+ function v({ title: e, projectId: t, description: n, actions: a, badges: o }) {
57
57
  let { slots: d } = m({ strict: !1 }), g = f(), { provider: v } = p({ strict: !1 }), y = [...g].reverse().find((e) => c(e.staticData)), b = y && c(y.staticData) ? y.staticData.service : void 0, x = b === "containers" && v === "ceph" ? "ceph-containers" : b, S = d?.servicePageActions && x ? /*#__PURE__*/ l(s, {
58
58
  component: d.servicePageActions,
59
59
  useShadowDOM: !1,
@@ -73,7 +73,7 @@ function v({ title: e, projectId: t, description: n, actions: r, badges: a }) {
73
73
  className: "flex flex-col sm:flex-row sm:items-center sm:justify-between",
74
74
  children: [/*#__PURE__*/ u("div", {
75
75
  className: "flex items-center gap-3",
76
- children: [/*#__PURE__*/ l(o, { children: e }), S]
76
+ children: [/*#__PURE__*/ l(i, { children: e }), S]
77
77
  }), /*#__PURE__*/ u("div", {
78
78
  className: "text-theme-light flex shrink-0 items-center gap-1 text-sm",
79
79
  children: [/*#__PURE__*/ u("span", {
@@ -95,15 +95,15 @@ function v({ title: e, projectId: t, description: n, actions: r, badges: a }) {
95
95
  }),
96
96
  C,
97
97
  w,
98
- /*#__PURE__*/ l(i, { className: "mt-4" }),
99
- (a || r) && /*#__PURE__*/ u("div", {
98
+ /*#__PURE__*/ l(r, { className: "mt-4" }),
99
+ (o || a) && /*#__PURE__*/ u("div", {
100
100
  className: "mt-3 flex items-start justify-between",
101
101
  children: [/*#__PURE__*/ u("div", {
102
102
  className: "flex items-center gap-2",
103
- children: [a, " "]
104
- }), r && /*#__PURE__*/ l("div", {
103
+ children: [o, " "]
104
+ }), a && /*#__PURE__*/ l("div", {
105
105
  className: "flex gap-2",
106
- children: r
106
+ children: a
107
107
  })]
108
108
  })
109
109
  ]
@@ -112,4 +112,4 @@ function v({ title: e, projectId: t, description: n, actions: r, badges: a }) {
112
112
  //#endregion
113
113
  export { _ as n, v as t };
114
114
 
115
- //# sourceMappingURL=ContentHeader-BHMUzd9H.mjs.map
115
+ //# sourceMappingURL=ContentHeader-CPJbgYWn.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"ContentHeader-BHMUzd9H.mjs","names":["React","useState","Tooltip","TooltipTrigger","TooltipContent","Icon","Stack","ClipboardText","text","tooltipContent","className","truncateAt","showTooltip","props","useLingui","copied","setCopied","isHovering","setIsHovering","combinedClassName","handleCopy","e","preventDefault","stopPropagation","navigator","clipboard","writeText","setTimeout","err","console","error","displayText","length","slice","getTooltipContent","t","tooltipIsOpen","div","open","onClick","onMouseEnter","onMouseLeave","aria-label","asChild","data-testid","button","type","direction","gap","span","icon","size","Divider","ContentHeading","useRouteContext","useMatches","useParams","ClipboardText","Slot","isRouteInfo","ContentHeader","title","projectId","description","actions","badges","slots","strict","matches","provider","activeMatch","reverse","find","m","staticData","routeService","service","undefined","currentService","slotActions","servicePageActions","component","useShadowDOM","serviceBanner","projectOverviewBanner","header","className","div","span","text","truncateAt","p"],"sources":["../../src/client/components/ClipboardText.tsx","../../src/client/components/ContentHeader/ContentHeader.tsx"],"sourcesContent":["import React, { useState } from \"react\"\nimport { Tooltip, TooltipTrigger, TooltipContent, Icon, Stack } from \"@cloudoperators/juno-ui-components\"\nimport { useLingui } from \"@lingui/react/macro\"\n\nexport interface ClipboardTextProps extends React.HTMLAttributes<HTMLDivElement> {\n tooltipContent?: string\n text: string\n className?: string\n truncateAt?: number\n showTooltip?: boolean\n}\n\nconst ClipboardText: React.FC<ClipboardTextProps> = ({\n text,\n tooltipContent,\n className,\n truncateAt,\n showTooltip = true,\n ...props\n}) => {\n const { t } = useLingui()\n const [copied, setCopied] = useState(false)\n const [isHovering, setIsHovering] = useState(false)\n\n const combinedClassName = `copyableTooltip inline-flex items-center ${className || \"\"}`\n\n const handleCopy = async (e: React.MouseEvent) => {\n e.preventDefault()\n e.stopPropagation()\n\n try {\n await navigator.clipboard.writeText(text)\n setCopied(true)\n setIsHovering(false)\n setTimeout(() => setCopied(false), 2000)\n } catch (err) {\n console.error(\"Failed to copy text:\", err)\n }\n }\n\n const displayText = truncateAt && text.length > truncateAt ? `${text.slice(0, truncateAt)}...` : text\n\n // Determine tooltip content based on state\n const getTooltipContent = () => {\n if (copied) {\n return tooltipContent || t`Copied to clipboard!`\n }\n return t`Copy`\n }\n\n const tooltipIsOpen = (copied && showTooltip) || (isHovering && showTooltip)\n\n return (\n <div {...props} className={combinedClassName}>\n <Tooltip open={tooltipIsOpen}>\n <TooltipTrigger\n onClick={handleCopy}\n onMouseEnter={() => !copied && setIsHovering(true)}\n onMouseLeave={() => setIsHovering(false)}\n aria-label={t`Copy ${text} to clipboard`}\n className=\"cursor-pointer\"\n asChild\n data-testid=\"clipboard-copy-trigger\"\n >\n <button type=\"button\" className=\"group border-none bg-transparent p-0 text-inherit\">\n <Stack direction=\"horizontal\" gap=\"1\" className=\"items-center hover:underline\">\n <span className=\"select-none\">{displayText}</span>\n <Icon icon={copied ? \"check\" : \"contentCopy\"} size=\"18\" />\n </Stack>\n </button>\n </TooltipTrigger>\n <TooltipContent>{getTooltipContent()}</TooltipContent>\n </Tooltip>\n </div>\n )\n}\n\nexport default ClipboardText\n","import type { ReactNode } from \"react\"\nimport { Divider, ContentHeading } from \"@cloudoperators/juno-ui-components\"\nimport { Trans } from \"@lingui/react/macro\"\nimport { useRouteContext, useMatches, useParams } from \"@tanstack/react-router\"\nimport ClipboardText from \"../ClipboardText\"\nimport { Slot } from \"../Slot\"\nimport { isRouteInfo } from \"@/client/routes/routeInfo\"\n\ninterface ContentHeaderProps {\n title: string\n projectId: string\n description?: string | null\n actions?: ReactNode\n badges?: ReactNode\n}\n\nexport function ContentHeader({ title, projectId, description, actions, badges }: ContentHeaderProps) {\n const { slots } = useRouteContext({ strict: false })\n const matches = useMatches()\n const { provider } = useParams({ strict: false }) as { provider?: string }\n\n const activeMatch = [...matches].reverse().find((m) => isRouteInfo(m.staticData))\n const routeService = activeMatch && isRouteInfo(activeMatch.staticData) ? activeMatch.staticData.service : undefined\n\n // Storage routes share service: \"containers\" for both Swift and Ceph.\n // Distinguish them by the $provider param.\n const currentService = routeService === \"containers\" && provider === \"ceph\" ? \"ceph-containers\" : routeService\n\n const slotActions =\n slots?.servicePageActions && currentService ? (\n <Slot component={slots.servicePageActions} useShadowDOM={false} currentService={currentService} />\n ) : null\n\n const serviceBanner =\n slots?.serviceBanner && currentService ? (\n <Slot component={slots.serviceBanner} useShadowDOM={false} currentService={currentService} />\n ) : null\n\n const projectOverviewBanner =\n slots?.projectOverviewBanner && !currentService ? (\n <Slot component={slots.projectOverviewBanner} useShadowDOM={false} />\n ) : null\n\n return (\n <header className=\"mb-8\">\n <div className=\"flex flex-col sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"flex items-center gap-3\">\n <ContentHeading>{title}</ContentHeading>\n {slotActions}\n </div>\n <div className=\"text-theme-light flex shrink-0 items-center gap-1 text-sm\">\n <span className=\"font-semibold\">\n <Trans>Project ID</Trans>:{\" \"}\n </span>\n <ClipboardText text={projectId} truncateAt={15} />\n </div>\n </div>\n {description && <p className=\"text-sm font-normal\">{description}</p>}\n {serviceBanner}\n {projectOverviewBanner}\n <Divider className=\"mt-4\" />\n {(badges || actions) && (\n <div className=\"mt-3 flex items-start justify-between\">\n <div className=\"flex items-center gap-2\">{badges} </div>\n {actions && <div className=\"flex gap-2\">{actions}</div>}\n </div>\n )}\n </header>\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ContentHeader-CPJbgYWn.mjs","names":["React","useState","Tooltip","TooltipTrigger","TooltipContent","Icon","Stack","ClipboardText","text","tooltipContent","className","truncateAt","showTooltip","props","useLingui","copied","setCopied","isHovering","setIsHovering","combinedClassName","handleCopy","e","preventDefault","stopPropagation","navigator","clipboard","writeText","setTimeout","err","console","error","displayText","length","slice","getTooltipContent","t","tooltipIsOpen","div","open","onClick","onMouseEnter","onMouseLeave","aria-label","asChild","data-testid","button","type","direction","gap","span","icon","size","Divider","ContentHeading","useRouteContext","useMatches","useParams","ClipboardText","Slot","isRouteInfo","ContentHeader","title","projectId","description","actions","badges","slots","strict","matches","provider","activeMatch","reverse","find","m","staticData","routeService","service","undefined","currentService","slotActions","servicePageActions","component","useShadowDOM","serviceBanner","projectOverviewBanner","header","className","div","span","text","truncateAt","p"],"sources":["../../src/client/components/ClipboardText.tsx","../../src/client/components/ContentHeader/ContentHeader.tsx"],"sourcesContent":["import React, { useState } from \"react\"\nimport { Tooltip, TooltipTrigger, TooltipContent, Icon, Stack } from \"@cloudoperators/juno-ui-components\"\nimport { useLingui } from \"@lingui/react/macro\"\n\nexport interface ClipboardTextProps extends React.HTMLAttributes<HTMLDivElement> {\n tooltipContent?: string\n text: string\n className?: string\n truncateAt?: number\n showTooltip?: boolean\n}\n\nconst ClipboardText: React.FC<ClipboardTextProps> = ({\n text,\n tooltipContent,\n className,\n truncateAt,\n showTooltip = true,\n ...props\n}) => {\n const { t } = useLingui()\n const [copied, setCopied] = useState(false)\n const [isHovering, setIsHovering] = useState(false)\n\n const combinedClassName = `copyableTooltip inline-flex items-center ${className || \"\"}`\n\n const handleCopy = async (e: React.MouseEvent) => {\n e.preventDefault()\n e.stopPropagation()\n\n try {\n await navigator.clipboard.writeText(text)\n setCopied(true)\n setIsHovering(false)\n setTimeout(() => setCopied(false), 2000)\n } catch (err) {\n console.error(\"Failed to copy text:\", err)\n }\n }\n\n const displayText = truncateAt && text.length > truncateAt ? `${text.slice(0, truncateAt)}...` : text\n\n // Determine tooltip content based on state\n const getTooltipContent = () => {\n if (copied) {\n return tooltipContent || t`Copied to clipboard!`\n }\n return t`Copy`\n }\n\n const tooltipIsOpen = (copied && showTooltip) || (isHovering && showTooltip)\n\n return (\n <div {...props} className={combinedClassName}>\n <Tooltip open={tooltipIsOpen}>\n <TooltipTrigger\n onClick={handleCopy}\n onMouseEnter={() => !copied && setIsHovering(true)}\n onMouseLeave={() => setIsHovering(false)}\n aria-label={t`Copy ${text} to clipboard`}\n className=\"cursor-pointer\"\n asChild\n data-testid=\"clipboard-copy-trigger\"\n >\n <button type=\"button\" className=\"group border-none bg-transparent p-0 text-inherit\">\n <Stack direction=\"horizontal\" gap=\"1\" className=\"items-center hover:underline\">\n <span className=\"select-none\">{displayText}</span>\n <Icon icon={copied ? \"check\" : \"contentCopy\"} size=\"18\" />\n </Stack>\n </button>\n </TooltipTrigger>\n <TooltipContent>{getTooltipContent()}</TooltipContent>\n </Tooltip>\n </div>\n )\n}\n\nexport default ClipboardText\n","import type { ReactNode } from \"react\"\nimport { Divider, ContentHeading } from \"@cloudoperators/juno-ui-components\"\nimport { Trans } from \"@lingui/react/macro\"\nimport { useRouteContext, useMatches, useParams } from \"@tanstack/react-router\"\nimport ClipboardText from \"../ClipboardText\"\nimport { Slot } from \"../Slot\"\nimport { isRouteInfo } from \"@/client/routes/routeInfo\"\n\ninterface ContentHeaderProps {\n title: string\n projectId: string\n description?: string | null\n actions?: ReactNode\n badges?: ReactNode\n}\n\nexport function ContentHeader({ title, projectId, description, actions, badges }: ContentHeaderProps) {\n const { slots } = useRouteContext({ strict: false })\n const matches = useMatches()\n const { provider } = useParams({ strict: false }) as { provider?: string }\n\n const activeMatch = [...matches].reverse().find((m) => isRouteInfo(m.staticData))\n const routeService = activeMatch && isRouteInfo(activeMatch.staticData) ? activeMatch.staticData.service : undefined\n\n // Storage routes share service: \"containers\" for both Swift and Ceph.\n // Distinguish them by the $provider param.\n const currentService = routeService === \"containers\" && provider === \"ceph\" ? \"ceph-containers\" : routeService\n\n const slotActions =\n slots?.servicePageActions && currentService ? (\n <Slot component={slots.servicePageActions} useShadowDOM={false} currentService={currentService} />\n ) : null\n\n const serviceBanner =\n slots?.serviceBanner && currentService ? (\n <Slot component={slots.serviceBanner} useShadowDOM={false} currentService={currentService} />\n ) : null\n\n const projectOverviewBanner =\n slots?.projectOverviewBanner && !currentService ? (\n <Slot component={slots.projectOverviewBanner} useShadowDOM={false} />\n ) : null\n\n return (\n <header className=\"mb-8\">\n <div className=\"flex flex-col sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"flex items-center gap-3\">\n <ContentHeading>{title}</ContentHeading>\n {slotActions}\n </div>\n <div className=\"text-theme-light flex shrink-0 items-center gap-1 text-sm\">\n <span className=\"font-semibold\">\n <Trans>Project ID</Trans>:{\" \"}\n </span>\n <ClipboardText text={projectId} truncateAt={15} />\n </div>\n </div>\n {description && <p className=\"text-sm font-normal\">{description}</p>}\n {serviceBanner}\n {projectOverviewBanner}\n <Divider className=\"mt-4\" />\n {(badges || actions) && (\n <div className=\"mt-3 flex items-start justify-between\">\n <div className=\"flex items-center gap-2\">{badges} </div>\n {actions && <div className=\"flex gap-2\">{actions}</div>}\n </div>\n )}\n </header>\n )\n}\n"],"mappings":"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"}