@backstage/plugin-user-settings 0.8.32-next.1 → 0.9.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,44 @@
1
1
  # @backstage/plugin-user-settings
2
2
 
3
+ ## 0.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 104ca74: User-settings will now use DataLoader to batch consecutive calls into one API call to improve performance
8
+
9
+ ### Patch Changes
10
+
11
+ - 018ca87: Added `title` and `icon` to the plugin definition for the new frontend system.
12
+ - a7e0d50: Updated `react-router-dom` peer dependency to `^6.30.2` and explicitly disabled v7 future flags to suppress deprecation warnings.
13
+ - Updated dependencies
14
+ - @backstage/plugin-catalog-react@2.0.0
15
+ - @backstage/core-components@0.18.7
16
+ - @backstage/core-app-api@1.19.5
17
+ - @backstage/theme@0.7.2
18
+ - @backstage/frontend-plugin-api@0.14.0
19
+ - @backstage/core-plugin-api@1.12.3
20
+ - @backstage/plugin-user-settings-common@0.1.0
21
+ - @backstage/plugin-signals-react@0.0.19
22
+
23
+ ## 0.9.0-next.2
24
+
25
+ ### Minor Changes
26
+
27
+ - 104ca74: User-settings will now use DataLoader to batch consecutive calls into one API call to improve performance
28
+
29
+ ### Patch Changes
30
+
31
+ - 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.
32
+ - Updated dependencies
33
+ - @backstage/plugin-catalog-react@2.0.0-next.2
34
+ - @backstage/core-app-api@1.19.5-next.1
35
+ - @backstage/plugin-user-settings-common@0.1.0-next.0
36
+ - @backstage/frontend-plugin-api@0.14.0-next.2
37
+ - @backstage/core-components@0.18.7-next.2
38
+ - @backstage/core-plugin-api@1.12.3-next.1
39
+ - @backstage/plugin-signals-react@0.0.19-next.1
40
+ - @backstage/theme@0.7.2-next.1
41
+
3
42
  ## 0.8.32-next.1
4
43
 
5
44
  ### Patch Changes
