@backstage/plugin-app 0.4.0-next.1 → 0.4.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 (24) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/apis/PluginHeaderActionsApi/DefaultPluginHeaderActionsApi.esm.js +24 -0
  3. package/dist/apis/PluginHeaderActionsApi/DefaultPluginHeaderActionsApi.esm.js.map +1 -0
  4. package/dist/extensions/AppNav.esm.js +102 -17
  5. package/dist/extensions/AppNav.esm.js.map +1 -1
  6. package/dist/extensions/AppRoot.esm.js +11 -1
  7. package/dist/extensions/AppRoot.esm.js.map +1 -1
  8. package/dist/extensions/PluginHeaderActionsApi.esm.js +28 -0
  9. package/dist/extensions/PluginHeaderActionsApi.esm.js.map +1 -0
  10. package/dist/extensions/components.esm.js +36 -3
  11. package/dist/extensions/components.esm.js.map +1 -1
  12. package/dist/index.d.ts +38 -1
  13. package/dist/packages/core-app-api/src/apis/implementations/AppLanguageApi/AppLanguageSelector.esm.js +24 -3
  14. package/dist/packages/core-app-api/src/apis/implementations/AppLanguageApi/AppLanguageSelector.esm.js.map +1 -1
  15. package/dist/packages/core-app-api/src/apis/implementations/AppThemeApi/AppThemeSelector.esm.js +24 -3
  16. package/dist/packages/core-app-api/src/apis/implementations/AppThemeApi/AppThemeSelector.esm.js.map +1 -1
  17. package/dist/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.esm.js +33 -2
  18. package/dist/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.esm.js.map +1 -1
  19. package/dist/packages/frontend-internal/src/wiring/InternalFrontendPlugin.esm.js.map +1 -1
  20. package/dist/plugin.esm.js +4 -1
  21. package/dist/plugin.esm.js.map +1 -1
  22. package/dist/plugins/app/package.json.esm.js +4 -3
  23. package/dist/plugins/app/package.json.esm.js.map +1 -1
  24. package/package.json +18 -17
package/CHANGELOG.md CHANGED
@@ -1,5 +1,51 @@
1
1
  # @backstage/plugin-app
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - ef6916e: Added `SubPageBlueprint` for creating sub-page tabs, `PluginHeaderActionBlueprint` and `PluginHeaderActionsApi` for plugin-scoped header actions, and `PageLayout` as a swappable component. The `PageBlueprint` now supports sub-pages with tabbed navigation, page title, icon, and header actions. Plugins can now specify a `title` and `icon` in `createFrontendPlugin`.
8
+ - 7edb810: **BREAKING**: Extensions created with the following blueprints must now be provided via an override or a module for the `app` plugin. Extensions from other plugins will now trigger a warning in the app and be ignored.
9
+
10
+ - `IconBundleBlueprint`
11
+ - `NavContentBlueprint`
12
+ - `RouterBlueprint`
13
+ - `SignInPageBlueprint`
14
+ - `SwappableComponentBlueprint`
15
+ - `ThemeBlueprint`
16
+ - `TranslationBlueprint`
17
+
18
+ ### Patch Changes
19
+
20
+ - a2133be: Added new `NavContentNavItem`, `NavContentNavItems`, and `navItems` prop to `NavContentComponentProps` for auto-discovering navigation items from page extensions. The new `navItems` collection supports `take(id)` and `rest()` methods for placing specific items in custom sidebar positions, as well as `withComponent(Component)` which returns a `NavContentNavItemsWithComponent` for rendering items directly as elements. The existing `items` prop is now deprecated in favor of `navItems`.
21
+ - a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings.
22
+ - 69d880e: Bump to latest zod to ensure it has the latest features
23
+ - Updated dependencies
24
+ - @backstage/ui@0.12.0
25
+ - @backstage/core-components@0.18.7
26
+ - @backstage/theme@0.7.2
27
+ - @backstage/frontend-plugin-api@0.14.0
28
+ - @backstage/plugin-app-react@0.2.0
29
+ - @backstage/core-plugin-api@1.12.3
30
+ - @backstage/integration-react@1.2.15
31
+ - @backstage/plugin-permission-react@0.4.40
32
+ - @backstage/version-bridge@1.0.12
33
+
34
+ ## 0.4.0-next.2
35
+
36
+ ### Patch Changes
37
+
38
+ - a7e0d50: Prepare for React Router v7 migration by updating to v6.30.2 across all NFS packages and enabling v7 future flags. Convert routes from splat paths to parent/child structure with Outlet components.
39
+ - Updated dependencies
40
+ - @backstage/frontend-plugin-api@0.14.0-next.2
41
+ - @backstage/integration-react@1.2.15-next.2
42
+ - @backstage/core-components@0.18.7-next.2
43
+ - @backstage/core-plugin-api@1.12.3-next.1
44
+ - @backstage/plugin-permission-react@0.4.40-next.1
45
+ - @backstage/version-bridge@1.0.12-next.0
46
+ - @backstage/theme@0.7.2-next.1
47
+ - @backstage/plugin-app-react@0.1.1-next.0
48
+
3
49
  ## 0.4.0-next.1
4
50
 
5
51
  ### Patch Changes
@@ -0,0 +1,24 @@
1
+ const EMPTY_ACTIONS = new Array();
2
+ class DefaultPluginHeaderActionsApi {
3
+ constructor(actionsByPlugin) {
4
+ this.actionsByPlugin = actionsByPlugin;
5
+ }
6
+ getPluginHeaderActions(pluginId) {
7
+ return this.actionsByPlugin.get(pluginId) ?? EMPTY_ACTIONS;
8
+ }
9
+ static fromActions(actions) {
10
+ const actionsByPlugin = /* @__PURE__ */ new Map();
11
+ for (const action of actions) {
12
+ let pluginActions = actionsByPlugin.get(action.pluginId);
13
+ if (!pluginActions) {
14
+ pluginActions = [];
15
+ actionsByPlugin.set(action.pluginId, pluginActions);
16
+ }
17
+ pluginActions.push(action.element);
18
+ }
19
+ return new DefaultPluginHeaderActionsApi(actionsByPlugin);
20
+ }
21
+ }
22
+
23
+ export { DefaultPluginHeaderActionsApi };
24
+ //# sourceMappingURL=DefaultPluginHeaderActionsApi.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DefaultPluginHeaderActionsApi.esm.js","sources":["../../../src/apis/PluginHeaderActionsApi/DefaultPluginHeaderActionsApi.tsx"],"sourcesContent":["/*\n * Copyright 2026 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { JSX } from 'react';\nimport { type PluginHeaderActionsApi } from '@backstage/frontend-plugin-api';\n\n// Stable reference\nconst EMPTY_ACTIONS = new Array<JSX.Element | null>();\n\ntype ActionInput = {\n element: JSX.Element;\n pluginId: string;\n};\n\n/**\n * Default implementation of PluginHeaderActionsApi.\n *\n * @internal\n */\nexport class DefaultPluginHeaderActionsApi implements PluginHeaderActionsApi {\n constructor(\n private readonly actionsByPlugin: Map<string, Array<JSX.Element | null>>,\n ) {}\n\n getPluginHeaderActions(pluginId: string): Array<JSX.Element | null> {\n return this.actionsByPlugin.get(pluginId) ?? EMPTY_ACTIONS;\n }\n\n static fromActions(\n actions: Array<ActionInput>,\n ): DefaultPluginHeaderActionsApi {\n const actionsByPlugin = new Map<string, Array<JSX.Element | null>>();\n\n for (const action of actions) {\n let pluginActions = actionsByPlugin.get(action.pluginId);\n if (!pluginActions) {\n pluginActions = [];\n actionsByPlugin.set(action.pluginId, pluginActions);\n }\n\n pluginActions.push(action.element);\n }\n\n return new DefaultPluginHeaderActionsApi(actionsByPlugin);\n }\n}\n"],"names":[],"mappings":"AAoBA,MAAM,aAAA,GAAgB,IAAI,KAAA,EAA0B;AAY7C,MAAM,6BAAA,CAAgE;AAAA,EAC3E,YACmB,eAAA,EACjB;AADiB,IAAA,IAAA,CAAA,eAAA,GAAA,eAAA;AAAA,EAChB;AAAA,EAEH,uBAAuB,QAAA,EAA6C;AAClE,IAAA,OAAO,IAAA,CAAK,eAAA,CAAgB,GAAA,CAAI,QAAQ,CAAA,IAAK,aAAA;AAAA,EAC/C;AAAA,EAEA,OAAO,YACL,OAAA,EAC+B;AAC/B,IAAA,MAAM,eAAA,uBAAsB,GAAA,EAAuC;AAEnE,IAAA,KAAA,MAAW,UAAU,OAAA,EAAS;AAC5B,MAAA,IAAI,aAAA,GAAgB,eAAA,CAAgB,GAAA,CAAI,MAAA,CAAO,QAAQ,CAAA;AACvD,MAAA,IAAI,CAAC,aAAA,EAAe;AAClB,QAAA,aAAA,GAAgB,EAAC;AACjB,QAAA,eAAA,CAAgB,GAAA,CAAI,MAAA,CAAO,QAAA,EAAU,aAAa,CAAA;AAAA,MACpD;AAEA,MAAA,aAAA,CAAc,IAAA,CAAK,OAAO,OAAO,CAAA;AAAA,IACnC;AAEA,IAAA,OAAO,IAAI,8BAA8B,eAAe,CAAA;AAAA,EAC1D;AACF;;;;"}
@@ -1,31 +1,74 @@
1
1
  import { jsx } from 'react/jsx-runtime';
2
- import { createExtension, coreExtensionData, createExtensionInput, NavItemBlueprint, useApi, routeResolutionApiRef } from '@backstage/frontend-plugin-api';
2
+ import { createExtension, coreExtensionData, createExtensionInput, NavItemBlueprint, useApi, appTreeApiRef, routeResolutionApiRef } from '@backstage/frontend-plugin-api';
3
3
  import { NavContentBlueprint } from '@backstage/plugin-app-react';
4
4
  import { Sidebar, SidebarItem } from '@backstage/core-components';
5
5
  import { useMemo } from 'react';
6
6
 
