Merge branch 'upstream-master' into bug/25090-select-component

Signed-off-by: Emile Fokkema <emilefokkema@gmail.com>
This commit is contained in:
Emile Fokkema
2024-06-09 12:32:03 +00:00
20 changed files with 293 additions and 20 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-user-settings-backend': patch
'@backstage/plugin-user-settings-common': patch
'@backstage/plugin-user-settings': patch
---
Use signals to update user settings across sessions
@@ -4,34 +4,68 @@ title: Linking in Local Packages
description: How to link in other local packages into your Backstage monorepo
---
It can often be useful to try out changes to the packages in the main Backstage
repo within your own app. For example if you want to make modifications to
`@backstage/core-plugin-api` and try them out in your app.
## Why?
To link in external packages, add them to your `package.json` and `lerna.json`
workspace paths. These can be either relative or absolute paths with or without
globs. For example:
If you are looking to make changes within the core Backstage repository and test
those changes within your Backstage application, you will need to link the two
together:
```json
```text
~/backstage // cloned from Github
~/my-backstage-application // generated using npx
```
For example, you might want to make modifications to `@backstage/core-plugin-api` and try them out in your company's
instance of Backstage.
## Linking in Backstage NPM Packages
To link in external packages, add them to your root `package.json` and `lerna.json`
`"workspace"` paths. These can be either relative or absolute paths with or without
globs.
For example:
```json title="/lerna.json"
...
"packages": [
"packages/*",
"plugins/*",
"../backstage/packages/core-plugin-api", // New path added to work on @backstage/core-plugin-api
],
...
```
Then reinstall packages to make yarn set up symlinks:
```json title="/package.json"
...
"workspaces": {
"packages": [
"packages/*",
"plugins/*",
"../backstage/packages/core-plugin-api", // New path added to work on @backstage/core-plugin-api
],
}
...
```
Now reinstall all packages from the root to make yarn set up symlinks from your application to the core Backstage clone:
```bash
yarn install
```
## Making Backstage Changes
With this in place you can now modify the `@backstage/core-plugin-api` package
within the main repo, and have those changes be reflected and tested in your
app. Simply run your app using `yarn dev` (or `yarn start` for just frontend) as
normal.
Note that for backend packages you need to make sure that linked packages are
## Common Problems
### Backend Issues
For backend packages you need to make sure that linked packages are
not dependencies of any non-linked package. If you for example want to work on
`@backstage/backend-common`, you need to also link in other backend plugins and
packages that depend on `@backstage/backend-common`, or temporarily disable
@@ -39,6 +73,8 @@ those plugins in your backend. This is because the transformation of backend
module tree stops whenever a non-local package is encountered, and from that
point node will `require` packages directly for that entire module subtree.
### Typescript Issues
Type checking can also have issues when linking in external packages, since the
linked in packages will use the types in the external project and dependency
version mismatches between the two projects may cause errors. To fix any of
@@ -54,3 +90,17 @@ invalidate correctly for the linked in packages, causing type checking to not
reflect changes made to types. You can work around this by either setting
`compilerOptions.incremental = false` in `tsconfig.json`, or by deleting the
types cache folder `dist-types` before running `yarn tsc`.
### Version Issues
While `yarn install` might not error, it does not mean that the linking worked properly.
You will know that linking worked properly when:
1. Your Backstage application root `/node_modules/@backstage/[some package]` is a symlink
2. Your Backstage application `/packages/app/node_modules` and `/packages/backend/node_modules` does
not contain the package you are attempting to link!
If you see Yarn continuing to download the package you are trying to link from NPM, you might need to be
explicit in your `package.json` version so that it exactly matches what you have in the cloned Backstage
repository on your machine. For example, if you have cloned `/plugins/catalog` with version
`"version": "1.19.1-next.1"` you will need to be explicit in your application to point to `"1.19.1-next.1"`.
+18 -3
View File
@@ -6,13 +6,26 @@ authorization token.
## Setup backend
1. Install the backend plugin:
Install the backend plugin
```bash
# From your Backstage root directory
yarn --cwd packages/backend add @backstage/plugin-user-settings-backend
```
### New backend
Add the plugin to your backend in `packages/backend/src/index.ts`:
```ts
backend.add(import('@backstage/plugin-user-settings-backend/alpha'));
```
To get real-time updates of the user settings across different user sessions, you must also install
the `@backstage/plugin-signals-backend` plugin.
### Old backend
1. Configure the routes by adding a new `userSettings.ts` file in
`packages/backend/src/plugins/`:
@@ -29,7 +42,7 @@ export default async function createPlugin(env: PluginEnvironment) {
}
```
3. Add the new routes to your backend by modifying `packages/backend/src/index.ts`:
2. Add the new routes to your backend by modifying `packages/backend/src/index.ts`:
```diff
// packages/backend/src/index.ts
@@ -58,6 +71,7 @@ To make use of the user settings backend, replace the `WebStorage` with the
+ storageApiRef,
} from '@backstage/core-plugin-api';
+import { UserSettingsStorage } from '@backstage/plugin-user-settings';
+import { signalApiRef } from '@backstage/plugin-signals-react';
export const apis: AnyApiFactory[] = [
+ createApiFactory({
@@ -66,7 +80,8 @@ To make use of the user settings backend, replace the `WebStorage` with the
+ discoveryApi: discoveryApiRef,
+ errorApi: errorApiRef,
+ fetchApi: fetchApiRef,
+ identityApi: identityApiRef
+ identityApi: identityApiRef,
+ signalApi: signalApiRef, // Optional
+ },
+ factory: deps => UserSettingsStorage.create(deps),
+ }),
@@ -6,6 +6,7 @@
import express from 'express';
import { IdentityApi } from '@backstage/plugin-auth-node';
import { PluginDatabaseManager } from '@backstage/backend-common';
import { SignalsService } from '@backstage/plugin-signals-node';
// @public
export function createRouter(options: RouterOptions): Promise<express.Router>;
@@ -16,6 +17,8 @@ export interface RouterOptions {
database: PluginDatabaseManager;
// (undocumented)
identity: IdentityApi;
// (undocumented)
signals?: SignalsService;
}
// (No @packageDocumentation comment for this package)
@@ -51,6 +51,8 @@
"@backstage/config": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/plugin-auth-node": "workspace:^",
"@backstage/plugin-signals-node": "workspace:^",
"@backstage/plugin-user-settings-common": "workspace:^",
"@backstage/types": "workspace:^",
"@types/express": "^4.17.6",
"express": "^4.17.1",
+5 -3
View File
@@ -15,10 +15,11 @@
*/
import {
createBackendPlugin,
coreServices,
createBackendPlugin,
} from '@backstage/backend-plugin-api';
import { createRouter } from './service/router';
import { signalsServiceRef } from '@backstage/plugin-signals-node';
/**
* The user settings backend plugin.
@@ -33,9 +34,10 @@ export default createBackendPlugin({
database: coreServices.database,
identity: coreServices.identity,
httpRouter: coreServices.httpRouter,
signals: signalsServiceRef,
},
async init({ database, identity, httpRouter }) {
httpRouter.use(await createRouter({ database, identity }));
async init({ database, identity, httpRouter, signals }) {
httpRouter.use(await createRouter({ database, identity, signals }));
},
});
},
@@ -22,6 +22,7 @@ import express from 'express';
import request from 'supertest';
import { UserSettingsStore } from '../database/UserSettingsStore';
import { createRouterInternal } from './router';
import { SignalsService } from '@backstage/plugin-signals-node';
describe('createRouter', () => {
const userSettingsStore: jest.Mocked<UserSettingsStore> = {
@@ -36,6 +37,9 @@ describe('createRouter', () => {
const identityApi: jest.Mocked<Partial<IdentityApi>> = {
getIdentity: getIdentityMock,
};
const signalService: jest.Mocked<SignalsService> = {
publish: jest.fn(),
};
let app: express.Express;
@@ -43,6 +47,7 @@ describe('createRouter', () => {
const router = await createRouterInternal({
userSettingsStore,
identity: identityApi as IdentityApi,
signals: signalService as SignalsService,
});
app = express().use(router);
@@ -118,6 +123,11 @@ describe('createRouter', () => {
bucket: 'my-bucket',
key: 'my-key',
});
expect(signalService.publish).toHaveBeenCalledWith({
recipients: { type: 'user', entityRef: 'user-1' },
channel: `user-settings`,
message: { type: 'key-deleted', key: 'my-key' },
});
});
it('returns an error if the Authorization header is missing', async () => {
@@ -167,6 +177,11 @@ describe('createRouter', () => {
bucket: 'my-bucket',
key: 'my-key',
});
expect(signalService.publish).toHaveBeenCalledWith({
recipients: { type: 'user', entityRef: 'user-1' },
channel: `user-settings`,
message: { type: 'key-changed', key: 'my-key' },
});
});
it('returns an error if the value is not given', async () => {
@@ -21,6 +21,8 @@ import express, { Request } from 'express';
import Router from 'express-promise-router';
import { DatabaseUserSettingsStore } from '../database/DatabaseUserSettingsStore';
import { UserSettingsStore } from '../database/UserSettingsStore';
import { SignalsService } from '@backstage/plugin-signals-node';
import { UserSettingsSignal } from '@backstage/plugin-user-settings-common';
/**
* @public
@@ -28,6 +30,7 @@ import { UserSettingsStore } from '../database/UserSettingsStore';
export interface RouterOptions {
database: PluginDatabaseManager;
identity: IdentityApi;
signals?: SignalsService;
}
/**
@@ -45,12 +48,14 @@ export async function createRouter(
return await createRouterInternal({
userSettingsStore,
identity: options.identity,
signals: options.signals,
});
}
export async function createRouterInternal(options: {
identity: IdentityApi;
userSettingsStore: UserSettingsStore;
signals?: SignalsService;
}): Promise<express.Router> {
const router = Router();
router.use(express.json());
@@ -104,6 +109,14 @@ export async function createRouterInternal(options: {
key,
});
if (options.signals) {
await options.signals.publish<UserSettingsSignal>({
recipients: { type: 'user', entityRef: userEntityRef },
channel: `user-settings`,
message: { type: 'key-changed', key },
});
}
res.json(setting);
});
@@ -113,6 +126,13 @@ export async function createRouterInternal(options: {
const { bucket, key } = req.params;
await options.userSettingsStore.delete({ userEntityRef, bucket, key });
if (options.signals) {
await options.signals.publish<UserSettingsSignal>({
recipients: { type: 'user', entityRef: userEntityRef },
channel: 'user-settings',
message: { type: 'key-deleted', key },
});
}
res.status(204).end();
});
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
+5
View File
@@ -0,0 +1,5 @@
# @backstage/plugin-user-settings-common
Welcome to the common package for the user-settings plugin!
_This plugin was created through the Backstage CLI_
@@ -0,0 +1,13 @@
## API Report File for "@backstage/plugin-user-settings-common"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
// @public (undocumented)
export type UserSettingsSignal = {
type: 'key-changed' | 'key-deleted';
key: string;
};
// (No @packageDocumentation comment for this package)
```
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-plugin-user-settings-common
title: '@backstage/plugin-user-settings-common'
description: The Backstage common plugin to manage user settings
spec:
lifecycle: experimental
type: backstage-common-library
owner: maintainers
+37
View File
@@ -0,0 +1,37 @@
{
"name": "@backstage/plugin-user-settings-common",
"version": "0.0.0",
"description": "Common functionalities for the user-settings plugin",
"backstage": {
"role": "common-library"
},
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "plugins/user-settings-common"
},
"license": "Apache-2.0",
"sideEffects": false,
"main": "src/index.ts",
"types": "src/index.ts",
"files": [
"dist"
],
"scripts": {
"build": "backstage-cli package build",
"clean": "backstage-cli package clean",
"lint": "backstage-cli package lint",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack",
"test": "backstage-cli package test"
},
"devDependencies": {
"@backstage/cli": "workspace:^"
}
}
+17
View File
@@ -0,0 +1,17 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './types';
@@ -0,0 +1,16 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export {};
+21
View File
@@ -0,0 +1,21 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/** @public */
export type UserSettingsSignal = {
type: 'key-changed' | 'key-deleted';
key: string;
};
+2
View File
@@ -22,6 +22,7 @@ import { PropsWithChildren } from 'react';
import { default as React_2 } from 'react';
import { RouteRef } from '@backstage/core-plugin-api';
import { SessionApi } from '@backstage/core-plugin-api';
import { SignalApi } from '@backstage/plugin-signals-react';
import { StorageApi } from '@backstage/core-plugin-api';
import { StorageValueSnapshot } from '@backstage/core-plugin-api';
import { TabProps } from '@material-ui/core/Tab';
@@ -133,6 +134,7 @@ export class UserSettingsStorage implements StorageApi {
discoveryApi: DiscoveryApi;
errorApi: ErrorApi;
identityApi: IdentityApi;
signalApi?: SignalApi;
namespace?: string;
}): UserSettingsStorage;
// (undocumented)
+2
View File
@@ -56,6 +56,8 @@
"@backstage/errors": "workspace:^",
"@backstage/frontend-plugin-api": "workspace:^",
"@backstage/plugin-catalog-react": "workspace:^",
"@backstage/plugin-signals-react": "workspace:^",
"@backstage/plugin-user-settings-common": "workspace:^",
"@backstage/theme": "workspace:^",
"@backstage/types": "workspace:^",
"@material-ui/core": "^4.12.2",
@@ -25,7 +25,9 @@ import {
} from '@backstage/core-plugin-api';
import { ResponseError } from '@backstage/errors';
import { JsonValue, Observable } from '@backstage/types';
import { SignalApi, SignalSubscriber } from '@backstage/plugin-signals-react';
import ObservableImpl from 'zen-observable';
import { UserSettingsSignal } from '@backstage/plugin-user-settings-common';
const JSON_HEADERS = {
'Content-Type': 'application/json; charset=utf-8',
@@ -57,6 +59,7 @@ export class UserSettingsStorage implements StorageApi {
private readonly errorApi: ErrorApi,
private readonly identityApi: IdentityApi,
private readonly fallback: WebStorage,
private readonly signalApi?: SignalApi,
) {}
static create(options: {
@@ -64,6 +67,7 @@ export class UserSettingsStorage implements StorageApi {
discoveryApi: DiscoveryApi;
errorApi: ErrorApi;
identityApi: IdentityApi;
signalApi?: SignalApi;
namespace?: string;
}): UserSettingsStorage {
return new UserSettingsStorage(
@@ -76,6 +80,7 @@ export class UserSettingsStorage implements StorageApi {
namespace: options.namespace,
errorApi: options.errorApi,
}),
options.signalApi,
);
}
@@ -145,15 +150,33 @@ export class UserSettingsStorage implements StorageApi {
this.observables.set(
key,
new ObservableImpl<StorageValueSnapshot<JsonValue>>(subscriber => {
let signalSubscription: SignalSubscriber | undefined;
this.subscribers.add(subscriber);
// TODO(freben): Introduce server polling or similar, to ensure that different devices update when values change
Promise.resolve()
.then(() => this.get(key))
.then(snapshot => subscriber.next(snapshot))
.catch(error => this.errorApi.post(error));
const updateSnapshot = () => {
Promise.resolve()
.then(() => this.get(key))
.then(snapshot => subscriber.next(snapshot))
.catch(error => this.errorApi.post(error));
};
if (this.signalApi) {
signalSubscription = this.signalApi.subscribe(
`user-settings`,
(msg: UserSettingsSignal) => {
if (msg.key === key) {
updateSnapshot();
}
},
);
}
updateSnapshot();
return () => {
if (signalSubscription) {
signalSubscription.unsubscribe();
}
this.subscribers.delete(subscriber);
};
}).filter(({ key: messageKey }) => messageKey === key),
+12
View File
@@ -7820,6 +7820,8 @@ __metadata:
"@backstage/config": "workspace:^"
"@backstage/errors": "workspace:^"
"@backstage/plugin-auth-node": "workspace:^"
"@backstage/plugin-signals-node": "workspace:^"
"@backstage/plugin-user-settings-common": "workspace:^"
"@backstage/types": "workspace:^"
"@types/express": ^4.17.6
"@types/supertest": ^2.0.8
@@ -7832,6 +7834,14 @@ __metadata:
languageName: unknown
linkType: soft
"@backstage/plugin-user-settings-common@workspace:^, @backstage/plugin-user-settings-common@workspace:plugins/user-settings-common":
version: 0.0.0-use.local
resolution: "@backstage/plugin-user-settings-common@workspace:plugins/user-settings-common"
dependencies:
"@backstage/cli": "workspace:^"
languageName: unknown
linkType: soft
"@backstage/plugin-user-settings@workspace:^, @backstage/plugin-user-settings@workspace:plugins/user-settings":
version: 0.0.0-use.local
resolution: "@backstage/plugin-user-settings@workspace:plugins/user-settings"
@@ -7846,6 +7856,8 @@ __metadata:
"@backstage/frontend-plugin-api": "workspace:^"
"@backstage/plugin-catalog": "workspace:^"
"@backstage/plugin-catalog-react": "workspace:^"
"@backstage/plugin-signals-react": "workspace:^"
"@backstage/plugin-user-settings-common": "workspace:^"
"@backstage/test-utils": "workspace:^"
"@backstage/theme": "workspace:^"
"@backstage/types": "workspace:^"