package/README.md CHANGED
@@ -90,7 +90,7 @@ import {
90
90
  To standardize the UI of all setting tabs,
91
91
  make sure you use a similar component structure as the other tabs.
92
92
  You can take a look at
93
- [the example extra tab](https://github.com/backstage/backstage/blob/master/packages/app/src/components/advancedSettings/AdvancedSettings.tsx)
93
+ [the example extra tab](https://github.com/backstage/backstage/blob/master/packages/app-legacy/src/components/advancedSettings/AdvancedSettings.tsx)
94
94
  we have created in Backstage's example app.
95
95
 
96
96
  To change the layout altogether, create a custom page in `packages/app/src/components/user-settings/SettingsPage.tsx`:
package/dist/alpha.d.ts CHANGED
@@ -42,10 +42,10 @@ declare const userSettingsTranslationRef: _backstage_frontend_plugin_api.Transla
42
42
  readonly "emptyProviders.action.title": "Open app-config.yaml and make the changes as highlighted below:";
43
43
  readonly "emptyProviders.action.readMoreButtonTitle": "Read More";
44
44
  readonly "emptyProviders.description": "You can add Authentication Providers to Backstage which allows you to use these providers to authenticate yourself.";
45
- readonly "providerSettingsItem.title.signIn": "Sign in to {{title}}";
46
45
  readonly "providerSettingsItem.title.signOut": "Sign out from {{title}}";
47
- readonly "providerSettingsItem.buttonTitle.signIn": "Sign in";
46
+ readonly "providerSettingsItem.title.signIn": "Sign in to {{title}}";
48
47
  readonly "providerSettingsItem.buttonTitle.signOut": "Sign out";
48
+ readonly "providerSettingsItem.buttonTitle.signIn": "Sign in";
49
49
  readonly "authProviders.title": "Available Providers";
50
50
  readonly "defaultSettingsPage.tabsTitle.featureFlags": "Feature Flags";
51
51
  readonly "defaultSettingsPage.tabsTitle.authProviders": "Authentication Providers";
@@ -100,14 +100,31 @@ declare const _default: _backstage_frontend_plugin_api.OverridableFrontendPlugin
100
100
  "page:user-settings": _backstage_frontend_plugin_api.OverridableExtensionDefinition<{
101
101
  config: {
102
102
  path: string | undefined;
103
+ title: string | undefined;
103
104
  };
104
105
  configInput: {
106
+ title?: string | undefined;
105
107
  path?: string | undefined;
106
108
  };
107
- output: _backstage_frontend_plugin_api.ExtensionDataRef<string, "core.routing.path", {}> | _backstage_frontend_plugin_api.ExtensionDataRef<react.JSX.Element, "core.reactElement", {}> | _backstage_frontend_plugin_api.ExtensionDataRef<_backstage_frontend_plugin_api.RouteRef<_backstage_frontend_plugin_api.AnyRouteRefParams>, "core.routing.ref", {
109
+ output: _backstage_frontend_plugin_api.ExtensionDataRef<string, "core.routing.path", {}> | _backstage_frontend_plugin_api.ExtensionDataRef<_backstage_frontend_plugin_api.RouteRef<_backstage_frontend_plugin_api.AnyRouteRefParams>, "core.routing.ref", {
110
+ optional: true;
111
+ }> | _backstage_frontend_plugin_api.ExtensionDataRef<react.JSX.Element, "core.reactElement", {}> | _backstage_frontend_plugin_api.ExtensionDataRef<string, "core.title", {
112
+ optional: true;
113
+ }> | _backstage_frontend_plugin_api.ExtensionDataRef<_backstage_frontend_plugin_api.IconElement, "core.icon", {
108
114
  optional: true;
109
115
  }>;
110
116
  inputs: {
117
+ pages: _backstage_frontend_plugin_api.ExtensionInput<_backstage_frontend_plugin_api.ConfigurableExtensionDataRef<react.JSX.Element, "core.reactElement", {}> | _backstage_frontend_plugin_api.ConfigurableExtensionDataRef<string, "core.routing.path", {}> | _backstage_frontend_plugin_api.ConfigurableExtensionDataRef<_backstage_frontend_plugin_api.RouteRef<_backstage_frontend_plugin_api.AnyRouteRefParams>, "core.routing.ref", {
118
+ optional: true;
119
+ }> | _backstage_frontend_plugin_api.ConfigurableExtensionDataRef<string, "core.title", {
120
+ optional: true;
121
+ }> | _backstage_frontend_plugin_api.ConfigurableExtensionDataRef<_backstage_frontend_plugin_api.IconElement, "core.icon", {
122
+ optional: true;
123
+ }>, {
124
+ singleton: false;
125
+ optional: false;
126
+ internal: false;
127
+ }>;
111
128
  providerSettings: _backstage_frontend_plugin_api.ExtensionInput<_backstage_frontend_plugin_api.ConfigurableExtensionDataRef<react.JSX.Element, "core.reactElement", {}>, {
112
129
  singleton: true;
113
130
  optional: true;
@@ -119,8 +136,11 @@ declare const _default: _backstage_frontend_plugin_api.OverridableFrontendPlugin
119
136
  params: {
120
137
  defaultPath?: [Error: `Use the 'path' param instead`];
121
138
  path: string;
122
- loader: () => Promise<JSX.Element>;
139
+ title?: string;
140
+ icon?: _backstage_frontend_plugin_api.IconElement;
141
+ loader?: () => Promise<react.JSX.Element>;
123
142
  routeRef?: _backstage_frontend_plugin_api.RouteRef;
143
+ noHeader?: boolean;
124
144
  };
125
145
  }>;
126
146
  }>;
package/dist/alpha.esm.js CHANGED
@@ -35,6 +35,8 @@ const settingsNavItem = NavItemBlueprint.make({
35
35
  });
36
36
  var alpha = createFrontendPlugin({
37
37
  pluginId: "user-settings",
38
+ title: "Settings",
39
+ icon: /* @__PURE__ */ jsx(SettingsIcon, {}),
38
40
  info: { packageJson: () => import('./package.json.esm.js') },
39
41
  extensions: [userSettingsPage, settingsNavItem],
40
42
  routes: {
@@ -1 +1 @@
1
- {"version":3,"file":"alpha.esm.js","sources":["../src/alpha.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 */\nimport {\n coreExtensionData,\n createExtensionInput,\n createFrontendPlugin,\n PageBlueprint,\n NavItemBlueprint,\n} from '@backstage/frontend-plugin-api';\nimport SettingsIcon from '@material-ui/icons/Settings';\nimport { settingsRouteRef } from './plugin';\n\nexport * from './translation';\n\nconst userSettingsPage = PageBlueprint.makeWithOverrides({\n inputs: {\n providerSettings: createExtensionInput([coreExtensionData.reactElement], {\n singleton: true,\n optional: true,\n }),\n },\n factory(originalFactory, { inputs }) {\n return originalFactory({\n path: '/settings',\n routeRef: settingsRouteRef,\n loader: () =>\n import('./components/SettingsPage').then(m => (\n <m.SettingsPage\n providerSettings={inputs.providerSettings?.get(\n coreExtensionData.reactElement,\n )}\n />\n )),\n });\n },\n});\n\n/** @alpha */\nexport const settingsNavItem = NavItemBlueprint.make({\n params: {\n routeRef: settingsRouteRef,\n title: 'Settings',\n icon: SettingsIcon,\n },\n});\n\n/**\n * @alpha\n */\nexport default createFrontendPlugin({\n pluginId: 'user-settings',\n info: { packageJson: () => import('../package.json') },\n extensions: [userSettingsPage, settingsNavItem],\n routes: {\n root: settingsRouteRef,\n },\n});\n"],"names":[],"mappings":";;;;;;AA2BA,MAAM,gBAAA,GAAmB,cAAc,iBAAA,CAAkB;AAAA,EACvD,MAAA,EAAQ;AAAA,IACN,gBAAA,EAAkB,oBAAA,CAAqB,CAAC,iBAAA,CAAkB,YAAY,CAAA,EAAG;AAAA,MACvE,SAAA,EAAW,IAAA;AAAA,MACX,QAAA,EAAU;AAAA,KACX;AAAA,GACH;AAAA,EACA,OAAA,CAAQ,eAAA,EAAiB,EAAE,MAAA,EAAO,EAAG;AACnC,IAAA,OAAO,eAAA,CAAgB;AAAA,MACrB,IAAA,EAAM,WAAA;AAAA,MACN,QAAA,EAAU,gBAAA;AAAA,MACV,QAAQ,MACN,OAAO,wCAA2B,CAAA,CAAE,KAAK,CAAA,CAAA,qBACvC,GAAA;AAAA,QAAC,CAAA,CAAE,YAAA;AAAA,QAAF;AAAA,UACC,gBAAA,EAAkB,OAAO,gBAAA,EAAkB,GAAA;AAAA,YACzC,iBAAA,CAAkB;AAAA;AACpB;AAAA,OAEH;AAAA,KACJ,CAAA;AAAA,EACH;AACF,CAAC,CAAA;AAGM,MAAM,eAAA,GAAkB,iBAAiB,IAAA,CAAK;AAAA,EACnD,MAAA,EAAQ;AAAA,IACN,QAAA,EAAU,gBAAA;AAAA,IACV,KAAA,EAAO,UAAA;AAAA,IACP,IAAA,EAAM;AAAA;AAEV,CAAC;AAKD,YAAe,oBAAA,CAAqB;AAAA,EAClC,QAAA,EAAU,eAAA;AAAA,EACV,MAAM,EAAE,WAAA,EAAa,MAAM,OAAO,uBAAiB,CAAA,EAAE;AAAA,EACrD,UAAA,EAAY,CAAC,gBAAA,EAAkB,eAAe,CAAA;AAAA,EAC9C,MAAA,EAAQ;AAAA,IACN,IAAA,EAAM;AAAA;AAEV,CAAC,CAAA;;;;"}
1
+ {"version":3,"file":"alpha.esm.js","sources":["../src/alpha.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 */\nimport {\n coreExtensionData,\n createExtensionInput,\n createFrontendPlugin,\n PageBlueprint,\n NavItemBlueprint,\n} from '@backstage/frontend-plugin-api';\nimport SettingsIcon from '@material-ui/icons/Settings';\nimport { settingsRouteRef } from './plugin';\n\nexport * from './translation';\n\nconst userSettingsPage = PageBlueprint.makeWithOverrides({\n inputs: {\n providerSettings: createExtensionInput([coreExtensionData.reactElement], {\n singleton: true,\n optional: true,\n }),\n },\n factory(originalFactory, { inputs }) {\n return originalFactory({\n path: '/settings',\n routeRef: settingsRouteRef,\n loader: () =>\n import('./components/SettingsPage').then(m => (\n <m.SettingsPage\n providerSettings={inputs.providerSettings?.get(\n coreExtensionData.reactElement,\n )}\n />\n )),\n });\n },\n});\n\n/** @alpha */\nexport const settingsNavItem = NavItemBlueprint.make({\n params: {\n routeRef: settingsRouteRef,\n title: 'Settings',\n icon: SettingsIcon,\n },\n});\n\n/**\n * @alpha\n */\nexport default createFrontendPlugin({\n pluginId: 'user-settings',\n title: 'Settings',\n icon: <SettingsIcon />,\n info: { packageJson: () => import('../package.json') },\n extensions: [userSettingsPage, settingsNavItem],\n routes: {\n root: settingsRouteRef,\n },\n});\n"],"names":[],"mappings":";;;;;;AA2BA,MAAM,gBAAA,GAAmB,cAAc,iBAAA,CAAkB;AAAA,EACvD,MAAA,EAAQ;AAAA,IACN,gBAAA,EAAkB,oBAAA,CAAqB,CAAC,iBAAA,CAAkB,YAAY,CAAA,EAAG;AAAA,MACvE,SAAA,EAAW,IAAA;AAAA,MACX,QAAA,EAAU;AAAA,KACX;AAAA,GACH;AAAA,EACA,OAAA,CAAQ,eAAA,EAAiB,EAAE,MAAA,EAAO,EAAG;AACnC,IAAA,OAAO,eAAA,CAAgB;AAAA,MACrB,IAAA,EAAM,WAAA;AAAA,MACN,QAAA,EAAU,gBAAA;AAAA,MACV,QAAQ,MACN,OAAO,wCAA2B,CAAA,CAAE,KAAK,CAAA,CAAA,qBACvC,GAAA;AAAA,QAAC,CAAA,CAAE,YAAA;AAAA,QAAF;AAAA,UACC,gBAAA,EAAkB,OAAO,gBAAA,EAAkB,GAAA;AAAA,YACzC,iBAAA,CAAkB;AAAA;AACpB;AAAA,OAEH;AAAA,KACJ,CAAA;AAAA,EACH;AACF,CAAC,CAAA;AAGM,MAAM,eAAA,GAAkB,iBAAiB,IAAA,CAAK;AAAA,EACnD,MAAA,EAAQ;AAAA,IACN,QAAA,EAAU,gBAAA;AAAA,IACV,KAAA,EAAO,UAAA;AAAA,IACP,IAAA,EAAM;AAAA;AAEV,CAAC;AAKD,YAAe,oBAAA,CAAqB;AAAA,EAClC,QAAA,EAAU,eAAA;AAAA,EACV,KAAA,EAAO,UAAA;AAAA,EACP,IAAA,sBAAO,YAAA,EAAA,EAAa,CAAA;AAAA,EACpB,MAAM,EAAE,WAAA,EAAa,MAAM,OAAO,uBAAiB,CAAA,EAAE;AAAA,EACrD,UAAA,EAAY,CAAC,gBAAA,EAAkB,eAAe,CAAA;AAAA,EAC9C,MAAA,EAAQ;AAAA,IACN,IAAA,EAAM;AAAA;AAEV,CAAC,CAAA;;;;"}
@@ -0,0 +1,35 @@
1
+ class CacheMap extends Map {
2
+ #ttlMs;
3
+ #timestamps = /* @__PURE__ */ new Map();
4
+ constructor(ttlMs) {
5
+ super();
6
+ this.#ttlMs = ttlMs;
7
+ }
8
+ set(key, value) {
9
+ const result = super.set(key, value);
10
+ this.#timestamps.set(key, Date.now());
11
+ return result;
12
+ }
13
+ get(key) {
14
+ if (!this.has(key)) {
15
+ return void 0;
16
+ }
17
+ const timestamp = this.#timestamps.get(key);
18
+ if (Date.now() - timestamp > this.#ttlMs) {
19
+ this.delete(key);
20
+ return void 0;
21
+ }
22
+ return super.get(key);
23
+ }
24
+ delete(key) {
25
+ this.#timestamps.delete(key);
26
+ return super.delete(key);
27
+ }
28
+ clear() {
29
+ this.#timestamps.clear();
30
+ return super.clear();
31
+ }
32
+ }
33
+
34
+ export { CacheMap };
35
+ //# sourceMappingURL=CacheMap.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CacheMap.esm.js","sources":["../../../src/apis/StorageApi/CacheMap.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\n/**\n * A Map<K, V> with TTL (time-to-live) support, for the Dataloader of user settings.\n */\nexport class CacheMap<K, V> extends Map<K, V> {\n #ttlMs: number;\n #timestamps: Map<K, number> = new Map();\n\n constructor(ttlMs: number) {\n super();\n this.#ttlMs = ttlMs;\n }\n\n set(key: K, value: V) {\n const result = super.set(key, value);\n this.#timestamps.set(key, Date.now());\n return result;\n }\n\n get(key: K) {\n if (!this.has(key)) {\n return undefined;\n }\n const timestamp = this.#timestamps.get(key)!;\n if (Date.now() - timestamp > this.#ttlMs) {\n this.delete(key);\n return undefined;\n }\n return super.get(key);\n }\n\n delete(key: K) {\n this.#timestamps.delete(key);\n return super.delete(key);\n }\n\n clear() {\n this.#timestamps.clear();\n return super.clear();\n }\n}\n"],"names":[],"mappings":"AAmBO,MAAM,iBAAuB,GAAA,CAAU;AAAA,EAC5C,MAAA;AAAA,EACA,WAAA,uBAAkC,GAAA,EAAI;AAAA,EAEtC,YAAY,KAAA,EAAe;AACzB,IAAA,KAAA,EAAM;AACN,IAAA,IAAA,CAAK,MAAA,GAAS,KAAA;AAAA,EAChB;AAAA,EAEA,GAAA,CAAI,KAAQ,KAAA,EAAU;AACpB,IAAA,MAAM,MAAA,GAAS,KAAA,CAAM,GAAA,CAAI,GAAA,EAAK,KAAK,CAAA;AACnC,IAAA,IAAA,CAAK,WAAA,CAAY,GAAA,CAAI,GAAA,EAAK,IAAA,CAAK,KAAK,CAAA;AACpC,IAAA,OAAO,MAAA;AAAA,EACT;AAAA,EAEA,IAAI,GAAA,EAAQ;AACV,IAAA,IAAI,CAAC,IAAA,CAAK,GAAA,CAAI,GAAG,CAAA,EAAG;AAClB,MAAA,OAAO,MAAA;AAAA,IACT;AACA,IAAA,MAAM,SAAA,GAAY,IAAA,CAAK,WAAA,CAAY,GAAA,CAAI,GAAG,CAAA;AAC1C,IAAA,IAAI,IAAA,CAAK,GAAA,EAAI,GAAI,SAAA,GAAY,KAAK,MAAA,EAAQ;AACxC,MAAA,IAAA,CAAK,OAAO,GAAG,CAAA;AACf,MAAA,OAAO,MAAA;AAAA,IACT;AACA,IAAA,OAAO,KAAA,CAAM,IAAI,GAAG,CAAA;AAAA,EACtB;AAAA,EAEA,OAAO,GAAA,EAAQ;AACb,IAAA,IAAA,CAAK,WAAA,CAAY,OAAO,GAAG,CAAA;AAC3B,IAAA,OAAO,KAAA,CAAM,OAAO,GAAG,CAAA;AAAA,EACzB;AAAA,EAEA,KAAA,GAAQ;AACN,IAAA,IAAA,CAAK,YAAY,KAAA,EAAM;AACvB,IAAA,OAAO,MAAM,KAAA,EAAM;AAAA,EACrB;AACF;;;;"}
@@ -1,12 +1,16 @@
1
1
  import { WebStorage } from '@backstage/core-app-api';
2
2
  import { ResponseError } from '@backstage/errors';
3
3
  import ObservableImpl from 'zen-observable';
4
+ import DataLoader from 'dataloader';
5
+ import { CacheMap } from './CacheMap.esm.js';
4
6
 
5
7
  const JSON_HEADERS = {
6
8
  "Content-Type": "application/json; charset=utf-8",
7
9
  Accept: "application/json"
8
10
  };
9
11
  const buckets = /* @__PURE__ */ new Map();
12
+ const DATALOADER_CACHE_TTL_MS = 2 * 1e3;
13
+ const DATALOADER_WINDOW_MS = 10;
10
14
  class UserSettingsStorage {
11
15
  subscribers = /* @__PURE__ */ new Set();
12
16
  observables = /* @__PURE__ */ new Map();
@@ -17,7 +21,8 @@ class UserSettingsStorage {
17
21
  identityApi;
18
22
  fallback;
19
23
  signalApi;
20
- constructor(namespace, fetchApi, discoveryApi, errorApi, identityApi, fallback, signalApi) {
24
+ userSettingsLoader;
25
+ constructor(namespace, fetchApi, discoveryApi, errorApi, identityApi, fallback, signalApi, userSettingsLoader) {
21
26
  this.namespace = namespace;
22
27
  this.fetchApi = fetchApi;
23
28
  this.discoveryApi = discoveryApi;
@@ -25,6 +30,25 @@ class UserSettingsStorage {
25
30
  this.identityApi = identityApi;
26
31
  this.fallback = fallback;
27
32
  this.signalApi = signalApi;
33
+ this.userSettingsLoader = userSettingsLoader ?? new DataLoader(
34
+ async (bucketAndKeyList) => this.getMulti(bucketAndKeyList),
35
+ {
36
+ name: "UserSettingsStorage.userSettingsLoader",
37
+ cacheMap: new CacheMap(DATALOADER_CACHE_TTL_MS),
38
+ cacheKeyFn: (bucketAndKey) => this.stringifyDataLoaderKey(bucketAndKey),
39
+ maxBatchSize: 100,
40
+ batchScheduleFn: (cb) => setTimeout(cb, DATALOADER_WINDOW_MS)
41
+ }
42
+ );
43
+ }
44
+ stringifyDataLoaderKey({
45
+ bucket,
46
+ key
47
+ }) {
48
+ return `${encodeURIComponent(bucket)}/${encodeURIComponent(key)}`;
49
+ }
50
+ clearCacheKey(key) {
51
+ this.userSettingsLoader.clear({ bucket: this.namespace, key });
28
52
  }
29
53
  static create(options) {
30
54
  return new UserSettingsStorage(
@@ -51,7 +75,9 @@ class UserSettingsStorage {
51
75
  this.discoveryApi,
52
76
  this.errorApi,
53
77
  this.identityApi,
54
- this.fallback
78
+ this.fallback,
79
+ this.signalApi,
80
+ this.userSettingsLoader
55
81
  )
56
82
  );
57
83
  }
@@ -65,14 +91,17 @@ class UserSettingsStorage {
65
91
  if (!response.ok && response.status !== 404) {
66
92
  throw await ResponseError.fromResponse(response);
67
93
  }
94
+ this.clearCacheKey(key);
68
95
  this.notifyChanges({ key, presence: "absent" });
69
96
  }
70
97
  async set(key, data) {
71
98
  if (!await this.isSignedIn()) {
72
99
  await this.fallback.set(key, data);
73
100
  this.notifyChanges({ key, presence: "present", value: data });
101
+ this.clearCacheKey(key);
74
102
  return;
75
103
  }
104
+ this.clearCacheKey(key);
76
105
  const fetchUrl = await this.getFetchUrl(key);
77
106
  const response = await this.fetchApi.fetch(fetchUrl, {
78
107
  method: "PUT",
@@ -83,6 +112,14 @@ class UserSettingsStorage {
83
112
  throw await ResponseError.fromResponse(response);
84
113
  }
85
114
  const { value } = await response.json();
115
+ this.userSettingsLoader.prime(
116
+ { bucket: this.namespace, key },
117
+ {
118
+ key,
119
+ presence: "present",
120
+ value
121
+ }
122
+ );
86
123
  this.notifyChanges({ key, value, presence: "present" });
87
124
  }
88
125
  observe$(key) {
@@ -93,7 +130,9 @@ class UserSettingsStorage {
93
130
  let signalSubscription;
94
131
  this.subscribers.add(subscriber);
95
132
  const updateSnapshot = () => {
96
- Promise.resolve().then(() => this.get(key)).then((snapshot) => subscriber.next(snapshot)).catch((error) => this.errorApi.post(error));
133
+ Promise.resolve().then(
134
+ () => this.userSettingsLoader.load({ bucket: this.namespace, key })
135
+ ).then((snapshot) => subscriber.next(snapshot)).catch((error) => this.errorApi.post(error));
97
136
  };
98
137
  if (this.signalApi) {
99
138
  signalSubscription = this.signalApi.subscribe(
@@ -120,29 +159,53 @@ class UserSettingsStorage {
120
159
  snapshot(key) {
121
160
  return { key, presence: "unknown" };
122
161
  }
123
- async get(key) {
162
+ async getMulti(bucketAndKeyList) {
163
+ if (bucketAndKeyList.length === 0) return [];
124
164
  if (!await this.isSignedIn()) {
125
- return this.fallback.snapshot(key);
126
- }
127
- const fetchUrl = await this.getFetchUrl(key);
128
- const response = await this.fetchApi.fetch(fetchUrl);
129
- if (response.status === 404) {
130
- return { key, presence: "absent" };
131
- }
132
- if (!response.ok) {
133
- throw await ResponseError.fromResponse(response);
165
+ return bucketAndKeyList.map(
166
+ (bucketAndKey) => this.fallback.snapshot(bucketAndKey.key)
167
+ );
134
168
  }
135
169
  try {
136
- const { value: rawValue } = await response.json();
137
- const value = JSON.parse(JSON.stringify(rawValue), (_key, val) => {
138
- if (typeof val === "object" && val !== null) {
139
- Object.freeze(val);
140
- }
141
- return val;
170
+ const baseUrl = await this.discoveryApi.getBaseUrl("user-settings");
171
+ const response = await this.fetchApi.fetch(`${baseUrl}/multiget`, {
172
+ method: "POST",
173
+ headers: JSON_HEADERS,
174
+ body: JSON.stringify({ items: bucketAndKeyList })
142
175
  });
143
- return { key, presence: "present", value };
144
- } catch {
145
- return { key, presence: "absent" };
176
+ if (response.status === 404) {
177
+ return bucketAndKeyList.map((bucketAndKey) => ({
178
+ key: bucketAndKey.key,
179
+ presence: "absent"
180
+ }));
181
+ }
182
+ if (!response.ok) {
183
+ throw await ResponseError.fromResponse(response);
184
+ }
185
+ const { items: values } = await response.json();
186
+ return bucketAndKeyList.map(
187
+ ({ key }, i) => {
188
+ if (!values[i]) {
189
+ return { key, presence: "absent" };
190
+ }
191
+ return {
192
+ key,
193
+ presence: "present",
194
+ value: JSON.parse(JSON.stringify(values[i].value), (_key, val) => {
195
+ if (typeof val === "object" && val !== null) {
196
+ Object.freeze(val);
197
+ }
198
+ return val;
199
+ })
200
+ };
201
+ }
202
+ );
203
+ } catch (e) {
204
+ this.errorApi.post(new Error(`Failed to fetch user settings, ${e}`));
205
+ return bucketAndKeyList.map((bucketAndKey) => ({
206
+ key: bucketAndKey.key,
207
+ presence: "absent"
208
+ }));
146
209
  }
147
210
  }
148
211
  async getFetchUrl(key) {
@@ -1 +1 @@
1
- {"version":3,"file":"UserSettingsStorage.esm.js","sources":["../../../src/apis/StorageApi/UserSettingsStorage.ts"],"sourcesContent":["/*\n * Copyright 2022 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 { WebStorage } from '@backstage/core-app-api';\nimport {\n DiscoveryApi,\n ErrorApi,\n FetchApi,\n IdentityApi,\n StorageApi,\n StorageValueSnapshot,\n} from '@backstage/core-plugin-api';\nimport { ResponseError } from '@backstage/errors';\nimport { JsonValue, Observable } from '@backstage/types';\nimport { SignalApi, SignalSubscriber } from '@backstage/plugin-signals-react';\nimport ObservableImpl from 'zen-observable';\nimport { UserSettingsSignal } from '@backstage/plugin-user-settings-common';\n\nconst JSON_HEADERS = {\n 'Content-Type': 'application/json; charset=utf-8',\n Accept: 'application/json',\n};\n\nconst buckets = new Map<string, UserSettingsStorage>();\n\n/**\n * An implementation of the storage API, that uses the user-settings backend to\n * persist the data in the DB.\n *\n * @public\n */\nexport class UserSettingsStorage implements StorageApi {\n private subscribers = new Set<\n ZenObservable.SubscriptionObserver<StorageValueSnapshot<JsonValue>>\n >();\n\n private readonly observables = new Map<\n string,\n Observable<StorageValueSnapshot<JsonValue>>\n >();\n\n private readonly namespace: string;\n private readonly fetchApi: FetchApi;\n private readonly discoveryApi: DiscoveryApi;\n private readonly errorApi: ErrorApi;\n private readonly identityApi: IdentityApi;\n private readonly fallback: WebStorage;\n private readonly signalApi?: SignalApi;\n\n private constructor(\n namespace: string,\n fetchApi: FetchApi,\n discoveryApi: DiscoveryApi,\n errorApi: ErrorApi,\n identityApi: IdentityApi,\n fallback: WebStorage,\n signalApi?: SignalApi,\n ) {\n this.namespace = namespace;\n this.fetchApi = fetchApi;\n this.discoveryApi = discoveryApi;\n this.errorApi = errorApi;\n this.identityApi = identityApi;\n this.fallback = fallback;\n this.signalApi = signalApi;\n }\n\n static create(options: {\n fetchApi: FetchApi;\n discoveryApi: DiscoveryApi;\n errorApi: ErrorApi;\n identityApi: IdentityApi;\n signalApi?: SignalApi;\n namespace?: string;\n }): UserSettingsStorage {\n return new UserSettingsStorage(\n options.namespace ?? 'default',\n options.fetchApi,\n options.discoveryApi,\n options.errorApi,\n options.identityApi,\n WebStorage.create({\n namespace: options.namespace,\n errorApi: options.errorApi,\n }),\n options.signalApi,\n );\n }\n\n forBucket(name: string): StorageApi {\n // use dot instead of slash separator to have nicer URLs\n const bucketPath = `${this.namespace}.${name}`;\n\n if (!buckets.has(bucketPath)) {\n buckets.set(\n bucketPath,\n new UserSettingsStorage(\n bucketPath,\n this.fetchApi,\n this.discoveryApi,\n this.errorApi,\n this.identityApi,\n this.fallback,\n ),\n );\n }\n\n return buckets.get(bucketPath)!;\n }\n\n async remove(key: string): Promise<void> {\n const fetchUrl = await this.getFetchUrl(key);\n\n const response = await this.fetchApi.fetch(fetchUrl, {\n method: 'DELETE',\n });\n\n if (!response.ok && response.status !== 404) {\n throw await ResponseError.fromResponse(response);\n }\n\n this.notifyChanges({ key, presence: 'absent' });\n }\n\n async set<T extends JsonValue>(key: string, data: T): Promise<void> {\n if (!(await this.isSignedIn())) {\n await this.fallback.set(key, data);\n this.notifyChanges({ key, presence: 'present', value: data });\n return;\n }\n\n const fetchUrl = await this.getFetchUrl(key);\n\n const response = await this.fetchApi.fetch(fetchUrl, {\n method: 'PUT',\n headers: JSON_HEADERS,\n body: JSON.stringify({ value: data }),\n });\n\n if (!response.ok) {\n throw await ResponseError.fromResponse(response);\n }\n\n const { value } = await response.json();\n\n this.notifyChanges({ key, value, presence: 'present' });\n }\n\n observe$<T extends JsonValue>(\n key: string,\n ): Observable<StorageValueSnapshot<T>> {\n if (!this.observables.has(key)) {\n this.observables.set(\n key,\n new ObservableImpl<StorageValueSnapshot<JsonValue>>(subscriber => {\n let signalSubscription: SignalSubscriber | undefined;\n this.subscribers.add(subscriber);\n\n const updateSnapshot = () => {\n Promise.resolve()\n .then(() => this.get(key))\n .then(snapshot => subscriber.next(snapshot))\n .catch(error => this.errorApi.post(error));\n };\n\n if (this.signalApi) {\n signalSubscription = this.signalApi.subscribe(\n `user-settings`,\n (msg: UserSettingsSignal) => {\n if (msg.key === key) {\n updateSnapshot();\n }\n },\n );\n }\n\n updateSnapshot();\n\n return () => {\n if (signalSubscription) {\n signalSubscription.unsubscribe();\n }\n this.subscribers.delete(subscriber);\n };\n }).filter(({ key: messageKey }) => messageKey === key),\n );\n }\n\n return this.observables.get(key) as Observable<StorageValueSnapshot<T>>;\n }\n\n snapshot<T extends JsonValue>(key: string): StorageValueSnapshot<T> {\n return { key, presence: 'unknown' };\n }\n\n private async get<T extends JsonValue>(\n key: string,\n ): Promise<StorageValueSnapshot<T>> {\n if (!(await this.isSignedIn())) {\n // This explicitly uses WebStorage, which we know is synchronous and doesn't return presence: unknown\n return this.fallback.snapshot(key);\n }\n\n const fetchUrl = await this.getFetchUrl(key);\n const response = await this.fetchApi.fetch(fetchUrl);\n\n if (response.status === 404) {\n return { key, presence: 'absent' };\n }\n\n if (!response.ok) {\n throw await ResponseError.fromResponse(response);\n }\n\n try {\n const { value: rawValue } = await response.json();\n const value = JSON.parse(JSON.stringify(rawValue), (_key, val) => {\n if (typeof val === 'object' && val !== null) {\n Object.freeze(val);\n }\n return val;\n });\n\n return { key, presence: 'present', value };\n } catch {\n // If the value is not valid JSON, we return an unknown presence. This should never happen\n return { key, presence: 'absent' };\n }\n }\n\n private async getFetchUrl(key: string) {\n const baseUrl = await this.discoveryApi.getBaseUrl('user-settings');\n const encodedNamespace = encodeURIComponent(this.namespace);\n const encodedKey = encodeURIComponent(key);\n return `${baseUrl}/buckets/${encodedNamespace}/keys/${encodedKey}`;\n }\n\n private async notifyChanges<T extends JsonValue>(\n snapshot: StorageValueSnapshot<T>,\n ) {\n for (const subscription of this.subscribers) {\n try {\n subscription.next(snapshot);\n } catch {\n // ignore\n }\n }\n }\n\n private async isSignedIn(): Promise<boolean> {\n try {\n const credentials = await this.identityApi.getCredentials();\n return credentials?.token ? true : false;\n } catch {\n return false;\n }\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"UserSettingsStorage.esm.js","sources":["../../../src/apis/StorageApi/UserSettingsStorage.ts"],"sourcesContent":["/*\n * Copyright 2022 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 { WebStorage } from '@backstage/core-app-api';\nimport {\n DiscoveryApi,\n ErrorApi,\n FetchApi,\n IdentityApi,\n StorageApi,\n StorageValueSnapshot,\n} from '@backstage/core-plugin-api';\nimport { ResponseError } from '@backstage/errors';\nimport { JsonValue, Observable } from '@backstage/types';\nimport { SignalApi, SignalSubscriber } from '@backstage/plugin-signals-react';\nimport ObservableImpl from 'zen-observable';\nimport {\n MultiGetResponse,\n UserSettingsSignal,\n} from '@backstage/plugin-user-settings-common';\nimport DataLoader from 'dataloader';\nimport { CacheMap } from './CacheMap';\n\nconst JSON_HEADERS = {\n 'Content-Type': 'application/json; charset=utf-8',\n Accept: 'application/json',\n};\n\nconst buckets = new Map<string, UserSettingsStorage>();\n\nconst DATALOADER_CACHE_TTL_MS = 2 * 1000; // 2 seconds cache\nconst DATALOADER_WINDOW_MS = 10; // 10 ms\n\ntype DataLoaderType = DataLoader<\n { bucket: string; key: string },\n StorageValueSnapshot<JsonValue>,\n string\n>;\n\n/**\n * An implementation of the storage API, that uses the user-settings backend to\n * persist the data in the DB.\n *\n * @public\n */\nexport class UserSettingsStorage implements StorageApi {\n private subscribers = new Set<\n ZenObservable.SubscriptionObserver<StorageValueSnapshot<JsonValue>>\n >();\n\n private readonly observables = new Map<\n string,\n Observable<StorageValueSnapshot<JsonValue>>\n >();\n\n private readonly namespace: string;\n private readonly fetchApi: FetchApi;\n private readonly discoveryApi: DiscoveryApi;\n private readonly errorApi: ErrorApi;\n private readonly identityApi: IdentityApi;\n private readonly fallback: WebStorage;\n private readonly signalApi?: SignalApi;\n private readonly userSettingsLoader: DataLoaderType;\n\n private constructor(\n namespace: string,\n fetchApi: FetchApi,\n discoveryApi: DiscoveryApi,\n errorApi: ErrorApi,\n identityApi: IdentityApi,\n fallback: WebStorage,\n signalApi?: SignalApi,\n userSettingsLoader?: DataLoaderType,\n ) {\n this.namespace = namespace;\n this.fetchApi = fetchApi;\n this.discoveryApi = discoveryApi;\n this.errorApi = errorApi;\n this.identityApi = identityApi;\n this.fallback = fallback;\n this.signalApi = signalApi;\n\n this.userSettingsLoader =\n userSettingsLoader ??\n new DataLoader(\n async bucketAndKeyList => this.getMulti(bucketAndKeyList),\n {\n name: 'UserSettingsStorage.userSettingsLoader',\n cacheMap: new CacheMap<\n string,\n Promise<StorageValueSnapshot<JsonValue>>\n >(DATALOADER_CACHE_TTL_MS),\n cacheKeyFn: bucketAndKey => this.stringifyDataLoaderKey(bucketAndKey),\n maxBatchSize: 100,\n batchScheduleFn: cb => setTimeout(cb, DATALOADER_WINDOW_MS),\n },\n );\n }\n\n private stringifyDataLoaderKey({\n bucket,\n key,\n }: {\n bucket: string;\n key: string;\n }) {\n return `${encodeURIComponent(bucket)}/${encodeURIComponent(key)}`;\n }\n private clearCacheKey(key: string) {\n this.userSettingsLoader.clear({ bucket: this.namespace, key });\n }\n\n static create(options: {\n fetchApi: FetchApi;\n discoveryApi: DiscoveryApi;\n errorApi: ErrorApi;\n identityApi: IdentityApi;\n signalApi?: SignalApi;\n namespace?: string;\n }): UserSettingsStorage {\n return new UserSettingsStorage(\n options.namespace ?? 'default',\n options.fetchApi,\n options.discoveryApi,\n options.errorApi,\n options.identityApi,\n WebStorage.create({\n namespace: options.namespace,\n errorApi: options.errorApi,\n }),\n options.signalApi,\n );\n }\n\n forBucket(name: string): StorageApi {\n // use dot instead of slash separator to have nicer URLs\n const bucketPath = `${this.namespace}.${name}`;\n\n if (!buckets.has(bucketPath)) {\n buckets.set(\n bucketPath,\n new UserSettingsStorage(\n bucketPath,\n this.fetchApi,\n this.discoveryApi,\n this.errorApi,\n this.identityApi,\n this.fallback,\n this.signalApi,\n this.userSettingsLoader,\n ),\n );\n }\n\n return buckets.get(bucketPath)!;\n }\n\n async remove(key: string): Promise<void> {\n const fetchUrl = await this.getFetchUrl(key);\n\n const response = await this.fetchApi.fetch(fetchUrl, {\n method: 'DELETE',\n });\n\n if (!response.ok && response.status !== 404) {\n throw await ResponseError.fromResponse(response);\n }\n\n this.clearCacheKey(key);\n\n this.notifyChanges({ key, presence: 'absent' });\n }\n\n async set<T extends JsonValue>(key: string, data: T): Promise<void> {\n if (!(await this.isSignedIn())) {\n await this.fallback.set(key, data);\n this.notifyChanges({ key, presence: 'present', value: data });\n this.clearCacheKey(key);\n return;\n }\n\n this.clearCacheKey(key);\n\n const fetchUrl = await this.getFetchUrl(key);\n\n const response = await this.fetchApi.fetch(fetchUrl, {\n method: 'PUT',\n headers: JSON_HEADERS,\n body: JSON.stringify({ value: data }),\n });\n\n if (!response.ok) {\n throw await ResponseError.fromResponse(response);\n }\n\n const { value } = await response.json();\n\n this.userSettingsLoader.prime(\n { bucket: this.namespace, key },\n {\n key,\n presence: 'present',\n value,\n },\n );\n\n this.notifyChanges({ key, value, presence: 'present' });\n }\n\n observe$<T extends JsonValue>(\n key: string,\n ): Observable<StorageValueSnapshot<T>> {\n if (!this.observables.has(key)) {\n this.observables.set(\n key,\n new ObservableImpl<StorageValueSnapshot<JsonValue>>(subscriber => {\n let signalSubscription: SignalSubscriber | undefined;\n this.subscribers.add(subscriber);\n\n const updateSnapshot = () => {\n Promise.resolve()\n .then(() =>\n this.userSettingsLoader.load({ bucket: this.namespace, key }),\n )\n .then(snapshot => subscriber.next(snapshot))\n .catch(error => this.errorApi.post(error));\n };\n\n if (this.signalApi) {\n signalSubscription = this.signalApi.subscribe(\n `user-settings`,\n (msg: UserSettingsSignal) => {\n if (msg.key === key) {\n updateSnapshot();\n }\n },\n );\n }\n\n updateSnapshot();\n\n return () => {\n if (signalSubscription) {\n signalSubscription.unsubscribe();\n }\n this.subscribers.delete(subscriber);\n };\n }).filter(({ key: messageKey }) => messageKey === key),\n );\n }\n\n return this.observables.get(key) as Observable<StorageValueSnapshot<T>>;\n }\n\n snapshot<T extends JsonValue>(key: string): StorageValueSnapshot<T> {\n return { key, presence: 'unknown' };\n }\n\n private async getMulti(\n bucketAndKeyList: readonly { bucket: string; key: string }[],\n ): Promise<StorageValueSnapshot<JsonValue>[]> {\n if (bucketAndKeyList.length === 0) return [];\n\n if (!(await this.isSignedIn())) {\n // This explicitly uses WebStorage, which we know is synchronous and doesn't return presence: unknown\n return bucketAndKeyList.map(bucketAndKey =>\n this.fallback.snapshot(bucketAndKey.key),\n );\n }\n\n try {\n const baseUrl = await this.discoveryApi.getBaseUrl('user-settings');\n const response = await this.fetchApi.fetch(`${baseUrl}/multiget`, {\n method: 'POST',\n headers: JSON_HEADERS,\n body: JSON.stringify({ items: bucketAndKeyList }),\n });\n\n if (response.status === 404) {\n return bucketAndKeyList.map(bucketAndKey => ({\n key: bucketAndKey.key,\n presence: 'absent',\n }));\n }\n\n if (!response.ok) {\n throw await ResponseError.fromResponse(response);\n }\n\n const { items: values } = (await response.json()) as MultiGetResponse;\n\n return bucketAndKeyList.map(\n ({ key }, i): StorageValueSnapshot<JsonValue> => {\n if (!values[i]) {\n return { key, presence: 'absent' };\n }\n return {\n key,\n presence: 'present',\n value: JSON.parse(JSON.stringify(values[i].value), (_key, val) => {\n if (typeof val === 'object' && val !== null) {\n Object.freeze(val);\n }\n return val;\n }),\n };\n },\n );\n } catch (e) {\n this.errorApi.post(new Error(`Failed to fetch user settings, ${e}`));\n return bucketAndKeyList.map(bucketAndKey => ({\n key: bucketAndKey.key,\n presence: 'absent',\n }));\n }\n }\n\n private async getFetchUrl(key: string) {\n const baseUrl = await this.discoveryApi.getBaseUrl('user-settings');\n const encodedNamespace = encodeURIComponent(this.namespace);\n const encodedKey = encodeURIComponent(key);\n return `${baseUrl}/buckets/${encodedNamespace}/keys/${encodedKey}`;\n }\n\n private async notifyChanges<T extends JsonValue>(\n snapshot: StorageValueSnapshot<T>,\n ) {\n for (const subscription of this.subscribers) {\n try {\n subscription.next(snapshot);\n } catch {\n // ignore\n }\n }\n }\n\n private async isSignedIn(): Promise<boolean> {\n try {\n const credentials = await this.identityApi.getCredentials();\n return credentials?.token ? true : false;\n } catch {\n return false;\n }\n }\n}\n"],"names":[],"mappings":";;;;;;AAoCA,MAAM,YAAA,GAAe;AAAA,EACnB,cAAA,EAAgB,iCAAA;AAAA,EAChB,MAAA,EAAQ;AACV,CAAA;AAEA,MAAM,OAAA,uBAAc,GAAA,EAAiC;AAErD,MAAM,0BAA0B,CAAA,GAAI,GAAA;AACpC,MAAM,oBAAA,GAAuB,EAAA;AActB,MAAM,mBAAA,CAA0C;AAAA,EAC7C,WAAA,uBAAkB,GAAA,EAExB;AAAA,EAEe,WAAA,uBAAkB,GAAA,EAGjC;AAAA,EAEe,SAAA;AAAA,EACA,QAAA;AAAA,EACA,YAAA;AAAA,EACA,QAAA;AAAA,EACA,WAAA;AAAA,EACA,QAAA;AAAA,EACA,SAAA;AAAA,EACA,kBAAA;AAAA,EAET,WAAA,CACN,WACA,QAAA,EACA,YAAA,EACA,UACA,WAAA,EACA,QAAA,EACA,WACA,kBAAA,EACA;AACA,IAAA,IAAA,CAAK,SAAA,GAAY,SAAA;AACjB,IAAA,IAAA,CAAK,QAAA,GAAW,QAAA;AAChB,IAAA,IAAA,CAAK,YAAA,GAAe,YAAA;AACpB,IAAA,IAAA,CAAK,QAAA,GAAW,QAAA;AAChB,IAAA,IAAA,CAAK,WAAA,GAAc,WAAA;AACnB,IAAA,IAAA,CAAK,QAAA,GAAW,QAAA;AAChB,IAAA,IAAA,CAAK,SAAA,GAAY,SAAA;AAEjB,IAAA,IAAA,CAAK,kBAAA,GACH,sBACA,IAAI,UAAA;AAAA,MACF,OAAM,gBAAA,KAAoB,IAAA,CAAK,QAAA,CAAS,gBAAgB,CAAA;AAAA,MACxD;AAAA,QACE,IAAA,EAAM,wCAAA;AAAA,QACN,QAAA,EAAU,IAAI,QAAA,CAGZ,uBAAuB,CAAA;AAAA,QACzB,UAAA,EAAY,CAAA,YAAA,KAAgB,IAAA,CAAK,sBAAA,CAAuB,YAAY,CAAA;AAAA,QACpE,YAAA,EAAc,GAAA;AAAA,QACd,eAAA,EAAiB,CAAA,EAAA,KAAM,UAAA,CAAW,EAAA,EAAI,oBAAoB;AAAA;AAC5D,KACF;AAAA,EACJ;AAAA,EAEQ,sBAAA,CAAuB;AAAA,IAC7B,MAAA;AAAA,IACA;AAAA,GACF,EAGG;AACD,IAAA,OAAO,GAAG,kBAAA,CAAmB,MAAM,CAAC,CAAA,CAAA,EAAI,kBAAA,CAAmB,GAAG,CAAC,CAAA,CAAA;AAAA,EACjE;AAAA,EACQ,cAAc,GAAA,EAAa;AACjC,IAAA,IAAA,CAAK,mBAAmB,KAAA,CAAM,EAAE,QAAQ,IAAA,CAAK,SAAA,EAAW,KAAK,CAAA;AAAA,EAC/D;AAAA,EAEA,OAAO,OAAO,OAAA,EAOU;AACtB,IAAA,OAAO,IAAI,mBAAA;AAAA,MACT,QAAQ,SAAA,IAAa,SAAA;AAAA,MACrB,OAAA,CAAQ,QAAA;AAAA,MACR,OAAA,CAAQ,YAAA;AAAA,MACR,OAAA,CAAQ,QAAA;AAAA,MACR,OAAA,CAAQ,WAAA;AAAA,MACR,WAAW,MAAA,CAAO;AAAA,QAChB,WAAW,OAAA,CAAQ,SAAA;AAAA,QACnB,UAAU,OAAA,CAAQ;AAAA,OACnB,CAAA;AAAA,MACD,OAAA,CAAQ;AAAA,KACV;AAAA,EACF;AAAA,EAEA,UAAU,IAAA,EAA0B;AAElC,IAAA,MAAM,UAAA,GAAa,CAAA,EAAG,IAAA,CAAK,SAAS,IAAI,IAAI,CAAA,CAAA;AAE5C,IAAA,IAAI,CAAC,OAAA,CAAQ,GAAA,CAAI,UAAU,CAAA,EAAG;AAC5B,MAAA,OAAA,CAAQ,GAAA;AAAA,QACN,UAAA;AAAA,QACA,IAAI,mBAAA;AAAA,UACF,UAAA;AAAA,UACA,IAAA,CAAK,QAAA;AAAA,UACL,IAAA,CAAK,YAAA;AAAA,UACL,IAAA,CAAK,QAAA;AAAA,UACL,IAAA,CAAK,WAAA;AAAA,UACL,IAAA,CAAK,QAAA;AAAA,UACL,IAAA,CAAK,SAAA;AAAA,UACL,IAAA,CAAK;AAAA;AACP,OACF;AAAA,IACF;AAEA,IAAA,OAAO,OAAA,CAAQ,IAAI,UAAU,CAAA;AAAA,EAC/B;AAAA,EAEA,MAAM,OAAO,GAAA,EAA4B;AACvC,IAAA,MAAM,QAAA,GAAW,MAAM,IAAA,CAAK,WAAA,CAAY,GAAG,CAAA;AAE3C,IAAA,MAAM,QAAA,GAAW,MAAM,IAAA,CAAK,QAAA,CAAS,MAAM,QAAA,EAAU;AAAA,MACnD,MAAA,EAAQ;AAAA,KACT,CAAA;AAED,IAAA,IAAI,CAAC,QAAA,CAAS,EAAA,IAAM,QAAA,CAAS,WAAW,GAAA,EAAK;AAC3C,MAAA,MAAM,MAAM,aAAA,CAAc,YAAA,CAAa,QAAQ,CAAA;AAAA,IACjD;AAEA,IAAA,IAAA,CAAK,cAAc,GAAG,CAAA;AAEtB,IAAA,IAAA,CAAK,aAAA,CAAc,EAAE,GAAA,EAAK,QAAA,EAAU,UAAU,CAAA;AAAA,EAChD;AAAA,EAEA,MAAM,GAAA,CAAyB,GAAA,EAAa,IAAA,EAAwB;AAClE,IAAA,IAAI,CAAE,MAAM,IAAA,CAAK,UAAA,EAAW,EAAI;AAC9B,MAAA,MAAM,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,GAAA,EAAK,IAAI,CAAA;AACjC,MAAA,IAAA,CAAK,cAAc,EAAE,GAAA,EAAK,UAAU,SAAA,EAAW,KAAA,EAAO,MAAM,CAAA;AAC5D,MAAA,IAAA,CAAK,cAAc,GAAG,CAAA;AACtB,MAAA;AAAA,IACF;AAEA,IAAA,IAAA,CAAK,cAAc,GAAG,CAAA;AAEtB,IAAA,MAAM,QAAA,GAAW,MAAM,IAAA,CAAK,WAAA,CAAY,GAAG,CAAA;AAE3C,IAAA,MAAM,QAAA,GAAW,MAAM,IAAA,CAAK,QAAA,CAAS,MAAM,QAAA,EAAU;AAAA,MACnD,MAAA,EAAQ,KAAA;AAAA,MACR,OAAA,EAAS,YAAA;AAAA,MACT,MAAM,IAAA,CAAK,SAAA,CAAU,EAAE,KAAA,EAAO,MAAM;AAAA,KACrC,CAAA;AAED,IAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,MAAA,MAAM,MAAM,aAAA,CAAc,YAAA,CAAa,QAAQ,CAAA;AAAA,IACjD;AAEA,IAAA,MAAM,EAAE,KAAA,EAAM,GAAI,MAAM,SAAS,IAAA,EAAK;AAEtC,IAAA,IAAA,CAAK,kBAAA,CAAmB,KAAA;AAAA,MACtB,EAAE,MAAA,EAAQ,IAAA,CAAK,SAAA,EAAW,GAAA,EAAI;AAAA,MAC9B;AAAA,QACE,GAAA;AAAA,QACA,QAAA,EAAU,SAAA;AAAA,QACV;AAAA;AACF,KACF;AAEA,IAAA,IAAA,CAAK,cAAc,EAAE,GAAA,EAAK,KAAA,EAAO,QAAA,EAAU,WAAW,CAAA;AAAA,EACxD;AAAA,EAEA,SACE,GAAA,EACqC;AACrC,IAAA,IAAI,CAAC,IAAA,CAAK,WAAA,CAAY,GAAA,CAAI,GAAG,CAAA,EAAG;AAC9B,MAAA,IAAA,CAAK,WAAA,CAAY,GAAA;AAAA,QACf,GAAA;AAAA,QACA,IAAI,eAAgD,CAAA,UAAA,KAAc;AAChE,UAAA,IAAI,kBAAA;AACJ,UAAA,IAAA,CAAK,WAAA,CAAY,IAAI,UAAU,CAAA;AAE/B,UAAA,MAAM,iBAAiB,MAAM;AAC3B,YAAA,OAAA,CAAQ,SAAQ,CACb,IAAA;AAAA,cAAK,MACJ,KAAK,kBAAA,CAAmB,IAAA,CAAK,EAAE,MAAA,EAAQ,IAAA,CAAK,SAAA,EAAW,GAAA,EAAK;AAAA,aAC9D,CACC,IAAA,CAAK,CAAA,QAAA,KAAY,UAAA,CAAW,KAAK,QAAQ,CAAC,CAAA,CAC1C,KAAA,CAAM,CAAA,KAAA,KAAS,IAAA,CAAK,QAAA,CAAS,IAAA,CAAK,KAAK,CAAC,CAAA;AAAA,UAC7C,CAAA;AAEA,UAAA,IAAI,KAAK,SAAA,EAAW;AAClB,YAAA,kBAAA,GAAqB,KAAK,SAAA,CAAU,SAAA;AAAA,cAClC,CAAA,aAAA,CAAA;AAAA,cACA,CAAC,GAAA,KAA4B;AAC3B,gBAAA,IAAI,GAAA,CAAI,QAAQ,GAAA,EAAK;AACnB,kBAAA,cAAA,EAAe;AAAA,gBACjB;AAAA,cACF;AAAA,aACF;AAAA,UACF;AAEA,UAAA,cAAA,EAAe;AAEf,UAAA,OAAO,MAAM;AACX,YAAA,IAAI,kBAAA,EAAoB;AACtB,cAAA,kBAAA,CAAmB,WAAA,EAAY;AAAA,YACjC;AACA,YAAA,IAAA,CAAK,WAAA,CAAY,OAAO,UAAU,CAAA;AAAA,UACpC,CAAA;AAAA,QACF,CAAC,EAAE,MAAA,CAAO,CAAC,EAAE,GAAA,EAAK,UAAA,EAAW,KAAM,UAAA,KAAe,GAAG;AAAA,OACvD;AAAA,IACF;AAEA,IAAA,OAAO,IAAA,CAAK,WAAA,CAAY,GAAA,CAAI,GAAG,CAAA;AAAA,EACjC;AAAA,EAEA,SAA8B,GAAA,EAAsC;AAClE,IAAA,OAAO,EAAE,GAAA,EAAK,QAAA,EAAU,SAAA,EAAU;AAAA,EACpC;AAAA,EAEA,MAAc,SACZ,gBAAA,EAC4C;AAC5C,IAAA,IAAI,gBAAA,CAAiB,MAAA,KAAW,CAAA,EAAG,OAAO,EAAC;AAE3C,IAAA,IAAI,CAAE,MAAM,IAAA,CAAK,UAAA,EAAW,EAAI;AAE9B,MAAA,OAAO,gBAAA,CAAiB,GAAA;AAAA,QAAI,CAAA,YAAA,KAC1B,IAAA,CAAK,QAAA,CAAS,QAAA,CAAS,aAAa,GAAG;AAAA,OACzC;AAAA,IACF;AAEA,IAAA,IAAI;AACF,MAAA,MAAM,OAAA,GAAU,MAAM,IAAA,CAAK,YAAA,CAAa,WAAW,eAAe,CAAA;AAClE,MAAA,MAAM,WAAW,MAAM,IAAA,CAAK,SAAS,KAAA,CAAM,CAAA,EAAG,OAAO,CAAA,SAAA,CAAA,EAAa;AAAA,QAChE,MAAA,EAAQ,MAAA;AAAA,QACR,OAAA,EAAS,YAAA;AAAA,QACT,MAAM,IAAA,CAAK,SAAA,CAAU,EAAE,KAAA,EAAO,kBAAkB;AAAA,OACjD,CAAA;AAED,MAAA,IAAI,QAAA,CAAS,WAAW,GAAA,EAAK;AAC3B,QAAA,OAAO,gBAAA,CAAiB,IAAI,CAAA,YAAA,MAAiB;AAAA,UAC3C,KAAK,YAAA,CAAa,GAAA;AAAA,UAClB,QAAA,EAAU;AAAA,SACZ,CAAE,CAAA;AAAA,MACJ;AAEA,MAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,QAAA,MAAM,MAAM,aAAA,CAAc,YAAA,CAAa,QAAQ,CAAA;AAAA,MACjD;AAEA,MAAA,MAAM,EAAE,KAAA,EAAO,MAAA,EAAO,GAAK,MAAM,SAAS,IAAA,EAAK;AAE/C,MAAA,OAAO,gBAAA,CAAiB,GAAA;AAAA,QACtB,CAAC,EAAE,GAAA,EAAI,EAAG,CAAA,KAAuC;AAC/C,UAAA,IAAI,CAAC,MAAA,CAAO,CAAC,CAAA,EAAG;AACd,YAAA,OAAO,EAAE,GAAA,EAAK,QAAA,EAAU,QAAA,EAAS;AAAA,UACnC;AACA,UAAA,OAAO;AAAA,YACL,GAAA;AAAA,YACA,QAAA,EAAU,SAAA;AAAA,YACV,KAAA,EAAO,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,SAAA,CAAU,MAAA,CAAO,CAAC,CAAA,CAAE,KAAK,CAAA,EAAG,CAAC,IAAA,EAAM,GAAA,KAAQ;AAChE,cAAA,IAAI,OAAO,GAAA,KAAQ,QAAA,IAAY,GAAA,KAAQ,IAAA,EAAM;AAC3C,gBAAA,MAAA,CAAO,OAAO,GAAG,CAAA;AAAA,cACnB;AACA,cAAA,OAAO,GAAA;AAAA,YACT,CAAC;AAAA,WACH;AAAA,QACF;AAAA,OACF;AAAA,IACF,SAAS,CAAA,EAAG;AACV,MAAA,IAAA,CAAK,SAAS,IAAA,CAAK,IAAI,MAAM,CAAA,+BAAA,EAAkC,CAAC,EAAE,CAAC,CAAA;AACnE,MAAA,OAAO,gBAAA,CAAiB,IAAI,CAAA,YAAA,MAAiB;AAAA,QAC3C,KAAK,YAAA,CAAa,GAAA;AAAA,QAClB,QAAA,EAAU;AAAA,OACZ,CAAE,CAAA;AAAA,IACJ;AAAA,EACF;AAAA,EAEA,MAAc,YAAY,GAAA,EAAa;AACrC,IAAA,MAAM,OAAA,GAAU,MAAM,IAAA,CAAK,YAAA,CAAa,WAAW,eAAe,CAAA;AAClE,IAAA,MAAM,gBAAA,GAAmB,kBAAA,CAAmB,IAAA,CAAK,SAAS,CAAA;AAC1D,IAAA,MAAM,UAAA,GAAa,mBAAmB,GAAG,CAAA;AACzC,IAAA,OAAO,CAAA,EAAG,OAAO,CAAA,SAAA,EAAY,gBAAgB,SAAS,UAAU,CAAA,CAAA;AAAA,EAClE;AAAA,EAEA,MAAc,cACZ,QAAA,EACA;AACA,IAAA,KAAA,MAAW,YAAA,IAAgB,KAAK,WAAA,EAAa;AAC3C,MAAA,IAAI;AACF,QAAA,YAAA,CAAa,KAAK,QAAQ,CAAA;AAAA,MAC5B,CAAA,CAAA,MAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAc,UAAA,GAA+B;AAC3C,IAAA,IAAI;AACF,MAAA,MAAM,WAAA,GAAc,MAAM,IAAA,CAAK,WAAA,CAAY,cAAA,EAAe;AAC1D,MAAA,OAAO,WAAA,EAAa,QAAQ,IAAA,GAAO,KAAA;AAAA,IACrC,CAAA,CAAA,MAAQ;AACN,MAAA,OAAO,KAAA;AAAA,IACT;AAAA,EACF;AACF;;;;"}
package/dist/index.d.ts CHANGED
@@ -23,7 +23,10 @@ declare class UserSettingsStorage implements StorageApi {
23
23
  private readonly identityApi;
24
24
  private readonly fallback;
25
25
  private readonly signalApi?;
26
+ private readonly userSettingsLoader;
26
27
  private constructor();
28
+ private stringifyDataLoaderKey;
29
+ private clearCacheKey;
27
30
  static create(options: {
28
31
  fetchApi: FetchApi;
29
32
  discoveryApi: DiscoveryApi;
@@ -37,7 +40,7 @@ declare class UserSettingsStorage implements StorageApi {
37
40
  set<T extends JsonValue>(key: string, data: T): Promise<void>;
38
41
  observe$<T extends JsonValue>(key: string): Observable<StorageValueSnapshot<T>>;
39
42
  snapshot<T extends JsonValue>(key: string): StorageValueSnapshot<T>;
40
- private get;
43
+ private getMulti;
41
44
  private getFetchUrl;
42
45
  private notifyChanges;
43
46
  private isSignedIn;
@@ -1,5 +1,5 @@
1
1
  var name = "@backstage/plugin-user-settings";
2
- var version = "0.8.32-next.1";
2
+ var version = "0.9.0";
3
3
  var description = "A Backstage plugin that provides a settings page";
4
4
  var backstage = {
5
5
  role: "frontend-plugin",
@@ -68,6 +68,7 @@ var dependencies = {
68
68
  "@material-ui/core": "^4.12.2",
69
69
  "@material-ui/icons": "^4.9.1",
70
70
  "@material-ui/lab": "4.0.0-alpha.61",
71
+ dataloader: "^2.0.0",
71
72
  "react-use": "^17.2.4",
72
73
  "zen-observable": "^0.10.0"
73
74
  };
@@ -84,13 +85,13 @@ var devDependencies = {
84
85
  msw: "^1.0.0",
85
86
  react: "^18.0.2",
86
87
  "react-dom": "^18.0.2",
87
- "react-router-dom": "^6.3.0"
88
+ "react-router-dom": "^6.30.2"
88
89
  };
89
90
  var peerDependencies = {
90
91
  "@types/react": "^17.0.0 || ^18.0.0",
91
92
  react: "^17.0.0 || ^18.0.0",
92
93
  "react-dom": "^17.0.0 || ^18.0.0",
93
- "react-router-dom": "^6.3.0"
94
+ "react-router-dom": "^6.30.2"
94
95
  };
95
96
  var peerDependenciesMeta = {
96
97
  "@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-user-settings",
3
- "version": "0.8.32-next.1",
3
+ "version": "0.9.0",
4
4
  "description": "A Backstage plugin that provides a settings page",
5
5
  "backstage": {
6
6
  "role": "frontend-plugin",
@@ -67,28 +67,29 @@
67
67
  "test": "backstage-cli package test"
68
68
  },
69
69
  "dependencies": {
70
- "@backstage/catalog-model": "1.7.6",
71
- "@backstage/core-app-api": "1.19.5-next.0",
72
- "@backstage/core-components": "0.18.7-next.1",
73
- "@backstage/core-plugin-api": "1.12.3-next.0",
74
- "@backstage/errors": "1.2.7",
75
- "@backstage/frontend-plugin-api": "0.14.0-next.1",
76
- "@backstage/plugin-catalog-react": "1.22.0-next.1",
77
- "@backstage/plugin-signals-react": "0.0.19-next.0",
78
- "@backstage/plugin-user-settings-common": "0.0.1",
79
- "@backstage/theme": "0.7.2-next.0",
80
- "@backstage/types": "1.2.2",
70
+ "@backstage/catalog-model": "^1.7.6",
71
+ "@backstage/core-app-api": "^1.19.5",
72
+ "@backstage/core-components": "^0.18.7",
73
+ "@backstage/core-plugin-api": "^1.12.3",
74
+ "@backstage/errors": "^1.2.7",
75
+ "@backstage/frontend-plugin-api": "^0.14.0",
76
+ "@backstage/plugin-catalog-react": "^2.0.0",
77
+ "@backstage/plugin-signals-react": "^0.0.19",
78
+ "@backstage/plugin-user-settings-common": "^0.1.0",
79
+ "@backstage/theme": "^0.7.2",
80
+ "@backstage/types": "^1.2.2",
81
81
  "@material-ui/core": "^4.12.2",
82
82
  "@material-ui/icons": "^4.9.1",
83
83
  "@material-ui/lab": "4.0.0-alpha.61",
84
+ "dataloader": "^2.0.0",
84
85
  "react-use": "^17.2.4",
85
86
  "zen-observable": "^0.10.0"
86
87
  },
87
88
  "devDependencies": {
88
- "@backstage/cli": "0.35.4-next.1",
89
- "@backstage/dev-utils": "1.1.20-next.1",
90
- "@backstage/plugin-catalog": "1.33.0-next.1",
91
- "@backstage/test-utils": "1.7.15-next.1",
89
+ "@backstage/cli": "^0.35.4",
90
+ "@backstage/dev-utils": "^1.1.20",
91
+ "@backstage/plugin-catalog": "^1.33.0",
92
+ "@backstage/test-utils": "^1.7.15",
92
93
  "@testing-library/dom": "^10.0.0",
93
94
  "@testing-library/jest-dom": "^6.0.0",
94
95
  "@testing-library/react": "^16.0.0",
@@ -97,13 +98,13 @@
97
98
  "msw": "^1.0.0",
98
99
  "react": "^18.0.2",
99
100
  "react-dom": "^18.0.2",
100
- "react-router-dom": "^6.3.0"
101
+ "react-router-dom": "^6.30.2"
101
102
  },
102
103
  "peerDependencies": {
103
104
  "@types/react": "^17.0.0 || ^18.0.0",
104
105
  "react": "^17.0.0 || ^18.0.0",
105
106
  "react-dom": "^17.0.0 || ^18.0.0",
106
- "react-router-dom": "^6.3.0"
107
+ "react-router-dom": "^6.30.2"
107
108
  },
108
109
  "peerDependenciesMeta": {
109
110
  "@types/react": {