7
+ class NavItemBag {
8
+ #items;
9
+ #index;
10
+ #taken;
11
+ constructor(items, taken) {
12
+ this.#items = items;
13
+ this.#index = new Map(items.map((item) => [item.node.spec.id, item]));
14
+ this.#taken = new Set(taken);
15
+ }
16
+ take(id) {
17
+ const item = this.#index.get(id);
18
+ if (item) {
19
+ this.#taken.add(id);
20
+ }
21
+ return item;
22
+ }
23
+ rest() {
24
+ return this.#items.filter((item) => !this.#taken.has(item.node.spec.id));
25
+ }
26
+ clone() {
27
+ return new NavItemBag(this.#items, this.#taken);
28
+ }
29
+ withComponent(Component) {
30
+ return {
31
+ take: (id) => {
32
+ const item = this.take(id);
33
+ return item ? /* @__PURE__ */ jsx(Component, { ...item }) : null;
34
+ },
35
+ rest: (options) => {
36
+ const items = this.rest();
37
+ if (options?.sortBy === "title") {
38
+ items.sort((a, b) => a.title.localeCompare(b.title));
39
+ }
40
+ return items.map((item) => /* @__PURE__ */ jsx(Component, { ...item }, item.node.spec.id));
41
+ }
42
+ };
43
+ }
44
+ }
7
45
  function DefaultNavContent(props) {
8
- return /* @__PURE__ */ jsx(Sidebar, { children: props.items.map((item, index) => /* @__PURE__ */ jsx(
46
+ const items = props.navItems.rest();
47
+ return /* @__PURE__ */ jsx(Sidebar, { children: items.map((item) => /* @__PURE__ */ jsx(
9
48
  SidebarItem,
10
49
  {
11
- to: item.to,
12
- icon: item.icon,
13
- text: item.text
50
+ to: item.href,
51
+ icon: () => item.icon,
52
+ text: item.title
14
53
  },
15
- index
54
+ item.node.spec.id
16
55
  )) });
17
56
  }
57
+ function tryResolveLink(routeResolutionApi, routeRef) {
58
+ try {
59
+ const link = routeResolutionApi.resolve(routeRef);
60
+ return link?.();
61
+ } catch {
62
+ return void 0;
63
+ }
64
+ }
18
65
  function NavContentRenderer(props) {
66
+ const appTreeApi = useApi(appTreeApiRef);
19
67
  const routeResolutionApi = useApi(routeResolutionApiRef);
20
- const items = useMemo(() => {
21
- return props.items.flatMap((item) => {
68
+ const legacyItems = useMemo(() => {
69
+ return props.legacyNavItems.flatMap((item) => {
22
70
  const link = routeResolutionApi.resolve(item.routeRef);
23
- if (!link) {
24
- console.warn(
25
- `NavItemBlueprint: unable to resolve route ref ${item.routeRef}`
26
- );
27
- return [];
28
- }
71
+ if (!link) return [];
29
72
  return [
30
73
  {
31
74
  to: link(),
@@ -36,8 +79,50 @@ function NavContentRenderer(props) {
36
79
  }
37
80
  ];
38
81
  });
39
- }, [props.items, routeResolutionApi]);
40
- return /* @__PURE__ */ jsx(props.Content, { items });
82
+ }, [props.legacyNavItems, routeResolutionApi]);
83
+ const navItems = useMemo(() => {
84
+ const { tree } = appTreeApi.getTree();
85
+ const routesNode = tree.nodes.get("app/routes");
86
+ if (!routesNode) return new NavItemBag([]);
87
+ const navItemsByRouteRef = new Map(props.legacyNavItems.map((item) => [item.routeRef, item]));
88
+ const pageNodes = routesNode.edges.attachments.get("routes") ?? [];
89
+ const items = pageNodes.flatMap((node) => {
90
+ if (!node.instance || node.spec.disabled) {
91
+ return [];
92
+ }
93
+ const routeRef = node.instance.getData(coreExtensionData.routeRef);
94
+ if (!routeRef) {
95
+ return [];
96
+ }
97
+ const matchingNavItem = navItemsByRouteRef.get(routeRef);
98
+ const resolvedTitle = node.instance.getData(coreExtensionData.title);
99
+ const pluginTitle = node.spec.plugin.title;
100
+ const pluginId = node.spec.plugin.pluginId;
101
+ const hasExplicitPageTitle = resolvedTitle !== void 0 && resolvedTitle !== pluginTitle && resolvedTitle !== pluginId;
102
+ const title = hasExplicitPageTitle ? resolvedTitle : matchingNavItem?.title ?? pluginTitle ?? pluginId;
103
+ const resolvedIcon = node.instance.getData(coreExtensionData.icon);
104
+ const hasExplicitPageIcon = resolvedIcon && !node.spec.plugin.icon;
105
+ const NavItemIcon = matchingNavItem?.icon;
106
+ let icon;
107
+ if (hasExplicitPageIcon) {
108
+ icon = resolvedIcon;
109
+ } else if (NavItemIcon) {
110
+ icon = /* @__PURE__ */ jsx(NavItemIcon, {});
111
+ } else if (resolvedIcon) {
112
+ icon = resolvedIcon;
113
+ }
114
+ if (!title || !icon) {
115
+ return [];
116
+ }
117
+ const to = tryResolveLink(routeResolutionApi, routeRef);
118
+ if (!to) {
119
+ return [];
120
+ }
121
+ return [{ node, href: to, title, icon, routeRef }];
122
+ });
123
+ return new NavItemBag(items);
124
+ }, [appTreeApi, routeResolutionApi, props.legacyNavItems]);
125
+ return /* @__PURE__ */ jsx(props.Content, { navItems, items: legacyItems });
41
126
  }
42
127
  const AppNav = createExtension({
43
128
  name: "nav",
@@ -57,7 +142,7 @@ const AppNav = createExtension({
57
142
  /* @__PURE__ */ jsx(
58
143
  NavContentRenderer,
59
144
  {
60
- items: inputs.items.map(
145
+ legacyNavItems: inputs.items.map(
61
146
  (item) => item.get(NavItemBlueprint.dataRefs.target)
62
147
  ),
63
148
  Content
@@ -1 +1 @@
1
- {"version":3,"file":"AppNav.esm.js","sources":["../../src/extensions/AppNav.tsx"],"sourcesContent":["/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n createExtension,\n coreExtensionData,\n createExtensionInput,\n NavItemBlueprint,\n routeResolutionApiRef,\n IconComponent,\n RouteRef,\n useApi,\n} from '@backstage/frontend-plugin-api';\nimport {\n NavContentBlueprint,\n NavContentComponent,\n NavContentComponentProps,\n} from '@backstage/plugin-app-react';\nimport { Sidebar, SidebarItem } from '@backstage/core-components';\nimport { useMemo } from 'react';\n\nfunction DefaultNavContent(props: NavContentComponentProps) {\n return (\n <Sidebar>\n {props.items.map((item, index) => (\n <SidebarItem\n to={item.to}\n icon={item.icon}\n text={item.text}\n key={index}\n />\n ))}\n </Sidebar>\n );\n}\n\n// This helps defer rendering until the app is being rendered, which is needed\n// because the RouteResolutionApi can't be called until the app has been fully initialized.\nfunction NavContentRenderer(props: {\n Content: NavContentComponent;\n items: Array<{\n title: string;\n icon: IconComponent;\n routeRef: RouteRef<undefined>;\n }>;\n}) {\n const routeResolutionApi = useApi(routeResolutionApiRef);\n\n const items = useMemo(() => {\n return props.items.flatMap(item => {\n const link = routeResolutionApi.resolve(item.routeRef);\n if (!link) {\n // eslint-disable-next-line no-console\n console.warn(\n `NavItemBlueprint: unable to resolve route ref ${item.routeRef}`,\n );\n return [];\n }\n return [\n {\n to: link(),\n text: item.title,\n icon: item.icon,\n title: item.title,\n routeRef: item.routeRef,\n },\n ];\n });\n }, [props.items, routeResolutionApi]);\n\n return <props.Content items={items} />;\n}\n\nexport const AppNav = createExtension({\n name: 'nav',\n attachTo: { id: 'app/layout', input: 'nav' },\n inputs: {\n items: createExtensionInput([NavItemBlueprint.dataRefs.target]),\n content: createExtensionInput([NavContentBlueprint.dataRefs.component], {\n singleton: true,\n optional: true,\n internal: true,\n }),\n },\n output: [coreExtensionData.reactElement],\n *factory({ inputs }) {\n const Content =\n inputs.content?.get(NavContentBlueprint.dataRefs.component) ??\n DefaultNavContent;\n\n yield coreExtensionData.reactElement(\n <NavContentRenderer\n items={inputs.items.map(item =>\n item.get(NavItemBlueprint.dataRefs.target),\n )}\n Content={Content}\n />,\n );\n },\n});\n"],"names":[],"mappings":";;;;;;AAkCA,SAAS,kBAAkB,KAAA,EAAiC;AAC1D,EAAA,2BACG,OAAA,EAAA,EACE,QAAA,EAAA,KAAA,CAAM,MAAM,GAAA,CAAI,CAAC,MAAM,KAAA,qBACtB,GAAA;AAAA,IAAC,WAAA;AAAA,IAAA;AAAA,MACC,IAAI,IAAA,CAAK,EAAA;AAAA,MACT,MAAM,IAAA,CAAK,IAAA;AAAA,MACX,MAAM,IAAA,CAAK;AAAA,KAAA;AAAA,IACN;AAAA,GAER,CAAA,EACH,CAAA;AAEJ;AAIA,SAAS,mBAAmB,KAAA,EAOzB;AACD,EAAA,MAAM,kBAAA,GAAqB,OAAO,qBAAqB,CAAA;AAEvD,EAAA,MAAM,KAAA,GAAQ,QAAQ,MAAM;AAC1B,IAAA,OAAO,KAAA,CAAM,KAAA,CAAM,OAAA,CAAQ,CAAA,IAAA,KAAQ;AACjC,MAAA,MAAM,IAAA,GAAO,kBAAA,CAAmB,OAAA,CAAQ,IAAA,CAAK,QAAQ,CAAA;AACrD,MAAA,IAAI,CAAC,IAAA,EAAM;AAET,QAAA,OAAA,CAAQ,IAAA;AAAA,UACN,CAAA,8CAAA,EAAiD,KAAK,QAAQ,CAAA;AAAA,SAChE;AACA,QAAA,OAAO,EAAC;AAAA,MACV;AACA,MAAA,OAAO;AAAA,QACL;AAAA,UACE,IAAI,IAAA,EAAK;AAAA,UACT,MAAM,IAAA,CAAK,KAAA;AAAA,UACX,MAAM,IAAA,CAAK,IAAA;AAAA,UACX,OAAO,IAAA,CAAK,KAAA;AAAA,UACZ,UAAU,IAAA,CAAK;AAAA;AACjB,OACF;AAAA,IACF,CAAC,CAAA;AAAA,EACH,CAAA,EAAG,CAAC,KAAA,CAAM,KAAA,EAAO,kBAAkB,CAAC,CAAA;AAEpC,EAAA,uBAAO,GAAA,CAAC,KAAA,CAAM,OAAA,EAAN,EAAc,KAAA,EAAc,CAAA;AACtC;AAEO,MAAM,SAAS,eAAA,CAAgB;AAAA,EACpC,IAAA,EAAM,KAAA;AAAA,EACN,QAAA,EAAU,EAAE,EAAA,EAAI,YAAA,EAAc,OAAO,KAAA,EAAM;AAAA,EAC3C,MAAA,EAAQ;AAAA,IACN,OAAO,oBAAA,CAAqB,CAAC,gBAAA,CAAiB,QAAA,CAAS,MAAM,CAAC,CAAA;AAAA,IAC9D,SAAS,oBAAA,CAAqB,CAAC,mBAAA,CAAoB,QAAA,CAAS,SAAS,CAAA,EAAG;AAAA,MACtE,SAAA,EAAW,IAAA;AAAA,MACX,QAAA,EAAU,IAAA;AAAA,MACV,QAAA,EAAU;AAAA,KACX;AAAA,GACH;AAAA,EACA,MAAA,EAAQ,CAAC,iBAAA,CAAkB,YAAY,CAAA;AAAA,EACvC,CAAC,OAAA,CAAQ,EAAE,MAAA,EAAO,EAAG;AACnB,IAAA,MAAM,UACJ,MAAA,CAAO,OAAA,EAAS,IAAI,mBAAA,CAAoB,QAAA,CAAS,SAAS,CAAA,IAC1D,iBAAA;AAEF,IAAA,MAAM,iBAAA,CAAkB,YAAA;AAAA,sBACtB,GAAA;AAAA,QAAC,kBAAA;AAAA,QAAA;AAAA,UACC,KAAA,EAAO,OAAO,KAAA,CAAM,GAAA;AAAA,YAAI,CAAA,IAAA,KACtB,IAAA,CAAK,GAAA,CAAI,gBAAA,CAAiB,SAAS,MAAM;AAAA,WAC3C;AAAA,UACA;AAAA;AAAA;AACF,KACF;AAAA,EACF;AACF,CAAC;;;;"}
1
+ {"version":3,"file":"AppNav.esm.js","sources":["../../src/extensions/AppNav.tsx"],"sourcesContent":["/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n createExtension,\n coreExtensionData,\n createExtensionInput,\n NavItemBlueprint,\n routeResolutionApiRef,\n appTreeApiRef,\n IconComponent,\n IconElement,\n RouteRef,\n RouteResolutionApi,\n useApi,\n} from '@backstage/frontend-plugin-api';\nimport {\n NavContentBlueprint,\n NavContentComponent,\n NavContentComponentProps,\n NavContentNavItem,\n NavContentNavItems,\n} from '@backstage/plugin-app-react';\nimport { Sidebar, SidebarItem } from '@backstage/core-components';\nimport { useMemo } from 'react';\n\nclass NavItemBag implements NavContentNavItems {\n readonly #items: NavContentNavItem[];\n readonly #index: Map<string, NavContentNavItem>;\n readonly #taken: Set<string>;\n\n constructor(items: NavContentNavItem[], taken?: Iterable<string>) {\n this.#items = items;\n this.#index = new Map(items.map(item => [item.node.spec.id, item]));\n this.#taken = new Set(taken);\n }\n\n take(id: string): NavContentNavItem | undefined {\n const item = this.#index.get(id);\n if (item) {\n this.#taken.add(id);\n }\n return item;\n }\n\n rest(): NavContentNavItem[] {\n return this.#items.filter(item => !this.#taken.has(item.node.spec.id));\n }\n\n clone(): NavContentNavItems {\n return new NavItemBag(this.#items, this.#taken);\n }\n\n withComponent(Component: (props: NavContentNavItem) => JSX.Element) {\n return {\n take: (id: string) => {\n const item = this.take(id);\n return item ? <Component {...item} /> : null;\n },\n rest: (options?: { sortBy?: 'title' }) => {\n const items = this.rest();\n if (options?.sortBy === 'title') {\n items.sort((a, b) => a.title.localeCompare(b.title));\n }\n return items.map(item => (\n <Component key={item.node.spec.id} {...item} />\n ));\n },\n };\n }\n}\n\nfunction DefaultNavContent(props: NavContentComponentProps) {\n const items = props.navItems.rest();\n return (\n <Sidebar>\n {items.map(item => (\n <SidebarItem\n to={item.href}\n icon={() => item.icon}\n text={item.title}\n key={item.node.spec.id}\n />\n ))}\n </Sidebar>\n );\n}\n\n// Tries to resolve a routeRef to a link path, returning undefined if it\n// can't be resolved (e.g. parameterized routes).\nfunction tryResolveLink(\n routeResolutionApi: RouteResolutionApi,\n routeRef: RouteRef,\n): string | undefined {\n try {\n const link = routeResolutionApi.resolve(routeRef);\n return link?.();\n } catch {\n return undefined;\n }\n}\n\n// Defers rendering until the app is fully initialized so that APIs like\n// RouteResolutionApi and AppTreeApi are available.\nfunction NavContentRenderer(props: {\n Content: NavContentComponent;\n legacyNavItems: Array<{\n title: string;\n icon: IconComponent;\n routeRef: RouteRef<undefined>;\n }>;\n}) {\n const appTreeApi = useApi(appTreeApiRef);\n const routeResolutionApi = useApi(routeResolutionApiRef);\n\n // Deprecated items: just resolve nav item routeRefs to paths, no page discovery.\n const legacyItems = useMemo(() => {\n return props.legacyNavItems.flatMap(item => {\n const link = routeResolutionApi.resolve(item.routeRef);\n if (!link) return [];\n return [\n {\n to: link(),\n text: item.title,\n icon: item.icon,\n title: item.title,\n routeRef: item.routeRef,\n },\n ];\n });\n }, [props.legacyNavItems, routeResolutionApi]);\n\n // New navItems: discover pages from the extension tree, merged with nav items.\n const navItems = useMemo(() => {\n const { tree } = appTreeApi.getTree();\n const routesNode = tree.nodes.get('app/routes');\n if (!routesNode) return new NavItemBag([]);\n\n // Index nav items by routeRef for matching against pages\n const navItemsByRouteRef = new Map<\n RouteRef,\n { title: string; icon: IconComponent }\n >(props.legacyNavItems.map(item => [item.routeRef, item]));\n\n const pageNodes = routesNode.edges.attachments.get('routes') ?? [];\n const items = pageNodes.flatMap((node): NavContentNavItem[] => {\n if (!node.instance || node.spec.disabled) {\n return [];\n }\n\n const routeRef = node.instance.getData(coreExtensionData.routeRef);\n if (!routeRef) {\n return [];\n }\n\n const matchingNavItem = navItemsByRouteRef.get(routeRef);\n\n // PageBlueprint resolves title as: config.title ?? params.title ?? plugin.title ?? pluginId\n // We want the priority: page (config/params) -> nav item -> plugin -> pluginId\n const resolvedTitle = node.instance.getData(coreExtensionData.title);\n const pluginTitle = node.spec.plugin.title;\n const pluginId = node.spec.plugin.pluginId;\n const hasExplicitPageTitle =\n resolvedTitle !== undefined &&\n resolvedTitle !== pluginTitle &&\n resolvedTitle !== pluginId;\n const title = hasExplicitPageTitle\n ? resolvedTitle\n : matchingNavItem?.title ?? pluginTitle ?? pluginId;\n\n // PageBlueprint resolves icon as: params.icon ?? plugin.icon\n // We want the priority: page (params) -> nav item -> plugin -> (excluded)\n const resolvedIcon = node.instance.getData(coreExtensionData.icon);\n const hasExplicitPageIcon = resolvedIcon && !node.spec.plugin.icon;\n const NavItemIcon = matchingNavItem?.icon;\n\n let icon: IconElement | undefined;\n if (hasExplicitPageIcon) {\n icon = resolvedIcon;\n } else if (NavItemIcon) {\n icon = <NavItemIcon />;\n } else if (resolvedIcon) {\n icon = resolvedIcon;\n }\n\n if (!title || !icon) {\n return [];\n }\n\n const to = tryResolveLink(routeResolutionApi, routeRef);\n if (!to) {\n return [];\n }\n\n return [{ node, href: to, title, icon, routeRef }];\n });\n\n return new NavItemBag(items);\n }, [appTreeApi, routeResolutionApi, props.legacyNavItems]);\n\n return <props.Content navItems={navItems} items={legacyItems} />;\n}\n\nexport const AppNav = createExtension({\n name: 'nav',\n attachTo: { id: 'app/layout', input: 'nav' },\n inputs: {\n items: createExtensionInput([NavItemBlueprint.dataRefs.target]),\n content: createExtensionInput([NavContentBlueprint.dataRefs.component], {\n singleton: true,\n optional: true,\n internal: true,\n }),\n },\n output: [coreExtensionData.reactElement],\n *factory({ inputs }) {\n const Content =\n inputs.content?.get(NavContentBlueprint.dataRefs.component) ??\n DefaultNavContent;\n\n yield coreExtensionData.reactElement(\n <NavContentRenderer\n legacyNavItems={inputs.items.map(item =>\n item.get(NavItemBlueprint.dataRefs.target),\n )}\n Content={Content}\n />,\n );\n },\n});\n"],"names":[],"mappings":"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"}
@@ -110,7 +110,17 @@ function toAppIdentityProxy(identityApi) {
110
110
  function DefaultRouter(props) {
111
111
  const configApi = useApi(configApiRef);
112
112
  const basePath = getBasePath(configApi);
113
- return /* @__PURE__ */ jsx(BrowserRouter, { basename: basePath, children: props.children });
113
+ return /* @__PURE__ */ jsx(
114
+ BrowserRouter,
115
+ {
116
+ basename: basePath,
117
+ future: {
118
+ v7_relativeSplatPath: false,
119
+ v7_startTransition: false
120
+ },
121
+ children: props.children
122
+ }
123
+ );
114
124
  }
115
125
  function AppRouter(props) {
116
126
  const {
@@ -1 +1 @@
1
- {"version":3,"file":"AppRoot.esm.js","sources":["../../src/extensions/AppRoot.tsx"],"sourcesContent":["/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n ComponentType,\n PropsWithChildren,\n ReactNode,\n useState,\n JSX,\n} from 'react';\nimport {\n coreExtensionData,\n discoveryApiRef,\n fetchApiRef,\n errorApiRef,\n createExtension,\n createExtensionInput,\n routeResolutionApiRef,\n} from '@backstage/frontend-plugin-api';\nimport {\n AppRootWrapperBlueprint,\n RouterBlueprint,\n SignInPageBlueprint,\n} from '@backstage/plugin-app-react';\nimport {\n DiscoveryApi,\n ErrorApi,\n FetchApi,\n IdentityApi,\n ProfileInfo,\n SignInPageProps,\n configApiRef,\n identityApiRef,\n useApi,\n} from '@backstage/core-plugin-api';\n// eslint-disable-next-line @backstage/no-relative-monorepo-imports\nimport { isProtectedApp } from '../../../../packages/core-app-api/src/app/isProtectedApp';\nimport { BrowserRouter } from 'react-router-dom';\n// eslint-disable-next-line @backstage/no-relative-monorepo-imports\nimport { RouteTracker } from '../../../../packages/frontend-app-api/src/routing/RouteTracker';\n// eslint-disable-next-line @backstage/no-relative-monorepo-imports\nimport { getBasePath } from '../../../../packages/frontend-app-api/src/routing/getBasePath';\n\nexport const AppRoot = createExtension({\n name: 'root',\n attachTo: { id: 'app', input: 'root' },\n inputs: {\n router: createExtensionInput([RouterBlueprint.dataRefs.component], {\n singleton: true,\n optional: true,\n internal: true,\n }),\n signInPage: createExtensionInput([SignInPageBlueprint.dataRefs.component], {\n singleton: true,\n optional: true,\n internal: true,\n }),\n children: createExtensionInput([coreExtensionData.reactElement], {\n singleton: true,\n }),\n elements: createExtensionInput([coreExtensionData.reactElement]),\n wrappers: createExtensionInput(\n [AppRootWrapperBlueprint.dataRefs.component],\n {\n internal: true,\n },\n ),\n },\n output: [coreExtensionData.reactElement],\n factory({ inputs, apis }) {\n if (isProtectedApp()) {\n const identityApi = apis.get(identityApiRef);\n if (!identityApi) {\n throw new Error('App requires an Identity API implementation');\n }\n const appIdentityProxy = toAppIdentityProxy(identityApi);\n const discoveryApi = apis.get(discoveryApiRef);\n const errorApi = apis.get(errorApiRef);\n const fetchApi = apis.get(fetchApiRef);\n if (!discoveryApi || !errorApi || !fetchApi) {\n throw new Error(\n 'App is running in protected mode but missing required APIs',\n );\n }\n appIdentityProxy.enableCookieAuth({\n discoveryApi,\n errorApi,\n fetchApi,\n });\n }\n\n let content: ReactNode = inputs.children.get(\n coreExtensionData.reactElement,\n );\n\n for (const wrapper of inputs.wrappers) {\n const Component = wrapper.get(AppRootWrapperBlueprint.dataRefs.component);\n if (Component) {\n content = <Component>{content}</Component>;\n }\n }\n\n return [\n coreExtensionData.reactElement(\n <AppRouter\n SignInPageComponent={inputs.signInPage?.get(\n SignInPageBlueprint.dataRefs.component,\n )}\n RouterComponent={inputs.router?.get(\n RouterBlueprint.dataRefs.component,\n )}\n extraElements={inputs.elements?.map(el =>\n el.get(coreExtensionData.reactElement),\n )}\n >\n {content}\n </AppRouter>,\n ),\n ];\n },\n});\n\n// This wraps the sign-in page and waits for sign-in to be completed before rendering the app\nfunction SignInPageWrapper({\n component: Component,\n appIdentityProxy,\n children,\n}: {\n component: ComponentType<SignInPageProps>;\n appIdentityProxy: AppIdentityProxy;\n children: ReactNode;\n}) {\n const [identityApi, setIdentityApi] = useState<IdentityApi>();\n const configApi = useApi(configApiRef);\n const basePath = getBasePath(configApi);\n\n if (!identityApi) {\n return <Component onSignInSuccess={setIdentityApi} />;\n }\n\n appIdentityProxy.setTarget(identityApi, {\n signOutTargetUrl: basePath || '/',\n });\n return <>{children}</>;\n}\n\ntype AppIdentityProxy = IdentityApi & {\n enableCookieAuth(ctx: {\n errorApi: ErrorApi;\n fetchApi: FetchApi;\n discoveryApi: DiscoveryApi;\n }): void;\n setTarget(\n impl: IdentityApi & /* backwards compat stuff */ {\n getUserId?(): string;\n getIdToken?(): Promise<string | undefined>;\n getProfile?(): ProfileInfo;\n },\n options: { signOutTargetUrl: string },\n ): void;\n};\n\nfunction toAppIdentityProxy(identityApi: IdentityApi): AppIdentityProxy {\n if (!('enableCookieAuth' in identityApi)) {\n throw new Error('Unexpected Identity API implementation');\n }\n return identityApi as AppIdentityProxy;\n}\n\ntype RouteResolverProxy = {\n getRouteObjects(): any[];\n};\n\n/**\n * Props for the {@link AppRouter} component.\n * @public\n */\nexport interface AppRouterProps {\n children?: ReactNode;\n SignInPageComponent?: ComponentType<SignInPageProps>;\n RouterComponent?: (props: { children: ReactNode }) => JSX.Element | null;\n extraElements?: Array<JSX.Element>;\n}\n\nfunction DefaultRouter(props: PropsWithChildren<{}>) {\n const configApi = useApi(configApiRef);\n const basePath = getBasePath(configApi);\n return <BrowserRouter basename={basePath}>{props.children}</BrowserRouter>;\n}\n\n/**\n * App router and sign-in page wrapper.\n *\n * @remarks\n *\n * The AppRouter provides the routing context and renders the sign-in page.\n * Until the user has successfully signed in, this component will render\n * the sign-in page. Once the user has signed-in, it will instead render\n * the app, while providing routing and route tracking for the app.\n */\nexport function AppRouter(props: AppRouterProps) {\n const {\n children,\n SignInPageComponent,\n RouterComponent = DefaultRouter,\n extraElements = [],\n } = props;\n\n const configApi = useApi(configApiRef);\n const appIdentityProxy = toAppIdentityProxy(useApi(identityApiRef));\n const routeResolutionsApi = useApi(routeResolutionApiRef);\n const basePath = getBasePath(configApi);\n\n // TODO: Private access for now, probably replace with path -> node lookup method on the API\n if (!('getRouteObjects' in routeResolutionsApi)) {\n throw new Error('Unexpected route resolution API implementation');\n }\n const routeObjects = (\n routeResolutionsApi as RouteResolverProxy\n ).getRouteObjects();\n\n // If the app hasn't configured a sign-in page, we just continue as guest.\n if (!SignInPageComponent) {\n appIdentityProxy.setTarget(\n {\n getUserId: () => 'guest',\n getIdToken: async () => undefined,\n getProfile: () => ({\n email: 'guest@example.com',\n displayName: 'Guest',\n }),\n getProfileInfo: async () => ({\n email: 'guest@example.com',\n displayName: 'Guest',\n }),\n getBackstageIdentity: async () => ({\n type: 'user',\n userEntityRef: 'user:default/guest',\n ownershipEntityRefs: ['user:default/guest'],\n }),\n getCredentials: async () => ({}),\n signOut: async () => {},\n },\n { signOutTargetUrl: basePath || '/' },\n );\n\n return (\n <RouterComponent>\n {...extraElements}\n <RouteTracker routeObjects={routeObjects} />\n {children}\n </RouterComponent>\n );\n }\n\n return (\n <RouterComponent>\n {...extraElements}\n <RouteTracker routeObjects={routeObjects} />\n <SignInPageWrapper\n component={SignInPageComponent}\n appIdentityProxy={appIdentityProxy}\n >\n {children}\n </SignInPageWrapper>\n </RouterComponent>\n );\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"AppRoot.esm.js","sources":["../../src/extensions/AppRoot.tsx"],"sourcesContent":["/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n ComponentType,\n PropsWithChildren,\n ReactNode,\n useState,\n JSX,\n} from 'react';\nimport {\n coreExtensionData,\n discoveryApiRef,\n fetchApiRef,\n errorApiRef,\n createExtension,\n createExtensionInput,\n routeResolutionApiRef,\n} from '@backstage/frontend-plugin-api';\nimport {\n AppRootWrapperBlueprint,\n RouterBlueprint,\n SignInPageBlueprint,\n} from '@backstage/plugin-app-react';\nimport {\n DiscoveryApi,\n ErrorApi,\n FetchApi,\n IdentityApi,\n ProfileInfo,\n SignInPageProps,\n configApiRef,\n identityApiRef,\n useApi,\n} from '@backstage/core-plugin-api';\n// eslint-disable-next-line @backstage/no-relative-monorepo-imports\nimport { isProtectedApp } from '../../../../packages/core-app-api/src/app/isProtectedApp';\nimport { BrowserRouter } from 'react-router-dom';\n// eslint-disable-next-line @backstage/no-relative-monorepo-imports\nimport { RouteTracker } from '../../../../packages/frontend-app-api/src/routing/RouteTracker';\n// eslint-disable-next-line @backstage/no-relative-monorepo-imports\nimport { getBasePath } from '../../../../packages/frontend-app-api/src/routing/getBasePath';\n\nexport const AppRoot = createExtension({\n name: 'root',\n attachTo: { id: 'app', input: 'root' },\n inputs: {\n router: createExtensionInput([RouterBlueprint.dataRefs.component], {\n singleton: true,\n optional: true,\n internal: true,\n }),\n signInPage: createExtensionInput([SignInPageBlueprint.dataRefs.component], {\n singleton: true,\n optional: true,\n internal: true,\n }),\n children: createExtensionInput([coreExtensionData.reactElement], {\n singleton: true,\n }),\n elements: createExtensionInput([coreExtensionData.reactElement]),\n wrappers: createExtensionInput(\n [AppRootWrapperBlueprint.dataRefs.component],\n {\n internal: true,\n },\n ),\n },\n output: [coreExtensionData.reactElement],\n factory({ inputs, apis }) {\n if (isProtectedApp()) {\n const identityApi = apis.get(identityApiRef);\n if (!identityApi) {\n throw new Error('App requires an Identity API implementation');\n }\n const appIdentityProxy = toAppIdentityProxy(identityApi);\n const discoveryApi = apis.get(discoveryApiRef);\n const errorApi = apis.get(errorApiRef);\n const fetchApi = apis.get(fetchApiRef);\n if (!discoveryApi || !errorApi || !fetchApi) {\n throw new Error(\n 'App is running in protected mode but missing required APIs',\n );\n }\n appIdentityProxy.enableCookieAuth({\n discoveryApi,\n errorApi,\n fetchApi,\n });\n }\n\n let content: ReactNode = inputs.children.get(\n coreExtensionData.reactElement,\n );\n\n for (const wrapper of inputs.wrappers) {\n const Component = wrapper.get(AppRootWrapperBlueprint.dataRefs.component);\n if (Component) {\n content = <Component>{content}</Component>;\n }\n }\n\n return [\n coreExtensionData.reactElement(\n <AppRouter\n SignInPageComponent={inputs.signInPage?.get(\n SignInPageBlueprint.dataRefs.component,\n )}\n RouterComponent={inputs.router?.get(\n RouterBlueprint.dataRefs.component,\n )}\n extraElements={inputs.elements?.map(el =>\n el.get(coreExtensionData.reactElement),\n )}\n >\n {content}\n </AppRouter>,\n ),\n ];\n },\n});\n\n// This wraps the sign-in page and waits for sign-in to be completed before rendering the app\nfunction SignInPageWrapper({\n component: Component,\n appIdentityProxy,\n children,\n}: {\n component: ComponentType<SignInPageProps>;\n appIdentityProxy: AppIdentityProxy;\n children: ReactNode;\n}) {\n const [identityApi, setIdentityApi] = useState<IdentityApi>();\n const configApi = useApi(configApiRef);\n const basePath = getBasePath(configApi);\n\n if (!identityApi) {\n return <Component onSignInSuccess={setIdentityApi} />;\n }\n\n appIdentityProxy.setTarget(identityApi, {\n signOutTargetUrl: basePath || '/',\n });\n return <>{children}</>;\n}\n\ntype AppIdentityProxy = IdentityApi & {\n enableCookieAuth(ctx: {\n errorApi: ErrorApi;\n fetchApi: FetchApi;\n discoveryApi: DiscoveryApi;\n }): void;\n setTarget(\n impl: IdentityApi & /* backwards compat stuff */ {\n getUserId?(): string;\n getIdToken?(): Promise<string | undefined>;\n getProfile?(): ProfileInfo;\n },\n options: { signOutTargetUrl: string },\n ): void;\n};\n\nfunction toAppIdentityProxy(identityApi: IdentityApi): AppIdentityProxy {\n if (!('enableCookieAuth' in identityApi)) {\n throw new Error('Unexpected Identity API implementation');\n }\n return identityApi as AppIdentityProxy;\n}\n\ntype RouteResolverProxy = {\n getRouteObjects(): any[];\n};\n\n/**\n * Props for the {@link AppRouter} component.\n * @public\n */\nexport interface AppRouterProps {\n children?: ReactNode;\n SignInPageComponent?: ComponentType<SignInPageProps>;\n RouterComponent?: (props: { children: ReactNode }) => JSX.Element | null;\n extraElements?: Array<JSX.Element>;\n}\n\nfunction DefaultRouter(props: PropsWithChildren<{}>) {\n const configApi = useApi(configApiRef);\n const basePath = getBasePath(configApi);\n return (\n <BrowserRouter\n basename={basePath}\n future={{\n v7_relativeSplatPath: false,\n v7_startTransition: false,\n }}\n >\n {props.children}\n </BrowserRouter>\n );\n}\n\n/**\n * App router and sign-in page wrapper.\n *\n * @remarks\n *\n * The AppRouter provides the routing context and renders the sign-in page.\n * Until the user has successfully signed in, this component will render\n * the sign-in page. Once the user has signed-in, it will instead render\n * the app, while providing routing and route tracking for the app.\n */\nexport function AppRouter(props: AppRouterProps) {\n const {\n children,\n SignInPageComponent,\n RouterComponent = DefaultRouter,\n extraElements = [],\n } = props;\n\n const configApi = useApi(configApiRef);\n const appIdentityProxy = toAppIdentityProxy(useApi(identityApiRef));\n const routeResolutionsApi = useApi(routeResolutionApiRef);\n const basePath = getBasePath(configApi);\n\n // TODO: Private access for now, probably replace with path -> node lookup method on the API\n if (!('getRouteObjects' in routeResolutionsApi)) {\n throw new Error('Unexpected route resolution API implementation');\n }\n const routeObjects = (\n routeResolutionsApi as RouteResolverProxy\n ).getRouteObjects();\n\n // If the app hasn't configured a sign-in page, we just continue as guest.\n if (!SignInPageComponent) {\n appIdentityProxy.setTarget(\n {\n getUserId: () => 'guest',\n getIdToken: async () => undefined,\n getProfile: () => ({\n email: 'guest@example.com',\n displayName: 'Guest',\n }),\n getProfileInfo: async () => ({\n email: 'guest@example.com',\n displayName: 'Guest',\n }),\n getBackstageIdentity: async () => ({\n type: 'user',\n userEntityRef: 'user:default/guest',\n ownershipEntityRefs: ['user:default/guest'],\n }),\n getCredentials: async () => ({}),\n signOut: async () => {},\n },\n { signOutTargetUrl: basePath || '/' },\n );\n\n return (\n <RouterComponent>\n {...extraElements}\n <RouteTracker routeObjects={routeObjects} />\n {children}\n </RouterComponent>\n );\n }\n\n return (\n <RouterComponent>\n {...extraElements}\n <RouteTracker routeObjects={routeObjects} />\n <SignInPageWrapper\n component={SignInPageComponent}\n appIdentityProxy={appIdentityProxy}\n >\n {children}\n </SignInPageWrapper>\n </RouterComponent>\n );\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ import { ApiBlueprint, createExtensionInput, pluginHeaderActionsApiRef, coreExtensionData } from '@backstage/frontend-plugin-api';
2
+ import { DefaultPluginHeaderActionsApi } from '../apis/PluginHeaderActionsApi/DefaultPluginHeaderActionsApi.esm.js';
3
+
4
+ const PluginHeaderActionsApi = ApiBlueprint.makeWithOverrides({
5
+ name: "plugin-header-actions",
6
+ inputs: {
7
+ actions: createExtensionInput([coreExtensionData.reactElement])
8
+ },
9
+ factory: (originalFactory, { inputs }) => {
10
+ return originalFactory(
11
+ (defineParams) => defineParams({
12
+ api: pluginHeaderActionsApiRef,
13
+ deps: {},
14
+ factory: () => {
15
+ return DefaultPluginHeaderActionsApi.fromActions(
16
+ inputs.actions.map((actionInput) => ({
17
+ element: actionInput.get(coreExtensionData.reactElement),
18
+ pluginId: actionInput.node.spec.plugin.pluginId
19
+ }))
20
+ );
21
+ }
22
+ })
23
+ );
24
+ }
25
+ });
26
+
27
+ export { PluginHeaderActionsApi };
28
+ //# sourceMappingURL=PluginHeaderActionsApi.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PluginHeaderActionsApi.esm.js","sources":["../../src/extensions/PluginHeaderActionsApi.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n coreExtensionData,\n pluginHeaderActionsApiRef,\n createExtensionInput,\n ApiBlueprint,\n} from '@backstage/frontend-plugin-api';\nimport { DefaultPluginHeaderActionsApi } from '../apis/PluginHeaderActionsApi';\n\n/**\n * Contains the plugin-scoped header actions installed into the app.\n */\nexport const PluginHeaderActionsApi = ApiBlueprint.makeWithOverrides({\n name: 'plugin-header-actions',\n inputs: {\n actions: createExtensionInput([coreExtensionData.reactElement]),\n },\n factory: (originalFactory, { inputs }) => {\n return originalFactory(defineParams =>\n defineParams({\n api: pluginHeaderActionsApiRef,\n deps: {},\n factory: () => {\n return DefaultPluginHeaderActionsApi.fromActions(\n inputs.actions.map(actionInput => ({\n element: actionInput.get(coreExtensionData.reactElement),\n pluginId: actionInput.node.spec.plugin.pluginId,\n })),\n );\n },\n }),\n );\n },\n});\n"],"names":[],"mappings":";;;AA2BO,MAAM,sBAAA,GAAyB,aAAa,iBAAA,CAAkB;AAAA,EACnE,IAAA,EAAM,uBAAA;AAAA,EACN,MAAA,EAAQ;AAAA,IACN,OAAA,EAAS,oBAAA,CAAqB,CAAC,iBAAA,CAAkB,YAAY,CAAC;AAAA,GAChE;AAAA,EACA,OAAA,EAAS,CAAC,eAAA,EAAiB,EAAE,QAAO,KAAM;AACxC,IAAA,OAAO,eAAA;AAAA,MAAgB,kBACrB,YAAA,CAAa;AAAA,QACX,GAAA,EAAK,yBAAA;AAAA,QACL,MAAM,EAAC;AAAA,QACP,SAAS,MAAM;AACb,UAAA,OAAO,6BAAA,CAA8B,WAAA;AAAA,YACnC,MAAA,CAAO,OAAA,CAAQ,GAAA,CAAI,CAAA,WAAA,MAAgB;AAAA,cACjC,OAAA,EAAS,WAAA,CAAY,GAAA,CAAI,iBAAA,CAAkB,YAAY,CAAA;AAAA,cACvD,QAAA,EAAU,WAAA,CAAY,IAAA,CAAK,IAAA,CAAK,MAAA,CAAO;AAAA,aACzC,CAAE;AAAA,WACJ;AAAA,QACF;AAAA,OACD;AAAA,KACH;AAAA,EACF;AACF,CAAC;;;;"}
@@ -1,8 +1,10 @@
1
- import { jsx } from 'react/jsx-runtime';
2
- import { Progress as Progress$2, NotFoundErrorPage as NotFoundErrorPage$1, ErrorDisplay as ErrorDisplay$1 } from '@backstage/frontend-plugin-api';
1
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
+ import { Progress as Progress$2, NotFoundErrorPage as NotFoundErrorPage$1, ErrorDisplay as ErrorDisplay$1, PageLayout as PageLayout$1 } from '@backstage/frontend-plugin-api';
3
3
  import { SwappableComponentBlueprint } from '@backstage/plugin-app-react';
4
4
  import { Progress as Progress$1, ErrorPage, ErrorPanel } from '@backstage/core-components';
5
+ import { PluginHeader } from '@backstage/ui';
5
6
  import Button from '@material-ui/core/Button';
7
+ import { useMemo } from 'react';
6
8
 
7
9
  const Progress = SwappableComponentBlueprint.make({
8
10
  name: "core-progress",
@@ -29,6 +31,37 @@ const ErrorDisplay = SwappableComponentBlueprint.make({
29
31
  }
30
32
  })
31
33
  });
34
+ const PageLayout = SwappableComponentBlueprint.make({
35
+ name: "core-page-layout",
36
+ params: (define) => define({
37
+ component: PageLayout$1,
38
+ loader: () => (props) => {
39
+ const { title, icon, noHeader, headerActions, tabs, children } = props;
40
+ const tabsWithMatchStrategy = useMemo(
41
+ () => tabs?.map((tab) => ({
42
+ ...tab,
43
+ matchStrategy: "prefix"
44
+ })),
45
+ [tabs]
46
+ );
47
+ if (tabsWithMatchStrategy) {
48
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
49
+ !noHeader && /* @__PURE__ */ jsx(
50
+ PluginHeader,
51
+ {
52
+ title,
53
+ icon,
54
+ tabs: tabsWithMatchStrategy,
55
+ customActions: headerActions
56
+ }
57
+ ),
58
+ children
59
+ ] });
60
+ }
61
+ return /* @__PURE__ */ jsx(Fragment, { children });
62
+ }
63
+ })
64
+ });
32
65
 
33
- export { ErrorDisplay, NotFoundErrorPage, Progress };
66
+ export { ErrorDisplay, NotFoundErrorPage, PageLayout, Progress };
34
67
  //# sourceMappingURL=components.esm.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"components.esm.js","sources":["../../src/extensions/components.tsx"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport {\n NotFoundErrorPage as SwappableNotFoundErrorPage,\n Progress as SwappableProgress,\n ErrorDisplay as SwappableErrorDisplay,\n} from '@backstage/frontend-plugin-api';\nimport { SwappableComponentBlueprint } from '@backstage/plugin-app-react';\nimport {\n ErrorPage,\n ErrorPanel,\n Progress as ProgressComponent,\n} from '@backstage/core-components';\nimport Button from '@material-ui/core/Button';\n\nexport const Progress = SwappableComponentBlueprint.make({\n name: 'core-progress',\n params: define =>\n define({\n component: SwappableProgress,\n loader: () => ProgressComponent,\n }),\n});\n\nexport const NotFoundErrorPage = SwappableComponentBlueprint.make({\n name: 'core-not-found-error-page',\n params: define =>\n define({\n component: SwappableNotFoundErrorPage,\n loader: () => () =>\n <ErrorPage status=\"404\" statusMessage=\"PAGE NOT FOUND\" />,\n }),\n});\n\nexport const ErrorDisplay = SwappableComponentBlueprint.make({\n name: 'core-error-display',\n params: define =>\n define({\n component: SwappableErrorDisplay,\n loader: () => props => {\n const { plugin, error, resetError } = props;\n const title = `Error in ${plugin?.id}`;\n return (\n <ErrorPanel title={title} error={error} defaultExpanded>\n <Button variant=\"outlined\" onClick={resetError}>\n Retry\n </Button>\n </ErrorPanel>\n );\n },\n }),\n});\n"],"names":["SwappableProgress","ProgressComponent","SwappableNotFoundErrorPage","SwappableErrorDisplay"],"mappings":";;;;;;AA4BO,MAAM,QAAA,GAAW,4BAA4B,IAAA,CAAK;AAAA,EACvD,IAAA,EAAM,eAAA;AAAA,EACN,MAAA,EAAQ,YACN,MAAA,CAAO;AAAA,IACL,SAAA,EAAWA,UAAA;AAAA,IACX,QAAQ,MAAMC;AAAA,GACf;AACL,CAAC;AAEM,MAAM,iBAAA,GAAoB,4BAA4B,IAAA,CAAK;AAAA,EAChE,IAAA,EAAM,2BAAA;AAAA,EACN,MAAA,EAAQ,YACN,MAAA,CAAO;AAAA,IACL,SAAA,EAAWC,mBAAA;AAAA,IACX,MAAA,EAAQ,MAAM,sBACZ,GAAA,CAAC,aAAU,MAAA,EAAO,KAAA,EAAM,eAAc,gBAAA,EAAiB;AAAA,GAC1D;AACL,CAAC;AAEM,MAAM,YAAA,GAAe,4BAA4B,IAAA,CAAK;AAAA,EAC3D,IAAA,EAAM,oBAAA;AAAA,EACN,MAAA,EAAQ,YACN,MAAA,CAAO;AAAA,IACL,SAAA,EAAWC,cAAA;AAAA,IACX,MAAA,EAAQ,MAAM,CAAA,KAAA,KAAS;AACrB,MAAA,MAAM,EAAE,MAAA,EAAQ,KAAA,EAAO,UAAA,EAAW,GAAI,KAAA;AACtC,MAAA,MAAM,KAAA,GAAQ,CAAA,SAAA,EAAY,MAAA,EAAQ,EAAE,CAAA,CAAA;AACpC,MAAA,uBACE,GAAA,CAAC,UAAA,EAAA,EAAW,KAAA,EAAc,KAAA,EAAc,eAAA,EAAe,IAAA,EACrD,QAAA,kBAAA,GAAA,CAAC,MAAA,EAAA,EAAO,OAAA,EAAQ,UAAA,EAAW,OAAA,EAAS,UAAA,EAAY,mBAEhD,CAAA,EACF,CAAA;AAAA,IAEJ;AAAA,GACD;AACL,CAAC;;;;"}
1
+ {"version":3,"file":"components.esm.js","sources":["../../src/extensions/components.tsx"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport {\n NotFoundErrorPage as SwappableNotFoundErrorPage,\n Progress as SwappableProgress,\n ErrorDisplay as SwappableErrorDisplay,\n PageLayout as SwappablePageLayout,\n type PageLayoutProps,\n} from '@backstage/frontend-plugin-api';\nimport { SwappableComponentBlueprint } from '@backstage/plugin-app-react';\nimport {\n ErrorPage,\n ErrorPanel,\n Progress as ProgressComponent,\n} from '@backstage/core-components';\nimport { PluginHeader } from '@backstage/ui';\nimport Button from '@material-ui/core/Button';\nimport { useMemo } from 'react';\n\nexport const Progress = SwappableComponentBlueprint.make({\n name: 'core-progress',\n params: define =>\n define({\n component: SwappableProgress,\n loader: () => ProgressComponent,\n }),\n});\n\nexport const NotFoundErrorPage = SwappableComponentBlueprint.make({\n name: 'core-not-found-error-page',\n params: define =>\n define({\n component: SwappableNotFoundErrorPage,\n loader: () => () =>\n <ErrorPage status=\"404\" statusMessage=\"PAGE NOT FOUND\" />,\n }),\n});\n\nexport const ErrorDisplay = SwappableComponentBlueprint.make({\n name: 'core-error-display',\n params: define =>\n define({\n component: SwappableErrorDisplay,\n loader: () => props => {\n const { plugin, error, resetError } = props;\n const title = `Error in ${plugin?.id}`;\n return (\n <ErrorPanel title={title} error={error} defaultExpanded>\n <Button variant=\"outlined\" onClick={resetError}>\n Retry\n </Button>\n </ErrorPanel>\n );\n },\n }),\n});\n\nexport const PageLayout = SwappableComponentBlueprint.make({\n name: 'core-page-layout',\n params: define =>\n define({\n component: SwappablePageLayout,\n loader: () => (props: PageLayoutProps) => {\n const { title, icon, noHeader, headerActions, tabs, children } = props;\n const tabsWithMatchStrategy = useMemo(\n () =>\n tabs?.map(tab => ({\n ...tab,\n matchStrategy: 'prefix' as const,\n })),\n [tabs],\n );\n\n if (tabsWithMatchStrategy) {\n return (\n <>\n {!noHeader && (\n <PluginHeader\n title={title}\n icon={icon}\n tabs={tabsWithMatchStrategy}\n customActions={headerActions}\n />\n )}\n {children}\n </>\n );\n }\n return <>{children}</>;\n },\n }),\n});\n"],"names":["SwappableProgress","ProgressComponent","SwappableNotFoundErrorPage","SwappableErrorDisplay","SwappablePageLayout"],"mappings":";;;;;;;;AAgCO,MAAM,QAAA,GAAW,4BAA4B,IAAA,CAAK;AAAA,EACvD,IAAA,EAAM,eAAA;AAAA,EACN,MAAA,EAAQ,YACN,MAAA,CAAO;AAAA,IACL,SAAA,EAAWA,UAAA;AAAA,IACX,QAAQ,MAAMC;AAAA,GACf;AACL,CAAC;AAEM,MAAM,iBAAA,GAAoB,4BAA4B,IAAA,CAAK;AAAA,EAChE,IAAA,EAAM,2BAAA;AAAA,EACN,MAAA,EAAQ,YACN,MAAA,CAAO;AAAA,IACL,SAAA,EAAWC,mBAAA;AAAA,IACX,MAAA,EAAQ,MAAM,sBACZ,GAAA,CAAC,aAAU,MAAA,EAAO,KAAA,EAAM,eAAc,gBAAA,EAAiB;AAAA,GAC1D;AACL,CAAC;AAEM,MAAM,YAAA,GAAe,4BAA4B,IAAA,CAAK;AAAA,EAC3D,IAAA,EAAM,oBAAA;AAAA,EACN,MAAA,EAAQ,YACN,MAAA,CAAO;AAAA,IACL,SAAA,EAAWC,cAAA;AAAA,IACX,MAAA,EAAQ,MAAM,CAAA,KAAA,KAAS;AACrB,MAAA,MAAM,EAAE,MAAA,EAAQ,KAAA,EAAO,UAAA,EAAW,GAAI,KAAA;AACtC,MAAA,MAAM,KAAA,GAAQ,CAAA,SAAA,EAAY,MAAA,EAAQ,EAAE,CAAA,CAAA;AACpC,MAAA,uBACE,GAAA,CAAC,UAAA,EAAA,EAAW,KAAA,EAAc,KAAA,EAAc,eAAA,EAAe,IAAA,EACrD,QAAA,kBAAA,GAAA,CAAC,MAAA,EAAA,EAAO,OAAA,EAAQ,UAAA,EAAW,OAAA,EAAS,UAAA,EAAY,mBAEhD,CAAA,EACF,CAAA;AAAA,IAEJ;AAAA,GACD;AACL,CAAC;AAEM,MAAM,UAAA,GAAa,4BAA4B,IAAA,CAAK;AAAA,EACzD,IAAA,EAAM,kBAAA;AAAA,EACN,MAAA,EAAQ,YACN,MAAA,CAAO;AAAA,IACL,SAAA,EAAWC,YAAA;AAAA,IACX,MAAA,EAAQ,MAAM,CAAC,KAAA,KAA2B;AACxC,MAAA,MAAM,EAAE,KAAA,EAAO,IAAA,EAAM,UAAU,aAAA,EAAe,IAAA,EAAM,UAAS,GAAI,KAAA;AACjE,MAAA,MAAM,qBAAA,GAAwB,OAAA;AAAA,QAC5B,MACE,IAAA,EAAM,GAAA,CAAI,CAAA,GAAA,MAAQ;AAAA,UAChB,GAAG,GAAA;AAAA,UACH,aAAA,EAAe;AAAA,SACjB,CAAE,CAAA;AAAA,QACJ,CAAC,IAAI;AAAA,OACP;AAEA,MAAA,IAAI,qBAAA,EAAuB;AACzB,QAAA,uBACE,IAAA,CAAA,QAAA,EAAA,EACG,QAAA,EAAA;AAAA,UAAA,CAAC,QAAA,oBACA,GAAA;AAAA,YAAC,YAAA;AAAA,YAAA;AAAA,cACC,KAAA;AAAA,cACA,IAAA;AAAA,cACA,IAAA,EAAM,qBAAA;AAAA,cACN,aAAA,EAAe;AAAA;AAAA,WACjB;AAAA,UAED;AAAA,SAAA,EACH,CAAA;AAAA,MAEJ;AACA,MAAA,uCAAU,QAAA,EAAS,CAAA;AAAA,IACrB;AAAA,GACD;AACL,CAAC;;;;"}
package/dist/index.d.ts CHANGED
@@ -287,7 +287,7 @@ declare const appPlugin: _backstage_frontend_plugin_api.OverridableFrontendPlugi
287
287
  output: _backstage_frontend_plugin_api.ExtensionDataRef<_backstage_frontend_plugin_api.AnyApiFactory, "core.api.factory", {}>;
288
288
  inputs: {
289
289
  icons: _backstage_frontend_plugin_api.ExtensionInput<_backstage_frontend_plugin_api.ConfigurableExtensionDataRef<{
290
- [x: string]: _backstage_frontend_plugin_api.IconComponent;
290
+ [x: string]: _backstage_frontend_plugin_api.IconComponent | _backstage_frontend_plugin_api.IconElement;
291
291
  }, "core.icons", {}>, {
292
292
  singleton: false;
293
293
  optional: false;
@@ -352,6 +352,21 @@ declare const appPlugin: _backstage_frontend_plugin_api.OverridableFrontendPlugi
352
352
  inputs: {};
353
353
  params: <TApi, TImpl extends TApi, TDeps extends { [name in string]: unknown; }>(params: _backstage_frontend_plugin_api.ApiFactory<TApi, TImpl, TDeps>) => _backstage_frontend_plugin_api.ExtensionBlueprintParams<_backstage_frontend_plugin_api.AnyApiFactory>;
354
354
  }>;
355
+ "api:app/plugin-header-actions": _backstage_frontend_plugin_api.OverridableExtensionDefinition<{
356
+ config: {};
357
+ configInput: {};
358
+ output: _backstage_frontend_plugin_api.ExtensionDataRef<_backstage_frontend_plugin_api.AnyApiFactory, "core.api.factory", {}>;
359
+ inputs: {
360
+ actions: _backstage_frontend_plugin_api.ExtensionInput<_backstage_frontend_plugin_api.ConfigurableExtensionDataRef<react.JSX.Element, "core.reactElement", {}>, {
361
+ singleton: false;
362
+ optional: false;
363
+ internal: false;
364
+ }>;
365
+ };
366
+ kind: "api";
367
+ name: "plugin-header-actions";
368
+ params: <TApi, TImpl extends TApi, TDeps extends { [name in string]: unknown; }>(params: _backstage_frontend_plugin_api.ApiFactory<TApi, TImpl, TDeps>) => _backstage_frontend_plugin_api.ExtensionBlueprintParams<_backstage_frontend_plugin_api.AnyApiFactory>;
369
+ }>;
355
370
  "api:app/plugin-wrapper": _backstage_frontend_plugin_api.OverridableExtensionDefinition<{
356
371
  config: {};
357
372
  configInput: {};
@@ -531,6 +546,28 @@ declare const appPlugin: _backstage_frontend_plugin_api.OverridableFrontendPlugi
531
546
  loader: Ref extends _backstage_frontend_plugin_api.SwappableComponentRef<infer IInnerComponentProps, any> ? (() => (props: IInnerComponentProps) => JSX.Element | null) | (() => Promise<(props: IInnerComponentProps) => JSX.Element | null>) : never;
532
547
  }>;
533
548
  }>;
549
+ "component:app/core-page-layout": _backstage_frontend_plugin_api.OverridableExtensionDefinition<{
550
+ kind: "component";
551
+ name: "core-page-layout";
552
+ config: {};
553
+ configInput: {};
554
+ output: _backstage_frontend_plugin_api.ExtensionDataRef<{
555
+ ref: _backstage_frontend_plugin_api.SwappableComponentRef;
556
+ loader: (() => (props: {}) => JSX.Element | null) | (() => Promise<(props: {}) => JSX.Element | null>);
557
+ }, "core.swappableComponent", {}>;
558
+ inputs: {};
559
+ params: <Ref extends _backstage_frontend_plugin_api.SwappableComponentRef<any>>(params: {
560
+ component: Ref extends _backstage_frontend_plugin_api.SwappableComponentRef<any, infer IExternalComponentProps> ? {
561
+ ref: Ref;
562
+ } & ((props: IExternalComponentProps) => JSX.Element | null) : never;
563
+ loader: Ref extends _backstage_frontend_plugin_api.SwappableComponentRef<infer IInnerComponentProps, any> ? (() => (props: IInnerComponentProps) => JSX.Element | null) | (() => Promise<(props: IInnerComponentProps) => JSX.Element | null>) : never;
564
+ }) => _backstage_frontend_plugin_api.ExtensionBlueprintParams<{
565
+ component: Ref extends _backstage_frontend_plugin_api.SwappableComponentRef<any, infer IExternalComponentProps> ? {
566
+ ref: Ref;
567
+ } & ((props: IExternalComponentProps) => JSX.Element | null) : never;
568
+ loader: Ref extends _backstage_frontend_plugin_api.SwappableComponentRef<infer IInnerComponentProps, any> ? (() => (props: IInnerComponentProps) => JSX.Element | null) | (() => Promise<(props: IInnerComponentProps) => JSX.Element | null>) : never;
569
+ }>;
570
+ }>;
534
571
  "component:app/core-progress": _backstage_frontend_plugin_api.OverridableExtensionDefinition<{
535
572
  kind: "component";
536
573
  name: "core-progress";
@@ -31,24 +31,30 @@ class AppLanguageSelector {
31
31
  if (storedLanguage && languages.includes(storedLanguage)) {
32
32
  selector.setLanguage(storedLanguage);
33
33
  }
34
- selector.language$().subscribe(({ language }) => {
34
+ const subscription = selector.language$().subscribe(({ language }) => {
35
35
  if (language !== window.localStorage.getItem(STORAGE_KEY)) {
36
36
  window.localStorage.setItem(STORAGE_KEY, language);
37
37
  }
38
38
  });
39
- window.addEventListener("storage", (event) => {
39
+ const storageListener = (event) => {
40
40
  if (event.key === STORAGE_KEY) {
41
41
  const language = localStorage.getItem(STORAGE_KEY) ?? void 0;
42
42
  if (language) {
43
43
  selector.setLanguage(language);
44
44
  }
45
45
  }
46
- });
46
+ };
47
+ window.addEventListener("storage", storageListener);
48
+ selector.#storageSubscription = subscription;
49
+ selector.#storageListener = storageListener;
47
50
  return selector;
48
51
  }
49
52
  #languages;
50
53
  #language;
51
54
  #subject;
55
+ // References for cleanup when using createWithStorage
56
+ #storageSubscription;
57
+ #storageListener;
52
58
  constructor(languages, initialLanguage) {
53
59
  this.#languages = languages;
54
60
  this.#language = initialLanguage;
@@ -80,6 +86,21 @@ class AppLanguageSelector {
80
86
  language$() {
81
87
  return this.#subject;
82
88
  }
89
+ /**
90
+ * Cleans up resources created by createWithStorage().
91
+ * Call this method when the selector is no longer needed to prevent memory leaks.
92
+ * This is particularly useful in testing scenarios or when the app is unmounted.
93
+ */
94
+ dispose() {
95
+ if (this.#storageSubscription) {
96
+ this.#storageSubscription.unsubscribe();
97
+ this.#storageSubscription = void 0;
98
+ }
99
+ if (this.#storageListener) {
100
+ window.removeEventListener("storage", this.#storageListener);
101
+ this.#storageListener = void 0;
102
+ }
103
+ }
83
104
  }
84
105
 
85
106
  export { AppLanguageSelector, DEFAULT_LANGUAGE };
@@ -1 +1 @@
1
- {"version":3,"file":"AppLanguageSelector.esm.js","sources":["../../../../../../../../../packages/core-app-api/src/apis/implementations/AppLanguageApi/AppLanguageSelector.ts"],"sourcesContent":["/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// Internal import to avoid code duplication, this will lead to duplication in build output\n// eslint-disable-next-line @backstage/no-relative-monorepo-imports\nimport { AppLanguageApi } from '@backstage/core-plugin-api/alpha';\nimport { Observable } from '@backstage/types';\nimport { BehaviorSubject } from '../../../lib';\n\nconst STORAGE_KEY = 'language';\nexport const DEFAULT_LANGUAGE = 'en';\n\n/** @alpha */\nexport interface AppLanguageSelectorOptions {\n defaultLanguage?: string;\n availableLanguages?: string[];\n}\n\n/**\n * Exposes the available languages in the app and allows for switching of the active language.\n *\n * @alpha\n */\nexport class AppLanguageSelector implements AppLanguageApi {\n static create(options?: AppLanguageSelectorOptions) {\n const languages = options?.availableLanguages ?? [DEFAULT_LANGUAGE];\n if (languages.length !== new Set(languages).size) {\n throw new Error(\n `Supported languages may not contain duplicates, got '${languages.join(\n \"', '\",\n )}'`,\n );\n }\n\n const initialLanguage = options?.defaultLanguage ?? DEFAULT_LANGUAGE;\n\n if (!languages.includes(initialLanguage)) {\n throw new Error(\n `Initial language must be one of the supported languages, got '${initialLanguage}'`,\n );\n }\n\n return new AppLanguageSelector(languages, initialLanguage);\n }\n\n static createWithStorage(options?: AppLanguageSelectorOptions) {\n const selector = AppLanguageSelector.create(options);\n\n if (!window.localStorage) {\n return selector;\n }\n\n const storedLanguage = window.localStorage.getItem(STORAGE_KEY);\n const { languages } = selector.getAvailableLanguages();\n if (storedLanguage && languages.includes(storedLanguage)) {\n selector.setLanguage(storedLanguage);\n }\n\n selector.language$().subscribe(({ language }) => {\n if (language !== window.localStorage.getItem(STORAGE_KEY)) {\n window.localStorage.setItem(STORAGE_KEY, language);\n }\n });\n\n window.addEventListener('storage', event => {\n if (event.key === STORAGE_KEY) {\n const language = localStorage.getItem(STORAGE_KEY) ?? undefined;\n if (language) {\n selector.setLanguage(language);\n }\n }\n });\n\n return selector;\n }\n\n #languages: string[];\n #language: string;\n #subject: BehaviorSubject<{ language: string }>;\n\n private constructor(languages: string[], initialLanguage: string) {\n this.#languages = languages;\n this.#language = initialLanguage;\n this.#subject = new BehaviorSubject<{ language: string }>({\n language: this.#language,\n });\n }\n\n getAvailableLanguages(): { languages: string[] } {\n return { languages: this.#languages.slice() };\n }\n\n setLanguage(language?: string | undefined): void {\n const lng = language ?? DEFAULT_LANGUAGE;\n if (lng === this.#language) {\n return;\n }\n if (lng && !this.#languages.includes(lng)) {\n throw new Error(\n `Failed to change language to '${lng}', available languages are '${this.#languages.join(\n \"', '\",\n )}'`,\n );\n }\n this.#language = lng;\n this.#subject.next({ language: lng });\n }\n\n getLanguage(): { language: string } {\n return { language: this.#language };\n }\n\n language$(): Observable<{ language: string }> {\n return this.#subject;\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"AppLanguageSelector.esm.js","sources":["../../../../../../../../../packages/core-app-api/src/apis/implementations/AppLanguageApi/AppLanguageSelector.ts"],"sourcesContent":["/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// Internal import to avoid code duplication, this will lead to duplication in build output\n// eslint-disable-next-line @backstage/no-relative-monorepo-imports\nimport { AppLanguageApi } from '@backstage/core-plugin-api/alpha';\nimport { Observable } from '@backstage/types';\nimport { BehaviorSubject } from '../../../lib';\n\nconst STORAGE_KEY = 'language';\nexport const DEFAULT_LANGUAGE = 'en';\n\n/** @alpha */\nexport interface AppLanguageSelectorOptions {\n defaultLanguage?: string;\n availableLanguages?: string[];\n}\n\n/**\n * Exposes the available languages in the app and allows for switching of the active language.\n *\n * @alpha\n */\nexport class AppLanguageSelector implements AppLanguageApi {\n static create(options?: AppLanguageSelectorOptions) {\n const languages = options?.availableLanguages ?? [DEFAULT_LANGUAGE];\n if (languages.length !== new Set(languages).size) {\n throw new Error(\n `Supported languages may not contain duplicates, got '${languages.join(\n \"', '\",\n )}'`,\n );\n }\n\n const initialLanguage = options?.defaultLanguage ?? DEFAULT_LANGUAGE;\n\n if (!languages.includes(initialLanguage)) {\n throw new Error(\n `Initial language must be one of the supported languages, got '${initialLanguage}'`,\n );\n }\n\n return new AppLanguageSelector(languages, initialLanguage);\n }\n\n static createWithStorage(options?: AppLanguageSelectorOptions) {\n const selector = AppLanguageSelector.create(options);\n\n if (!window.localStorage) {\n return selector;\n }\n\n const storedLanguage = window.localStorage.getItem(STORAGE_KEY);\n const { languages } = selector.getAvailableLanguages();\n if (storedLanguage && languages.includes(storedLanguage)) {\n selector.setLanguage(storedLanguage);\n }\n\n const subscription = selector.language$().subscribe(({ language }) => {\n if (language !== window.localStorage.getItem(STORAGE_KEY)) {\n window.localStorage.setItem(STORAGE_KEY, language);\n }\n });\n\n const storageListener = (event: StorageEvent) => {\n if (event.key === STORAGE_KEY) {\n const language = localStorage.getItem(STORAGE_KEY) ?? undefined;\n if (language) {\n selector.setLanguage(language);\n }\n }\n };\n window.addEventListener('storage', storageListener);\n\n // Store cleanup references for potential disposal\n selector.#storageSubscription = subscription;\n selector.#storageListener = storageListener;\n\n return selector;\n }\n\n #languages: string[];\n #language: string;\n #subject: BehaviorSubject<{ language: string }>;\n\n // References for cleanup when using createWithStorage\n #storageSubscription?: { unsubscribe(): void };\n #storageListener?: (event: StorageEvent) => void;\n\n private constructor(languages: string[], initialLanguage: string) {\n this.#languages = languages;\n this.#language = initialLanguage;\n this.#subject = new BehaviorSubject<{ language: string }>({\n language: this.#language,\n });\n }\n\n getAvailableLanguages(): { languages: string[] } {\n return { languages: this.#languages.slice() };\n }\n\n setLanguage(language?: string | undefined): void {\n const lng = language ?? DEFAULT_LANGUAGE;\n if (lng === this.#language) {\n return;\n }\n if (lng && !this.#languages.includes(lng)) {\n throw new Error(\n `Failed to change language to '${lng}', available languages are '${this.#languages.join(\n \"', '\",\n )}'`,\n );\n }\n this.#language = lng;\n this.#subject.next({ language: lng });\n }\n\n getLanguage(): { language: string } {\n return { language: this.#language };\n }\n\n language$(): Observable<{ language: string }> {\n return this.#subject;\n }\n\n /**\n * Cleans up resources created by createWithStorage().\n * Call this method when the selector is no longer needed to prevent memory leaks.\n * This is particularly useful in testing scenarios or when the app is unmounted.\n */\n dispose(): void {\n if (this.#storageSubscription) {\n this.#storageSubscription.unsubscribe();\n this.#storageSubscription = undefined;\n }\n if (this.#storageListener) {\n window.removeEventListener('storage', this.#storageListener);\n this.#storageListener = undefined;\n }\n }\n}\n"],"names":[],"mappings":";;;AAsBA,MAAM,WAAA,GAAc,UAAA;AACb,MAAM,gBAAA,GAAmB;AAazB,MAAM,mBAAA,CAA8C;AAAA,EACzD,OAAO,OAAO,OAAA,EAAsC;AAClD,IAAA,MAAM,SAAA,GAAY,OAAA,EAAS,kBAAA,IAAsB,CAAC,gBAAgB,CAAA;AAClE,IAAA,IAAI,UAAU,MAAA,KAAW,IAAI,GAAA,CAAI,SAAS,EAAE,IAAA,EAAM;AAChD,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,wDAAwD,SAAA,CAAU,IAAA;AAAA,UAChE;AAAA,SACD,CAAA,CAAA;AAAA,OACH;AAAA,IACF;AAEA,IAAA,MAAM,eAAA,GAAkB,SAAS,eAAA,IAAmB,gBAAA;AAEpD,IAAA,IAAI,CAAC,SAAA,CAAU,QAAA,CAAS,eAAe,CAAA,EAAG;AACxC,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,iEAAiE,eAAe,CAAA,CAAA;AAAA,OAClF;AAAA,IACF;AAEA,IAAA,OAAO,IAAI,mBAAA,CAAoB,SAAA,EAAW,eAAe,CAAA;AAAA,EAC3D;AAAA,EAEA,OAAO,kBAAkB,OAAA,EAAsC;AAC7D,IAAA,MAAM,QAAA,GAAW,mBAAA,CAAoB,MAAA,CAAO,OAAO,CAAA;AAEnD,IAAA,IAAI,CAAC,OAAO,YAAA,EAAc;AACxB,MAAA,OAAO,QAAA;AAAA,IACT;AAEA,IAAA,MAAM,cAAA,GAAiB,MAAA,CAAO,YAAA,CAAa,OAAA,CAAQ,WAAW,CAAA;AAC9D,IAAA,MAAM,EAAE,SAAA,EAAU,GAAI,QAAA,CAAS,qBAAA,EAAsB;AACrD,IAAA,IAAI,cAAA,IAAkB,SAAA,CAAU,QAAA,CAAS,cAAc,CAAA,EAAG;AACxD,MAAA,QAAA,CAAS,YAAY,cAAc,CAAA;AAAA,IACrC;AAEA,IAAA,MAAM,YAAA,GAAe,SAAS,SAAA,EAAU,CAAE,UAAU,CAAC,EAAE,UAAS,KAAM;AACpE,MAAA,IAAI,QAAA,KAAa,MAAA,CAAO,YAAA,CAAa,OAAA,CAAQ,WAAW,CAAA,EAAG;AACzD,QAAA,MAAA,CAAO,YAAA,CAAa,OAAA,CAAQ,WAAA,EAAa,QAAQ,CAAA;AAAA,MACnD;AAAA,IACF,CAAC,CAAA;AAED,IAAA,MAAM,eAAA,GAAkB,CAAC,KAAA,KAAwB;AAC/C,MAAA,IAAI,KAAA,CAAM,QAAQ,WAAA,EAAa;AAC7B,QAAA,MAAM,QAAA,GAAW,YAAA,CAAa,OAAA,CAAQ,WAAW,CAAA,IAAK,MAAA;AACtD,QAAA,IAAI,QAAA,EAAU;AACZ,UAAA,QAAA,CAAS,YAAY,QAAQ,CAAA;AAAA,QAC/B;AAAA,MACF;AAAA,IACF,CAAA;AACA,IAAA,MAAA,CAAO,gBAAA,CAAiB,WAAW,eAAe,CAAA;AAGlD,IAAA,QAAA,CAAS,oBAAA,GAAuB,YAAA;AAChC,IAAA,QAAA,CAAS,gBAAA,GAAmB,eAAA;AAE5B,IAAA,OAAO,QAAA;AAAA,EACT;AAAA,EAEA,UAAA;AAAA,EACA,SAAA;AAAA,EACA,QAAA;AAAA;AAAA,EAGA,oBAAA;AAAA,EACA,gBAAA;AAAA,EAEQ,WAAA,CAAY,WAAqB,eAAA,EAAyB;AAChE,IAAA,IAAA,CAAK,UAAA,GAAa,SAAA;AAClB,IAAA,IAAA,CAAK,SAAA,GAAY,eAAA;AACjB,IAAA,IAAA,CAAK,QAAA,GAAW,IAAI,eAAA,CAAsC;AAAA,MACxD,UAAU,IAAA,CAAK;AAAA,KAChB,CAAA;AAAA,EACH;AAAA,EAEA,qBAAA,GAAiD;AAC/C,IAAA,OAAO,EAAE,SAAA,EAAW,IAAA,CAAK,UAAA,CAAW,OAAM,EAAE;AAAA,EAC9C;AAAA,EAEA,YAAY,QAAA,EAAqC;AAC/C,IAAA,MAAM,MAAM,QAAA,IAAY,gBAAA;AACxB,IAAA,IAAI,GAAA,KAAQ,KAAK,SAAA,EAAW;AAC1B,MAAA;AAAA,IACF;AACA,IAAA,IAAI,OAAO,CAAC,IAAA,CAAK,UAAA,CAAW,QAAA,CAAS,GAAG,CAAA,EAAG;AACzC,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,CAAA,8BAAA,EAAiC,GAAG,CAAA,4BAAA,EAA+B,IAAA,CAAK,UAAA,CAAW,IAAA;AAAA,UACjF;AAAA,SACD,CAAA,CAAA;AAAA,OACH;AAAA,IACF;AACA,IAAA,IAAA,CAAK,SAAA,GAAY,GAAA;AACjB,IAAA,IAAA,CAAK,QAAA,CAAS,IAAA,CAAK,EAAE,QAAA,EAAU,KAAK,CAAA;AAAA,EACtC;AAAA,EAEA,WAAA,GAAoC;AAClC,IAAA,OAAO,EAAE,QAAA,EAAU,IAAA,CAAK,SAAA,EAAU;AAAA,EACpC;AAAA,EAEA,SAAA,GAA8C;AAC5C,IAAA,OAAO,IAAA,CAAK,QAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,OAAA,GAAgB;AACd,IAAA,IAAI,KAAK,oBAAA,EAAsB;AAC7B,MAAA,IAAA,CAAK,qBAAqB,WAAA,EAAY;AACtC,MAAA,IAAA,CAAK,oBAAA,GAAuB,MAAA;AAAA,IAC9B;AACA,IAAA,IAAI,KAAK,gBAAA,EAAkB;AACzB,MAAA,MAAA,CAAO,mBAAA,CAAoB,SAAA,EAAW,IAAA,CAAK,gBAAgB,CAAA;AAC3D,MAAA,IAAA,CAAK,gBAAA,GAAmB,MAAA;AAAA,IAC1B;AAAA,EACF;AACF;;;;"}
@@ -12,23 +12,29 @@ class AppThemeSelector {
12
12
  }
13
13
  const initialThemeId = window.localStorage.getItem(STORAGE_KEY) ?? void 0;
14
14
  selector.setActiveThemeId(initialThemeId);
15
- selector.activeThemeId$().subscribe((themeId) => {
15
+ const subscription = selector.activeThemeId$().subscribe((themeId) => {
16
16
  if (themeId) {
17
17
  window.localStorage.setItem(STORAGE_KEY, themeId);
18
18
  } else {
19
19
  window.localStorage.removeItem(STORAGE_KEY);
20
20
  }
21
21
  });
22
- window.addEventListener("storage", (event) => {
22
+ const storageListener = (event) => {
23
23
  if (event.key === STORAGE_KEY) {
24
24
  const themeId = localStorage.getItem(STORAGE_KEY) ?? void 0;
25
25
  selector.setActiveThemeId(themeId);
26
26
  }
27
- });
27
+ };
28
+ window.addEventListener("storage", storageListener);
29
+ selector.#storageSubscription = subscription;
30
+ selector.#storageListener = storageListener;
28
31
  return selector;
29
32
  }
30
33
  activeThemeId;
31
34
  subject = new BehaviorSubject(void 0);
35
+ // References for cleanup when using createWithStorage
36
+ #storageSubscription;
37
+ #storageListener;
32
38
  getInstalledThemes() {
33
39
  return this.themes.slice();
34
40
  }
@@ -42,6 +48,21 @@ class AppThemeSelector {
42
48
  this.activeThemeId = themeId;
43
49
  this.subject.next(themeId);
44
50
  }
51
+ /**
52
+ * Cleans up resources created by createWithStorage().
53
+ * Call this method when the selector is no longer needed to prevent memory leaks.
54
+ * This is particularly useful in testing scenarios or when the app is unmounted.
55
+ */
56
+ dispose() {
57
+ if (this.#storageSubscription) {
58
+ this.#storageSubscription.unsubscribe();
59
+ this.#storageSubscription = void 0;
60
+ }
61
+ if (this.#storageListener) {
62
+ window.removeEventListener("storage", this.#storageListener);
63
+ this.#storageListener = void 0;
64
+ }
65
+ }
45
66
  }
46
67
 
47
68
  export { AppThemeSelector };
@@ -1 +1 @@
1
- {"version":3,"file":"AppThemeSelector.esm.js","sources":["../../../../../../../../../packages/core-app-api/src/apis/implementations/AppThemeApi/AppThemeSelector.ts"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { AppThemeApi, AppTheme } from '@backstage/core-plugin-api';\nimport { Observable } from '@backstage/types';\nimport { BehaviorSubject } from '../../../lib/subjects';\n\nconst STORAGE_KEY = 'theme';\n\n/**\n * Exposes the themes installed in the app, and permits switching the currently\n * active theme.\n *\n * @public\n */\nexport class AppThemeSelector implements AppThemeApi {\n static createWithStorage(themes: AppTheme[]) {\n const selector = new AppThemeSelector(themes);\n\n if (!window.localStorage) {\n return selector;\n }\n\n const initialThemeId =\n window.localStorage.getItem(STORAGE_KEY) ?? undefined;\n\n selector.setActiveThemeId(initialThemeId);\n\n selector.activeThemeId$().subscribe(themeId => {\n if (themeId) {\n window.localStorage.setItem(STORAGE_KEY, themeId);\n } else {\n window.localStorage.removeItem(STORAGE_KEY);\n }\n });\n\n window.addEventListener('storage', event => {\n if (event.key === STORAGE_KEY) {\n const themeId = localStorage.getItem(STORAGE_KEY) ?? undefined;\n selector.setActiveThemeId(themeId);\n }\n });\n\n return selector;\n }\n\n private activeThemeId: string | undefined;\n private readonly subject = new BehaviorSubject<string | undefined>(undefined);\n\n constructor(private readonly themes: AppTheme[]) {}\n\n getInstalledThemes(): AppTheme[] {\n return this.themes.slice();\n }\n\n activeThemeId$(): Observable<string | undefined> {\n return this.subject;\n }\n\n getActiveThemeId(): string | undefined {\n return this.activeThemeId;\n }\n\n setActiveThemeId(themeId?: string): void {\n this.activeThemeId = themeId;\n this.subject.next(themeId);\n }\n}\n"],"names":[],"mappings":";;AAoBA,MAAM,WAAA,GAAc,OAAA;AAQb,MAAM,gBAAA,CAAwC;AAAA,EAkCnD,YAA6B,MAAA,EAAoB;AAApB,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AAAA,EAAqB;AAAA,EAjClD,OAAO,kBAAkB,MAAA,EAAoB;AAC3C,IAAA,MAAM,QAAA,GAAW,IAAI,gBAAA,CAAiB,MAAM,CAAA;AAE5C,IAAA,IAAI,CAAC,OAAO,YAAA,EAAc;AACxB,MAAA,OAAO,QAAA;AAAA,IACT;AAEA,IAAA,MAAM,cAAA,GACJ,MAAA,CAAO,YAAA,CAAa,OAAA,CAAQ,WAAW,CAAA,IAAK,MAAA;AAE9C,IAAA,QAAA,CAAS,iBAAiB,cAAc,CAAA;AAExC,IAAA,QAAA,CAAS,cAAA,EAAe,CAAE,SAAA,CAAU,CAAA,OAAA,KAAW;AAC7C,MAAA,IAAI,OAAA,EAAS;AACX,QAAA,MAAA,CAAO,YAAA,CAAa,OAAA,CAAQ,WAAA,EAAa,OAAO,CAAA;AAAA,MAClD,CAAA,MAAO;AACL,QAAA,MAAA,CAAO,YAAA,CAAa,WAAW,WAAW,CAAA;AAAA,MAC5C;AAAA,IACF,CAAC,CAAA;AAED,IAAA,MAAA,CAAO,gBAAA,CAAiB,WAAW,CAAA,KAAA,KAAS;AAC1C,MAAA,IAAI,KAAA,CAAM,QAAQ,WAAA,EAAa;AAC7B,QAAA,MAAM,OAAA,GAAU,YAAA,CAAa,OAAA,CAAQ,WAAW,CAAA,IAAK,MAAA;AACrD,QAAA,QAAA,CAAS,iBAAiB,OAAO,CAAA;AAAA,MACnC;AAAA,IACF,CAAC,CAAA;AAED,IAAA,OAAO,QAAA;AAAA,EACT;AAAA,EAEQ,aAAA;AAAA,EACS,OAAA,GAAU,IAAI,eAAA,CAAoC,MAAS,CAAA;AAAA,EAI5E,kBAAA,GAAiC;AAC/B,IAAA,OAAO,IAAA,CAAK,OAAO,KAAA,EAAM;AAAA,EAC3B;AAAA,EAEA,cAAA,GAAiD;AAC/C,IAAA,OAAO,IAAA,CAAK,OAAA;AAAA,EACd;AAAA,EAEA,gBAAA,GAAuC;AACrC,IAAA,OAAO,IAAA,CAAK,aAAA;AAAA,EACd;AAAA,EAEA,iBAAiB,OAAA,EAAwB;AACvC,IAAA,IAAA,CAAK,aAAA,GAAgB,OAAA;AACrB,IAAA,IAAA,CAAK,OAAA,CAAQ,KAAK,OAAO,CAAA;AAAA,EAC3B;AACF;;;;"}
1
+ {"version":3,"file":"AppThemeSelector.esm.js","sources":["../../../../../../../../../packages/core-app-api/src/apis/implementations/AppThemeApi/AppThemeSelector.ts"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { AppThemeApi, AppTheme } from '@backstage/core-plugin-api';\nimport { Observable } from '@backstage/types';\nimport { BehaviorSubject } from '../../../lib/subjects';\n\nconst STORAGE_KEY = 'theme';\n\n/**\n * Exposes the themes installed in the app, and permits switching the currently\n * active theme.\n *\n * @public\n */\nexport class AppThemeSelector implements AppThemeApi {\n static createWithStorage(themes: AppTheme[]) {\n const selector = new AppThemeSelector(themes);\n\n if (!window.localStorage) {\n return selector;\n }\n\n const initialThemeId =\n window.localStorage.getItem(STORAGE_KEY) ?? undefined;\n\n selector.setActiveThemeId(initialThemeId);\n\n const subscription = selector.activeThemeId$().subscribe(themeId => {\n if (themeId) {\n window.localStorage.setItem(STORAGE_KEY, themeId);\n } else {\n window.localStorage.removeItem(STORAGE_KEY);\n }\n });\n\n const storageListener = (event: StorageEvent) => {\n if (event.key === STORAGE_KEY) {\n const themeId = localStorage.getItem(STORAGE_KEY) ?? undefined;\n selector.setActiveThemeId(themeId);\n }\n };\n window.addEventListener('storage', storageListener);\n\n // Store cleanup references for potential disposal\n selector.#storageSubscription = subscription;\n selector.#storageListener = storageListener;\n\n return selector;\n }\n\n private activeThemeId: string | undefined;\n private readonly subject = new BehaviorSubject<string | undefined>(undefined);\n\n // References for cleanup when using createWithStorage\n #storageSubscription?: { unsubscribe(): void };\n #storageListener?: (event: StorageEvent) => void;\n\n constructor(private readonly themes: AppTheme[]) {}\n\n getInstalledThemes(): AppTheme[] {\n return this.themes.slice();\n }\n\n activeThemeId$(): Observable<string | undefined> {\n return this.subject;\n }\n\n getActiveThemeId(): string | undefined {\n return this.activeThemeId;\n }\n\n setActiveThemeId(themeId?: string): void {\n this.activeThemeId = themeId;\n this.subject.next(themeId);\n }\n\n /**\n * Cleans up resources created by createWithStorage().\n * Call this method when the selector is no longer needed to prevent memory leaks.\n * This is particularly useful in testing scenarios or when the app is unmounted.\n */\n dispose(): void {\n if (this.#storageSubscription) {\n this.#storageSubscription.unsubscribe();\n this.#storageSubscription = undefined;\n }\n if (this.#storageListener) {\n window.removeEventListener('storage', this.#storageListener);\n this.#storageListener = undefined;\n }\n }\n}\n"],"names":[],"mappings":";;AAoBA,MAAM,WAAA,GAAc,OAAA;AAQb,MAAM,gBAAA,CAAwC;AAAA,EA2CnD,YAA6B,MAAA,EAAoB;AAApB,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AAAA,EAAqB;AAAA,EA1ClD,OAAO,kBAAkB,MAAA,EAAoB;AAC3C,IAAA,MAAM,QAAA,GAAW,IAAI,gBAAA,CAAiB,MAAM,CAAA;AAE5C,IAAA,IAAI,CAAC,OAAO,YAAA,EAAc;AACxB,MAAA,OAAO,QAAA;AAAA,IACT;AAEA,IAAA,MAAM,cAAA,GACJ,MAAA,CAAO,YAAA,CAAa,OAAA,CAAQ,WAAW,CAAA,IAAK,MAAA;AAE9C,IAAA,QAAA,CAAS,iBAAiB,cAAc,CAAA;AAExC,IAAA,MAAM,YAAA,GAAe,QAAA,CAAS,cAAA,EAAe,CAAE,UAAU,CAAA,OAAA,KAAW;AAClE,MAAA,IAAI,OAAA,EAAS;AACX,QAAA,MAAA,CAAO,YAAA,CAAa,OAAA,CAAQ,WAAA,EAAa,OAAO,CAAA;AAAA,MAClD,CAAA,MAAO;AACL,QAAA,MAAA,CAAO,YAAA,CAAa,WAAW,WAAW,CAAA;AAAA,MAC5C;AAAA,IACF,CAAC,CAAA;AAED,IAAA,MAAM,eAAA,GAAkB,CAAC,KAAA,KAAwB;AAC/C,MAAA,IAAI,KAAA,CAAM,QAAQ,WAAA,EAAa;AAC7B,QAAA,MAAM,OAAA,GAAU,YAAA,CAAa,OAAA,CAAQ,WAAW,CAAA,IAAK,MAAA;AACrD,QAAA,QAAA,CAAS,iBAAiB,OAAO,CAAA;AAAA,MACnC;AAAA,IACF,CAAA;AACA,IAAA,MAAA,CAAO,gBAAA,CAAiB,WAAW,eAAe,CAAA;AAGlD,IAAA,QAAA,CAAS,oBAAA,GAAuB,YAAA;AAChC,IAAA,QAAA,CAAS,gBAAA,GAAmB,eAAA;AAE5B,IAAA,OAAO,QAAA;AAAA,EACT;AAAA,EAEQ,aAAA;AAAA,EACS,OAAA,GAAU,IAAI,eAAA,CAAoC,MAAS,CAAA;AAAA;AAAA,EAG5E,oBAAA;AAAA,EACA,gBAAA;AAAA,EAIA,kBAAA,GAAiC;AAC/B,IAAA,OAAO,IAAA,CAAK,OAAO,KAAA,EAAM;AAAA,EAC3B;AAAA,EAEA,cAAA,GAAiD;AAC/C,IAAA,OAAO,IAAA,CAAK,OAAA;AAAA,EACd;AAAA,EAEA,gBAAA,GAAuC;AACrC,IAAA,OAAO,IAAA,CAAK,aAAA;AAAA,EACd;AAAA,EAEA,iBAAiB,OAAA,EAAwB;AACvC,IAAA,IAAA,CAAK,aAAA,GAAgB,OAAA;AACrB,IAAA,IAAA,CAAK,OAAA,CAAQ,KAAK,OAAO,CAAA;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,OAAA,GAAgB;AACd,IAAA,IAAI,KAAK,oBAAA,EAAsB;AAC7B,MAAA,IAAA,CAAK,qBAAqB,WAAA,EAAY;AACtC,MAAA,IAAA,CAAK,oBAAA,GAAuB,MAAA;AAAA,IAC9B;AACA,IAAA,IAAI,KAAK,gBAAA,EAAkB;AACzB,MAAA,MAAA,CAAO,mBAAA,CAAoB,SAAA,EAAW,IAAA,CAAK,gBAAgB,CAAA;AAC3D,MAAA,IAAA,CAAK,gBAAA,GAAmB,MAAA;AAAA,IAC1B;AAAA,EACF;AACF;;;;"}
@@ -1,11 +1,42 @@
1
+ import { isValidElement, createElement } from 'react';
2
+
1
3
  class DefaultIconsApi {
2
4
  #icons;
5
+ #components = /* @__PURE__ */ new Map();
3
6
  constructor(icons) {
4
- this.#icons = new Map(Object.entries(icons));
7
+ const deprecatedKeys = [];
8
+ this.#icons = new Map(
9
+ Object.entries(icons).map(([key, value]) => {
10
+ if (value === null || isValidElement(value)) {
11
+ return [key, value];
12
+ }
13
+ deprecatedKeys.push(key);
14
+ return [key, createElement(value)];
15
+ })
16
+ );
17
+ if (deprecatedKeys.length > 0) {
18
+ const keys = deprecatedKeys.join(", ");
19
+ console.warn(
20
+ `The following icons were registered as IconComponent, which is deprecated. Use IconElement instead by passing <MyIcon /> rather than MyIcon: ${keys}`
21
+ );
22
+ }
5
23
  }
6
- getIcon(key) {
24
+ icon(key) {
7
25
  return this.#icons.get(key);
8
26
  }
27
+ getIcon(key) {
28
+ let component = this.#components.get(key);
29
+ if (component) {
30
+ return component;
31
+ }
32
+ const el = this.#icons.get(key);
33
+ if (el === void 0) {
34
+ return void 0;
35
+ }
36
+ component = () => el;
37
+ this.#components.set(key, component);
38
+ return component;
39
+ }
9
40
  listIconKeys() {
10
41
  return Array.from(this.#icons.keys());
11
42
  }
@@ -1 +1 @@
1
- {"version":3,"file":"DefaultIconsApi.esm.js","sources":["../../../../../../../../../packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.ts"],"sourcesContent":["/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { IconComponent, IconsApi } from '@backstage/frontend-plugin-api';\n\n/**\n * Implementation for the {@link IconsApi}\n *\n * @internal\n */\nexport class DefaultIconsApi implements IconsApi {\n #icons: Map<string, IconComponent>;\n\n constructor(icons: { [key in string]: IconComponent }) {\n this.#icons = new Map(Object.entries(icons));\n }\n\n getIcon(key: string): IconComponent | undefined {\n return this.#icons.get(key);\n }\n\n listIconKeys(): string[] {\n return Array.from(this.#icons.keys());\n }\n}\n"],"names":[],"mappings":"AAuBO,MAAM,eAAA,CAAoC;AAAA,EAC/C,MAAA;AAAA,EAEA,YAAY,KAAA,EAA2C;AACrD,IAAA,IAAA,CAAK,SAAS,IAAI,GAAA,CAAI,MAAA,CAAO,OAAA,CAAQ,KAAK,CAAC,CAAA;AAAA,EAC7C;AAAA,EAEA,QAAQ,GAAA,EAAwC;AAC9C,IAAA,OAAO,IAAA,CAAK,MAAA,CAAO,GAAA,CAAI,GAAG,CAAA;AAAA,EAC5B;AAAA,EAEA,YAAA,GAAyB;AACvB,IAAA,OAAO,KAAA,CAAM,IAAA,CAAK,IAAA,CAAK,MAAA,CAAO,MAAM,CAAA;AAAA,EACtC;AACF;;;;"}
1
+ {"version":3,"file":"DefaultIconsApi.esm.js","sources":["../../../../../../../../../packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.ts"],"sourcesContent":["/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n IconComponent,\n IconElement,\n IconsApi,\n} from '@backstage/frontend-plugin-api';\nimport { createElement, isValidElement } from 'react';\n\n/**\n * Implementation for the {@link IconsApi}\n *\n * @internal\n */\nexport class DefaultIconsApi implements IconsApi {\n #icons: Map<string, IconElement>;\n #components = new Map<string, IconComponent>();\n\n constructor(icons: { [key in string]: IconComponent | IconElement }) {\n const deprecatedKeys: string[] = [];\n\n this.#icons = new Map(\n Object.entries(icons).map(([key, value]) => {\n if (value === null || isValidElement(value)) {\n return [key, value];\n }\n deprecatedKeys.push(key);\n return [key, createElement(value as IconComponent)];\n }),\n );\n\n if (deprecatedKeys.length > 0) {\n const keys = deprecatedKeys.join(', ');\n // eslint-disable-next-line no-console\n console.warn(\n `The following icons were registered as IconComponent, which is deprecated. Use IconElement instead by passing <MyIcon /> rather than MyIcon: ${keys}`,\n );\n }\n }\n\n icon(key: string): IconElement | undefined {\n return this.#icons.get(key);\n }\n\n getIcon(key: string): IconComponent | undefined {\n let component = this.#components.get(key);\n if (component) {\n return component;\n }\n const el = this.#icons.get(key);\n if (el === undefined) {\n return undefined;\n }\n component = () => el;\n this.#components.set(key, component);\n return component;\n }\n\n listIconKeys(): string[] {\n return Array.from(this.#icons.keys());\n }\n}\n"],"names":[],"mappings":";;AA4BO,MAAM,eAAA,CAAoC;AAAA,EAC/C,MAAA;AAAA,EACA,WAAA,uBAAkB,GAAA,EAA2B;AAAA,EAE7C,YAAY,KAAA,EAAyD;AACnE,IAAA,MAAM,iBAA2B,EAAC;AAElC,IAAA,IAAA,CAAK,SAAS,IAAI,GAAA;AAAA,MAChB,MAAA,CAAO,QAAQ,KAAK,CAAA,CAAE,IAAI,CAAC,CAAC,GAAA,EAAK,KAAK,CAAA,KAAM;AAC1C,QAAA,IAAI,KAAA,KAAU,IAAA,IAAQ,cAAA,CAAe,KAAK,CAAA,EAAG;AAC3C,UAAA,OAAO,CAAC,KAAK,KAAK,CAAA;AAAA,QACpB;AACA,QAAA,cAAA,CAAe,KAAK,GAAG,CAAA;AACvB,QAAA,OAAO,CAAC,GAAA,EAAK,aAAA,CAAc,KAAsB,CAAC,CAAA;AAAA,MACpD,CAAC;AAAA,KACH;AAEA,IAAA,IAAI,cAAA,CAAe,SAAS,CAAA,EAAG;AAC7B,MAAA,MAAM,IAAA,GAAO,cAAA,CAAe,IAAA,CAAK,IAAI,CAAA;AAErC,MAAA,OAAA,CAAQ,IAAA;AAAA,QACN,gJAAgJ,IAAI,CAAA;AAAA,OACtJ;AAAA,IACF;AAAA,EACF;AAAA,EAEA,KAAK,GAAA,EAAsC;AACzC,IAAA,OAAO,IAAA,CAAK,MAAA,CAAO,GAAA,CAAI,GAAG,CAAA;AAAA,EAC5B;AAAA,EAEA,QAAQ,GAAA,EAAwC;AAC9C,IAAA,IAAI,SAAA,GAAY,IAAA,CAAK,WAAA,CAAY,GAAA,CAAI,GAAG,CAAA;AACxC,IAAA,IAAI,SAAA,EAAW;AACb,MAAA,OAAO,SAAA;AAAA,IACT;AACA,IAAA,MAAM,EAAA,GAAK,IAAA,CAAK,MAAA,CAAO,GAAA,CAAI,GAAG,CAAA;AAC9B,IAAA,IAAI,OAAO,MAAA,EAAW;AACpB,MAAA,OAAO,MAAA;AAAA,IACT;AACA,IAAA,SAAA,GAAY,MAAM,EAAA;AAClB,IAAA,IAAA,CAAK,WAAA,CAAY,GAAA,CAAI,GAAA,EAAK,SAAS,CAAA;AACnC,IAAA,OAAO,SAAA;AAAA,EACT;AAAA,EAEA,YAAA,GAAyB;AACvB,IAAA,OAAO,KAAA,CAAM,IAAA,CAAK,IAAA,CAAK,MAAA,CAAO,MAAM,CAAA;AAAA,EACtC;AACF;;;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"InternalFrontendPlugin.esm.js","sources":["../../../../../../../packages/frontend-internal/src/wiring/InternalFrontendPlugin.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n Extension,\n FeatureFlagConfig,\n OverridableFrontendPlugin,\n} from '@backstage/frontend-plugin-api';\nimport { JsonObject } from '@backstage/types';\nimport { OpaqueType } from '@internal/opaque';\n\nexport const OpaqueFrontendPlugin = OpaqueType.create<{\n public: OverridableFrontendPlugin;\n versions: {\n readonly version: 'v1';\n readonly extensions: Extension<unknown>[];\n readonly featureFlags: FeatureFlagConfig[];\n readonly infoOptions?: {\n packageJson?: () => Promise<JsonObject>;\n manifest?: () => Promise<JsonObject>;\n };\n };\n}>({\n type: '@backstage/FrontendPlugin',\n versions: ['v1'],\n});\n"],"names":[],"mappings":";;AAwBoC,WAAW,MAAA,CAW5C;AAAA,EACD,IAAA,EAAM,2BAAA;AAAA,EACN,QAAA,EAAU,CAAC,IAAI;AACjB,CAAC"}
1
+ {"version":3,"file":"InternalFrontendPlugin.esm.js","sources":["../../../../../../../packages/frontend-internal/src/wiring/InternalFrontendPlugin.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n Extension,\n FeatureFlagConfig,\n IconElement,\n OverridableFrontendPlugin,\n} from '@backstage/frontend-plugin-api';\nimport { JsonObject } from '@backstage/types';\nimport { OpaqueType } from '@internal/opaque';\n\nexport const OpaqueFrontendPlugin = OpaqueType.create<{\n public: OverridableFrontendPlugin;\n versions: {\n readonly version: 'v1';\n readonly title?: string;\n readonly icon?: IconElement;\n readonly extensions: Extension<unknown>[];\n readonly featureFlags: FeatureFlagConfig[];\n readonly infoOptions?: {\n packageJson?: () => Promise<JsonObject>;\n manifest?: () => Promise<JsonObject>;\n };\n };\n}>({\n type: '@backstage/FrontendPlugin',\n versions: ['v1'],\n});\n"],"names":[],"mappings":";;AAyBoC,WAAW,MAAA,CAa5C;AAAA,EACD,IAAA,EAAM,2BAAA;AAAA,EACN,QAAA,EAAU,CAAC,IAAI;AACjB,CAAC"}
@@ -14,8 +14,9 @@ import { TranslationsApi } from './extensions/TranslationsApi.esm.js';
14
14
  import { DefaultSignInPage } from './extensions/DefaultSignInPage.esm.js';
15
15
  import { dialogDisplayAppRootElement } from './extensions/DialogDisplay.esm.js';
16
16
  import { oauthRequestDialogAppRootElement, alertDisplayAppRootElement } from './extensions/elements.esm.js';
17
- import { Progress, NotFoundErrorPage, ErrorDisplay } from './extensions/components.esm.js';
17
+ import { Progress, NotFoundErrorPage, ErrorDisplay, PageLayout } from './extensions/components.esm.js';
18
18
  import { PluginWrapperApi } from './extensions/PluginWrapperApi.esm.js';
19
+ import { PluginHeaderActionsApi } from './extensions/PluginHeaderActionsApi.esm.js';
19
20
  import { apis } from './defaultApis.esm.js';
20
21
 
21
22
  const appPlugin = createFrontendPlugin({
@@ -36,6 +37,7 @@ const appPlugin = createFrontendPlugin({
36
37
  IconsApi,
37
38
  FeatureFlagsApi,
38
39
  PluginWrapperApi,
40
+ PluginHeaderActionsApi,
39
41
  TranslationsApi,
40
42
  DefaultSignInPage,
41
43
  oauthRequestDialogAppRootElement,
@@ -44,6 +46,7 @@ const appPlugin = createFrontendPlugin({
44
46
  Progress,
45
47
  NotFoundErrorPage,
46
48
  ErrorDisplay,
49
+ PageLayout,
47
50
  LegacyComponentsApi
48
51
  ]
49
52
  });
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.esm.js","sources":["../src/plugin.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { createFrontendPlugin } from '@backstage/frontend-plugin-api';\nimport {\n App,\n AppLanguageApi,\n AppLayout,\n AppNav,\n AppRoot,\n AppRoutes,\n AppThemeApi,\n DarkTheme,\n LightTheme,\n SwappableComponentsApi,\n IconsApi,\n FeatureFlagsApi,\n PluginWrapperApi,\n TranslationsApi,\n oauthRequestDialogAppRootElement,\n alertDisplayAppRootElement,\n DefaultSignInPage,\n dialogDisplayAppRootElement,\n Progress,\n NotFoundErrorPage,\n ErrorDisplay,\n LegacyComponentsApi,\n} from './extensions';\nimport { apis } from './defaultApis';\n\n/** @public */\nexport const appPlugin = createFrontendPlugin({\n pluginId: 'app',\n info: { packageJson: () => import('../package.json') },\n extensions: [\n ...apis,\n App,\n AppLanguageApi,\n AppLayout,\n AppNav,\n AppRoot,\n AppRoutes,\n AppThemeApi,\n DarkTheme,\n LightTheme,\n SwappableComponentsApi,\n IconsApi,\n FeatureFlagsApi,\n PluginWrapperApi,\n TranslationsApi,\n DefaultSignInPage,\n oauthRequestDialogAppRootElement,\n alertDisplayAppRootElement,\n dialogDisplayAppRootElement,\n Progress,\n NotFoundErrorPage,\n ErrorDisplay,\n LegacyComponentsApi,\n ],\n});\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;AA4CO,MAAM,YAAY,oBAAA,CAAqB;AAAA,EAC5C,QAAA,EAAU,KAAA;AAAA,EACV,MAAM,EAAE,WAAA,EAAa,MAAM,OAAO,mCAAiB,CAAA,EAAE;AAAA,EACrD,UAAA,EAAY;AAAA,IACV,GAAG,IAAA;AAAA,IACH,GAAA;AAAA,IACA,cAAA;AAAA,IACA,SAAA;AAAA,IACA,MAAA;AAAA,IACA,OAAA;AAAA,IACA,SAAA;AAAA,IACA,WAAA;AAAA,IACA,SAAA;AAAA,IACA,UAAA;AAAA,IACA,sBAAA;AAAA,IACA,QAAA;AAAA,IACA,eAAA;AAAA,IACA,gBAAA;AAAA,IACA,eAAA;AAAA,IACA,iBAAA;AAAA,IACA,gCAAA;AAAA,IACA,0BAAA;AAAA,IACA,2BAAA;AAAA,IACA,QAAA;AAAA,IACA,iBAAA;AAAA,IACA,YAAA;AAAA,IACA;AAAA;AAEJ,CAAC;;;;"}
1
+ {"version":3,"file":"plugin.esm.js","sources":["../src/plugin.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { createFrontendPlugin } from '@backstage/frontend-plugin-api';\nimport {\n App,\n AppLanguageApi,\n AppLayout,\n AppNav,\n AppRoot,\n AppRoutes,\n AppThemeApi,\n DarkTheme,\n LightTheme,\n SwappableComponentsApi,\n IconsApi,\n FeatureFlagsApi,\n PluginWrapperApi,\n PluginHeaderActionsApi,\n TranslationsApi,\n oauthRequestDialogAppRootElement,\n alertDisplayAppRootElement,\n DefaultSignInPage,\n dialogDisplayAppRootElement,\n Progress,\n NotFoundErrorPage,\n ErrorDisplay,\n PageLayout,\n LegacyComponentsApi,\n} from './extensions';\nimport { apis } from './defaultApis';\n\n/** @public */\nexport const appPlugin = createFrontendPlugin({\n pluginId: 'app',\n info: { packageJson: () => import('../package.json') },\n extensions: [\n ...apis,\n App,\n AppLanguageApi,\n AppLayout,\n AppNav,\n AppRoot,\n AppRoutes,\n AppThemeApi,\n DarkTheme,\n LightTheme,\n SwappableComponentsApi,\n IconsApi,\n FeatureFlagsApi,\n PluginWrapperApi,\n PluginHeaderActionsApi,\n TranslationsApi,\n DefaultSignInPage,\n oauthRequestDialogAppRootElement,\n alertDisplayAppRootElement,\n dialogDisplayAppRootElement,\n Progress,\n NotFoundErrorPage,\n ErrorDisplay,\n PageLayout,\n LegacyComponentsApi,\n ],\n});\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;AA8CO,MAAM,YAAY,oBAAA,CAAqB;AAAA,EAC5C,QAAA,EAAU,KAAA;AAAA,EACV,MAAM,EAAE,WAAA,EAAa,MAAM,OAAO,mCAAiB,CAAA,EAAE;AAAA,EACrD,UAAA,EAAY;AAAA,IACV,GAAG,IAAA;AAAA,IACH,GAAA;AAAA,IACA,cAAA;AAAA,IACA,SAAA;AAAA,IACA,MAAA;AAAA,IACA,OAAA;AAAA,IACA,SAAA;AAAA,IACA,WAAA;AAAA,IACA,SAAA;AAAA,IACA,UAAA;AAAA,IACA,sBAAA;AAAA,IACA,QAAA;AAAA,IACA,eAAA;AAAA,IACA,gBAAA;AAAA,IACA,sBAAA;AAAA,IACA,eAAA;AAAA,IACA,iBAAA;AAAA,IACA,gCAAA;AAAA,IACA,0BAAA;AAAA,IACA,2BAAA;AAAA,IACA,QAAA;AAAA,IACA,iBAAA;AAAA,IACA,YAAA;AAAA,IACA,UAAA;AAAA,IACA;AAAA;AAEJ,CAAC;;;;"}
@@ -1,5 +1,5 @@
1
1
  var name = "@backstage/plugin-app";
2
- var version = "0.4.0-next.1";
2
+ var version = "0.4.0";
3
3
  var backstage = {
4
4
  role: "frontend-plugin",
5
5
  pluginId: "app",
@@ -58,6 +58,7 @@ var dependencies = {
58
58
  "@backstage/plugin-permission-react": "workspace:^",
59
59
  "@backstage/theme": "workspace:^",
60
60
  "@backstage/types": "workspace:^",
61
+ "@backstage/ui": "workspace:^",
61
62
  "@backstage/version-bridge": "workspace:^",
62
63
  "@material-ui/core": "^4.9.13",
63
64
  "@material-ui/icons": "^4.9.1",
@@ -79,13 +80,13 @@ var devDependencies = {
79
80
  msw: "^1.0.0",
80
81
  react: "^18.0.2",
81
82
  "react-dom": "^18.0.2",
82
- "react-router-dom": "^6.3.0"
83
+ "react-router-dom": "^6.30.2"
83
84
  };
84
85
  var peerDependencies = {
85
86
  "@types/react": "^17.0.0 || ^18.0.0",
86
87
  react: "^17.0.0 || ^18.0.0",
87
88
  "react-dom": "^17.0.0 || ^18.0.0",
88
- "react-router-dom": "^6.3.0"
89
+ "react-router-dom": "^6.30.2"
89
90
  };
90
91
  var peerDependenciesMeta = {
91
92
  "@types/react": {
@@ -1 +1 @@
1
- {"version":3,"file":"package.json.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"package.json.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backstage/plugin-app",
3
- "version": "0.4.0-next.1",
3
+ "version": "0.4.0",
4
4
  "backstage": {
5
5
  "role": "frontend-plugin",
6
6
  "pluginId": "app",
@@ -63,15 +63,16 @@
63
63
  "test": "backstage-cli package test"
64
64
  },
65
65
  "dependencies": {
66
- "@backstage/core-components": "0.18.7-next.1",
67
- "@backstage/core-plugin-api": "1.12.3-next.0",
68
- "@backstage/frontend-plugin-api": "0.14.0-next.1",
69
- "@backstage/integration-react": "1.2.15-next.1",
70
- "@backstage/plugin-app-react": "0.1.1-next.0",
71
- "@backstage/plugin-permission-react": "0.4.40-next.0",
72
- "@backstage/theme": "0.7.2-next.0",
73
- "@backstage/types": "1.2.2",
74
- "@backstage/version-bridge": "1.0.11",
66
+ "@backstage/core-components": "^0.18.7",
67
+ "@backstage/core-plugin-api": "^1.12.3",
68
+ "@backstage/frontend-plugin-api": "^0.14.0",
69
+ "@backstage/integration-react": "^1.2.15",
70
+ "@backstage/plugin-app-react": "^0.2.0",
71
+ "@backstage/plugin-permission-react": "^0.4.40",
72
+ "@backstage/theme": "^0.7.2",
73
+ "@backstage/types": "^1.2.2",
74
+ "@backstage/ui": "^0.12.0",
75
+ "@backstage/version-bridge": "^1.0.12",
75
76
  "@material-ui/core": "^4.9.13",
76
77
  "@material-ui/icons": "^4.9.1",
77
78
  "@material-ui/lab": "^4.0.0-alpha.61",
@@ -80,11 +81,11 @@
80
81
  "zod": "^3.25.76"
81
82
  },
82
83
  "devDependencies": {
83
- "@backstage/cli": "0.35.4-next.1",
84
- "@backstage/dev-utils": "1.1.20-next.1",
85
- "@backstage/frontend-defaults": "0.4.0-next.1",
86
- "@backstage/frontend-test-utils": "0.4.6-next.1",
87
- "@backstage/test-utils": "1.7.15-next.1",
84
+ "@backstage/cli": "^0.35.4",
85
+ "@backstage/dev-utils": "^1.1.20",
86
+ "@backstage/frontend-defaults": "^0.4.0",
87
+ "@backstage/frontend-test-utils": "^0.5.0",
88
+ "@backstage/test-utils": "^1.7.15",
88
89
  "@testing-library/jest-dom": "^6.0.0",
89
90
  "@testing-library/react": "^16.0.0",
90
91
  "@testing-library/user-event": "^14.0.0",
@@ -92,13 +93,13 @@
92
93
  "msw": "^1.0.0",
93
94
  "react": "^18.0.2",
94
95
  "react-dom": "^18.0.2",
95
- "react-router-dom": "^6.3.0"
96
+ "react-router-dom": "^6.30.2"
96
97
  },
97
98
  "peerDependencies": {
98
99
  "@types/react": "^17.0.0 || ^18.0.0",
99
100
  "react": "^17.0.0 || ^18.0.0",
100
101
  "react-dom": "^17.0.0 || ^18.0.0",
101
- "react-router-dom": "^6.3.0"
102
+ "react-router-dom": "^6.30.2"
102
103
  },
103
104
  "peerDependenciesMeta": {
104
105
  "@types/react": {