Merge branch 'backstage:master' into master
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend-module-msgraph': patch
|
||||
---
|
||||
|
||||
Added $select attribute to user query
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-react': patch
|
||||
---
|
||||
|
||||
humanizeEntityRef function can now be forced to include default namespace
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/config-loader': patch
|
||||
---
|
||||
|
||||
No longer log when reloading remote config.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-plugin-api': patch
|
||||
---
|
||||
|
||||
Service are now scoped to either `'plugin'` or `'root'` scope. Service factories have been updated to provide dependency instances directly rather than factory functions.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
---
|
||||
|
||||
Support displaying and ordering by counts in `EntityTagPicker` field. Add the `showCounts` option to enable this. Also support configuring `helperText`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-node': patch
|
||||
---
|
||||
|
||||
Updated usage of experimental backend service APIs.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-app-api': patch
|
||||
---
|
||||
|
||||
Updated service implementations and backend wiring to support scoped service.
|
||||
@@ -49,6 +49,8 @@ The api for providing custom rules may differ between plugins, but there should
|
||||
// packages/backend/src/plugins/catalog.ts
|
||||
|
||||
import { isInSystemRule } from './permission';
|
||||
// The CatalogBuilder with the addPermissionRules function is in the alpha path
|
||||
import { CatalogBuilder } from '@backstage/plugin-catalog-backend/alpha';
|
||||
|
||||
...
|
||||
|
||||
@@ -56,7 +58,7 @@ export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
const builder = await CatalogBuilder.create(env);
|
||||
builder.addPermissionRules(isInSystem);
|
||||
builder.addPermissionRules(isInSystemRule);
|
||||
...
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -77,10 +77,7 @@ export default async function createPlugin(
|
||||
logger: env.logger,
|
||||
discovery: env.discovery,
|
||||
policy: new TestPermissionPolicy(),
|
||||
identity: IdentityClient.create({
|
||||
discovery: env.discovery,
|
||||
issuer: await env.discovery.getExternalBaseUrl('auth'),
|
||||
}),
|
||||
identity: env.identity,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
id: feature-flags
|
||||
title: Feature Flags
|
||||
description: Details the process of defining setting and reading a plugin feature flag.
|
||||
---
|
||||
|
||||
Backstage offers the ability to define feature flags inside a plugin. This allows you to restrict parts of your plugin to those individual users who have toggled the feature flag to on.
|
||||
|
||||
This page describes the process of defining setting and reading a plugin feature flag. If you are looking for using feature flags with software templates that can be found under [Writing Templates](https://backstage.io/docs/features/software-templates/writing-templates#remove-sections-or-fields-based-on-feature-flags).
|
||||
|
||||
## Defining a Feature Flag
|
||||
|
||||
Before using a feature flag we must first define it. This is done when we create the plugin by passing the name of the feature flag into the `featureFlags` array.
|
||||
|
||||
```ts
|
||||
/* src/plugin.ts */
|
||||
import { createPlugin, createRouteRef } from '@backstage/core-plugin-api';
|
||||
import ExampleComponent from './components/ExampleComponent';
|
||||
|
||||
export const examplePlugin = createPlugin({
|
||||
id: 'example',
|
||||
routes: {
|
||||
root: rootRouteRef,
|
||||
},
|
||||
featureFlags: [{ name: 'show-example-feature' }],
|
||||
});
|
||||
```
|
||||
|
||||
## Enabling Feature Flags
|
||||
|
||||
Feature flags are defaulted to off and can be updated by individual users in the backstage interface.
|
||||
|
||||
These are set by navigating to the page under `Settings` > `Feature Flags`.
|
||||
|
||||
The users selection is saved in the users browsers local storage. Once toggled it may be required for a user to refresh the page to see any new changes.
|
||||
|
||||
## FeatureFlagged Component
|
||||
|
||||
The easiest way to control content based on the state of a feature flag is to use the [FeatureFlagged](https://backstage.io/docs/reference/core-app-api.featureflagged) component.
|
||||
|
||||
```ts
|
||||
import { FeatureFlagged } from '@backstage/core-app-api'
|
||||
|
||||
...
|
||||
|
||||
<FeatureFlagged with="show-example-feature">
|
||||
<NewFeatureComponent />
|
||||
</FeatureFlagged>
|
||||
|
||||
<FeatureFlagged without="show-example-feature">
|
||||
<PreviousFeatureComponent />
|
||||
</FeatureFlagged>
|
||||
```
|
||||
|
||||
## Evaluating Feature Flag State
|
||||
|
||||
It is also possible to test the feature flag state using the [FeatureFlags Api](https://backstage.io/docs/reference/core-plugin-api.featureflagsapi).
|
||||
|
||||
```ts
|
||||
import { useApi, featureFlagsApiRef } from '@backstage/core-plugin-api';
|
||||
|
||||
const featureFlagsApi = useApi(featureFlagsApiRef);
|
||||
const isOn = featureFlagsApi.isActive('show-example-feature');
|
||||
```
|
||||
+1
-1
@@ -127,10 +127,10 @@ nav:
|
||||
- Create a Backstage Plugin: 'plugins/create-a-plugin.md'
|
||||
- Plugin Development: 'plugins/plugin-development.md'
|
||||
- Structure of a plugin: 'plugins/structure-of-a-plugin.md'
|
||||
- Plugin Development: 'plugins/plugin-development.md'
|
||||
- Integrate into the Software Catalog: 'plugins/integrating-plugin-into-software-catalog.md'
|
||||
- Composability System: 'plugins/composability.md'
|
||||
- Plugin Analytics: 'plugins/analytics.md'
|
||||
- Feature Flags: 'plugins/feature-flags.md'
|
||||
- Backends and APIs:
|
||||
- Proxying: 'plugins/proxying.md'
|
||||
- Backend plugin: 'plugins/backend-plugin.md'
|
||||
|
||||
@@ -18,6 +18,7 @@ import { CacheManager } from '@backstage/backend-common';
|
||||
import {
|
||||
configServiceRef,
|
||||
createServiceFactory,
|
||||
pluginMetadataServiceRef,
|
||||
cacheServiceRef,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
|
||||
@@ -25,13 +26,13 @@ import {
|
||||
export const cacheFactory = createServiceFactory({
|
||||
service: cacheServiceRef,
|
||||
deps: {
|
||||
configFactory: configServiceRef,
|
||||
config: configServiceRef,
|
||||
plugin: pluginMetadataServiceRef,
|
||||
},
|
||||
factory: async ({ configFactory }) => {
|
||||
const config = await configFactory('root');
|
||||
async factory({ config }) {
|
||||
const cacheManager = CacheManager.fromConfig(config);
|
||||
return async (pluginId: string) => {
|
||||
return cacheManager.forPlugin(pluginId);
|
||||
return async ({ plugin }) => {
|
||||
return cacheManager.forPlugin(plugin.getId());
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -19,23 +19,20 @@ import {
|
||||
configServiceRef,
|
||||
createServiceFactory,
|
||||
loggerToWinstonLogger,
|
||||
loggerServiceRef,
|
||||
rootLoggerServiceRef,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
|
||||
/** @public */
|
||||
export const configFactory = createServiceFactory({
|
||||
service: configServiceRef,
|
||||
deps: {
|
||||
loggerFactory: loggerServiceRef,
|
||||
logger: rootLoggerServiceRef,
|
||||
},
|
||||
factory: async ({ loggerFactory }) => {
|
||||
const logger = await loggerFactory('root');
|
||||
async factory({ logger }) {
|
||||
const config = await loadBackendConfig({
|
||||
argv: process.argv,
|
||||
logger: loggerToWinstonLogger(logger),
|
||||
});
|
||||
return async () => {
|
||||
return config;
|
||||
};
|
||||
return config;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -19,19 +19,20 @@ import {
|
||||
configServiceRef,
|
||||
createServiceFactory,
|
||||
databaseServiceRef,
|
||||
pluginMetadataServiceRef,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
|
||||
/** @public */
|
||||
export const databaseFactory = createServiceFactory({
|
||||
service: databaseServiceRef,
|
||||
deps: {
|
||||
configFactory: configServiceRef,
|
||||
config: configServiceRef,
|
||||
plugin: pluginMetadataServiceRef,
|
||||
},
|
||||
factory: async ({ configFactory }) => {
|
||||
const config = await configFactory('root');
|
||||
async factory({ config }) {
|
||||
const databaseManager = DatabaseManager.fromConfig(config);
|
||||
return async (pluginId: string) => {
|
||||
return databaseManager.forPlugin(pluginId);
|
||||
return async ({ plugin }) => {
|
||||
return databaseManager.forPlugin(plugin.getId());
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -25,10 +25,9 @@ import {
|
||||
export const discoveryFactory = createServiceFactory({
|
||||
service: discoveryServiceRef,
|
||||
deps: {
|
||||
configFactory: configServiceRef,
|
||||
config: configServiceRef,
|
||||
},
|
||||
factory: async ({ configFactory }) => {
|
||||
const config = await configFactory('root');
|
||||
async factory({ config }) {
|
||||
const discovery = SingleHostDiscovery.fromConfig(config);
|
||||
return async () => {
|
||||
return discovery;
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
createServiceFactory,
|
||||
httpRouterServiceRef,
|
||||
configServiceRef,
|
||||
pluginMetadataServiceRef,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import Router from 'express-promise-router';
|
||||
import { Handler } from 'express';
|
||||
@@ -27,18 +28,20 @@ import { createServiceBuilder } from '@backstage/backend-common';
|
||||
export const httpRouterFactory = createServiceFactory({
|
||||
service: httpRouterServiceRef,
|
||||
deps: {
|
||||
configFactory: configServiceRef,
|
||||
config: configServiceRef,
|
||||
plugin: pluginMetadataServiceRef,
|
||||
},
|
||||
factory: async ({ configFactory }) => {
|
||||
async factory({ config }) {
|
||||
const rootRouter = Router();
|
||||
|
||||
const service = createServiceBuilder(module)
|
||||
.loadConfig(await configFactory('root'))
|
||||
.loadConfig(config)
|
||||
.addRouter('', rootRouter);
|
||||
|
||||
await service.start();
|
||||
|
||||
return async (pluginId?: string) => {
|
||||
return async ({ plugin }) => {
|
||||
const pluginId = plugin.getId();
|
||||
const path = pluginId ? `/api/${pluginId}` : '';
|
||||
return {
|
||||
use(handler: Handler) {
|
||||
|
||||
@@ -14,38 +14,23 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createRootLogger } from '@backstage/backend-common';
|
||||
import {
|
||||
createServiceFactory,
|
||||
Logger,
|
||||
loggerServiceRef,
|
||||
pluginMetadataServiceRef,
|
||||
rootLoggerServiceRef,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { Logger as WinstonLogger } from 'winston';
|
||||
|
||||
class BackstageLogger implements Logger {
|
||||
static fromWinston(logger: WinstonLogger): BackstageLogger {
|
||||
return new BackstageLogger(logger);
|
||||
}
|
||||
|
||||
private constructor(private readonly winston: WinstonLogger) {}
|
||||
|
||||
info(message: string, ...meta: any[]): void {
|
||||
this.winston.info(message, ...meta);
|
||||
}
|
||||
|
||||
child(fields: { [name: string]: string }): Logger {
|
||||
return new BackstageLogger(this.winston.child(fields));
|
||||
}
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export const loggerFactory = createServiceFactory({
|
||||
service: loggerServiceRef,
|
||||
deps: {},
|
||||
factory: async () => {
|
||||
const root = BackstageLogger.fromWinston(createRootLogger());
|
||||
return async (pluginId: string) => {
|
||||
return root.child({ pluginId });
|
||||
deps: {
|
||||
rootLogger: rootLoggerServiceRef,
|
||||
plugin: pluginMetadataServiceRef,
|
||||
},
|
||||
async factory({ rootLogger }) {
|
||||
return async ({ plugin }) => {
|
||||
return rootLogger.child({ pluginId: plugin.getId() });
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -27,20 +27,16 @@ import { ServerPermissionClient } from '@backstage/plugin-permission-node';
|
||||
export const permissionsFactory = createServiceFactory({
|
||||
service: permissionsServiceRef,
|
||||
deps: {
|
||||
configFactory: configServiceRef,
|
||||
discoveryFactory: discoveryServiceRef,
|
||||
tokenManagerFactory: tokenManagerServiceRef,
|
||||
config: configServiceRef,
|
||||
discovery: discoveryServiceRef,
|
||||
tokenManager: tokenManagerServiceRef,
|
||||
},
|
||||
factory: async ({ configFactory, discoveryFactory, tokenManagerFactory }) => {
|
||||
const config = await configFactory('root');
|
||||
const discovery = await discoveryFactory('root');
|
||||
const tokenManager = await tokenManagerFactory('root');
|
||||
const permissions = ServerPermissionClient.fromConfig(config, {
|
||||
discovery,
|
||||
tokenManager,
|
||||
});
|
||||
return async (_pluginId: string) => {
|
||||
return permissions;
|
||||
async factory({ config }) {
|
||||
return async ({ discovery, tokenManager }) => {
|
||||
return ServerPermissionClient.fromConfig(config, {
|
||||
discovery,
|
||||
tokenManager,
|
||||
});
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2022 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.
|
||||
*/
|
||||
|
||||
import { createRootLogger } from '@backstage/backend-common';
|
||||
import {
|
||||
createServiceFactory,
|
||||
Logger,
|
||||
rootLoggerServiceRef,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { Logger as WinstonLogger } from 'winston';
|
||||
|
||||
class BackstageLogger implements Logger {
|
||||
static fromWinston(logger: WinstonLogger): BackstageLogger {
|
||||
return new BackstageLogger(logger);
|
||||
}
|
||||
|
||||
private constructor(private readonly winston: WinstonLogger) {}
|
||||
|
||||
info(message: string, ...meta: any[]): void {
|
||||
this.winston.info(message, ...meta);
|
||||
}
|
||||
|
||||
child(fields: { [name: string]: string }): Logger {
|
||||
return new BackstageLogger(this.winston.child(fields));
|
||||
}
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export const loggerFactory = createServiceFactory({
|
||||
service: rootLoggerServiceRef,
|
||||
deps: {},
|
||||
async factory() {
|
||||
return BackstageLogger.fromWinston(createRootLogger());
|
||||
},
|
||||
});
|
||||
@@ -17,6 +17,7 @@
|
||||
import {
|
||||
configServiceRef,
|
||||
createServiceFactory,
|
||||
pluginMetadataServiceRef,
|
||||
schedulerServiceRef,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { TaskScheduler } from '@backstage/backend-tasks';
|
||||
@@ -25,13 +26,13 @@ import { TaskScheduler } from '@backstage/backend-tasks';
|
||||
export const schedulerFactory = createServiceFactory({
|
||||
service: schedulerServiceRef,
|
||||
deps: {
|
||||
configFactory: configServiceRef,
|
||||
config: configServiceRef,
|
||||
plugin: pluginMetadataServiceRef,
|
||||
},
|
||||
factory: async ({ configFactory }) => {
|
||||
const config = await configFactory('root');
|
||||
async factory({ config }) {
|
||||
const taskScheduler = TaskScheduler.fromConfig(config);
|
||||
return async (pluginId: string) => {
|
||||
return taskScheduler.forPlugin(pluginId);
|
||||
return async ({ plugin }) => {
|
||||
return taskScheduler.forPlugin(plugin.getId());
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -27,32 +27,11 @@ import { ServerTokenManager } from '@backstage/backend-common';
|
||||
export const tokenManagerFactory = createServiceFactory({
|
||||
service: tokenManagerServiceRef,
|
||||
deps: {
|
||||
configFactory: configServiceRef,
|
||||
loggerFactory: loggerServiceRef,
|
||||
config: configServiceRef,
|
||||
logger: loggerServiceRef,
|
||||
},
|
||||
factory: async ({ configFactory, loggerFactory }) => {
|
||||
const logger = await loggerFactory('root');
|
||||
const config = await configFactory('root');
|
||||
return async (_pluginId: string) => {
|
||||
// doesn't the logger want to be inferred from the plugin tho here?
|
||||
// maybe ... also why do we recreate it every time otherwise
|
||||
// we should memoize on a per plugin right? so I think it's should be fine to re-use the plugin one
|
||||
// we shouldn't recreate on a per plugin basis.
|
||||
// hm - on the other hand, is this really ever called more than once?
|
||||
// not this function right. should only be called when the plugin requests this serviceRef
|
||||
// yeah so no need to worry about memo probably
|
||||
// but we still want to scope the logger to the ServrTokenmanagfer>?
|
||||
// mm sure maybe
|
||||
// maybe in this case it doesn't provide so much value b
|
||||
// oh hang on - isn't it up to THE MANAGER to make a child internally if it wants to do that
|
||||
// so that it becomes a property intrinsic to that class, no matter how it's constructed
|
||||
// or is that too much responsibility for it - making the constructor complex so to speak, making it harder to tweak that behavior
|
||||
// this is not ultra efficient :)
|
||||
|
||||
// I think the naming here is wrong to be gonest
|
||||
// this isn't like the cache manager or the database manager
|
||||
// the manager name is confusuion i think
|
||||
// aye perhaps
|
||||
async factory() {
|
||||
return async ({ config, logger }) => {
|
||||
return ServerTokenManager.fromConfig(config, {
|
||||
logger: loggerToWinstonLogger(logger),
|
||||
});
|
||||
|
||||
@@ -27,15 +27,14 @@ import {
|
||||
export const urlReaderFactory = createServiceFactory({
|
||||
service: urlReaderServiceRef,
|
||||
deps: {
|
||||
configFactory: configServiceRef,
|
||||
loggerFactory: loggerServiceRef,
|
||||
config: configServiceRef,
|
||||
logger: loggerServiceRef,
|
||||
},
|
||||
factory: async ({ configFactory, loggerFactory }) => {
|
||||
return async (pluginId: string) => {
|
||||
const logger = await loggerFactory(pluginId);
|
||||
async factory() {
|
||||
return async ({ config, logger }) => {
|
||||
return UrlReaders.default({
|
||||
config,
|
||||
logger: loggerToWinstonLogger(logger),
|
||||
config: await configFactory(pluginId),
|
||||
});
|
||||
};
|
||||
},
|
||||
|
||||
@@ -50,11 +50,12 @@ export class BackendInitializer {
|
||||
if (extensionPoint) {
|
||||
result.set(name, extensionPoint);
|
||||
} else {
|
||||
const factory = await this.#serviceHolder.get(
|
||||
const impl = await this.#serviceHolder.get(
|
||||
ref as ServiceRef<unknown>,
|
||||
pluginId,
|
||||
);
|
||||
if (factory) {
|
||||
result.set(name, await factory(pluginId));
|
||||
if (impl) {
|
||||
result.set(name, impl);
|
||||
} else {
|
||||
missingRefs.add(ref);
|
||||
}
|
||||
|
||||
@@ -18,158 +18,209 @@ import {
|
||||
createServiceRef,
|
||||
createServiceFactory,
|
||||
ServiceRef,
|
||||
pluginMetadataServiceRef,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { ServiceRegistry } from './ServiceRegistry';
|
||||
|
||||
const ref1 = createServiceRef<{ x: number; pluginId: string }>({
|
||||
const ref1 = createServiceRef<{ x: number }>({
|
||||
id: '1',
|
||||
});
|
||||
const sf1 = createServiceFactory({
|
||||
service: ref1,
|
||||
deps: {},
|
||||
factory: async () => {
|
||||
return async pluginId => {
|
||||
return { x: 1, pluginId };
|
||||
async factory() {
|
||||
return async () => {
|
||||
return { x: 1 };
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const ref2 = createServiceRef<{ x: number; pluginId: string }>({
|
||||
const ref2 = createServiceRef<{ x: number }>({
|
||||
scope: 'root',
|
||||
id: '2',
|
||||
});
|
||||
const sf2 = createServiceFactory({
|
||||
service: ref2,
|
||||
deps: {},
|
||||
factory: async () => {
|
||||
return async pluginId => {
|
||||
return { x: 2, pluginId };
|
||||
};
|
||||
async factory() {
|
||||
return { x: 2 };
|
||||
},
|
||||
});
|
||||
const sf2b = createServiceFactory({
|
||||
service: ref2,
|
||||
deps: {},
|
||||
factory: async () => {
|
||||
return async pluginId => {
|
||||
return { x: 22, pluginId };
|
||||
};
|
||||
async factory() {
|
||||
return { x: 22 };
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
||||
const refDefault1 = createServiceRef<{ x: number; pluginId: string }>({
|
||||
const refDefault1 = createServiceRef<{ x: number }>({
|
||||
id: '1',
|
||||
defaultFactory: async service =>
|
||||
createServiceFactory({
|
||||
service,
|
||||
deps: {},
|
||||
factory: async () => async pluginId => ({ x: 10, pluginId }),
|
||||
}),
|
||||
async factory() {
|
||||
return async () => ({ x: 10 });
|
||||
},
|
||||
})(),
|
||||
});
|
||||
|
||||
const refDefault2a = createServiceRef<{ x: number; pluginId: string }>({
|
||||
const refDefault2a = createServiceRef<{ x: number }>({
|
||||
id: '2a',
|
||||
defaultFactory: async service =>
|
||||
createServiceFactory({
|
||||
service,
|
||||
deps: {},
|
||||
factory: async () => async pluginId => ({ x: 20, pluginId }),
|
||||
async factory() {
|
||||
return async () => ({ x: 20 });
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const refDefault2b = createServiceRef<{ x: number; pluginId: string }>({
|
||||
const refDefault2b = createServiceRef<{ x: number }>({
|
||||
id: '2b',
|
||||
defaultFactory: async service =>
|
||||
createServiceFactory({
|
||||
service,
|
||||
deps: {},
|
||||
factory: async () => async pluginId => ({ x: 220, pluginId }),
|
||||
async factory() {
|
||||
return async () => ({ x: 220 });
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
describe('ServiceRegistry', () => {
|
||||
it('should return undefined if there is no factory defined', async () => {
|
||||
const registry = new ServiceRegistry([]);
|
||||
expect(registry.get(ref1)).toBe(undefined);
|
||||
expect(registry.get(ref1, 'catalog')).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should return a factory for a registered ref', async () => {
|
||||
it('should return an implementation for a registered ref', async () => {
|
||||
const registry = new ServiceRegistry([sf1]);
|
||||
const factory = registry.get(ref1)!;
|
||||
expect(factory).toEqual(expect.any(Function));
|
||||
await expect(factory('catalog')).resolves.toEqual({
|
||||
x: 1,
|
||||
pluginId: 'catalog',
|
||||
});
|
||||
await expect(factory('scaffolder')).resolves.toEqual({
|
||||
x: 1,
|
||||
pluginId: 'scaffolder',
|
||||
});
|
||||
expect(await factory('catalog')).toBe(await factory('catalog'));
|
||||
await expect(registry.get(ref1, 'catalog')).resolves.toEqual({ x: 1 });
|
||||
await expect(registry.get(ref1, 'scaffolder')).resolves.toEqual({ x: 1 });
|
||||
expect(await registry.get(ref1, 'catalog')).toBe(
|
||||
await registry.get(ref1, 'catalog'),
|
||||
);
|
||||
expect(await registry.get(ref1, 'scaffolder')).toBe(
|
||||
await registry.get(ref1, 'scaffolder'),
|
||||
);
|
||||
expect(await registry.get(ref1, 'catalog')).not.toBe(
|
||||
await registry.get(ref1, 'scaffolder'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle multiple factories with different serviceRefs', async () => {
|
||||
const registry = new ServiceRegistry([sf1, sf2]);
|
||||
const factory1 = registry.get(ref1)!;
|
||||
const factory2 = registry.get(ref2)!;
|
||||
expect(factory1).toEqual(expect.any(Function));
|
||||
expect(factory2).toEqual(expect.any(Function));
|
||||
await expect(factory1('catalog')).resolves.toEqual({
|
||||
|
||||
await expect(registry.get(ref1, 'catalog')).resolves.toEqual({
|
||||
x: 1,
|
||||
pluginId: 'catalog',
|
||||
});
|
||||
await expect(factory2('catalog')).resolves.toEqual({
|
||||
await expect(registry.get(ref2, 'catalog')).resolves.toEqual({
|
||||
x: 2,
|
||||
});
|
||||
expect(await registry.get(ref1, 'catalog')).not.toBe(
|
||||
await registry.get(ref2, 'catalog'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not be possible for root scoped services to depend on plugin scoped services', async () => {
|
||||
const factory = createServiceFactory({
|
||||
service: ref2,
|
||||
deps: { pluginDep: ref1 },
|
||||
async factory() {
|
||||
return { x: 2 };
|
||||
},
|
||||
});
|
||||
const registry = new ServiceRegistry([factory, sf1]);
|
||||
await expect(registry.get(ref2, 'catalog')).rejects.toThrow(
|
||||
"Failed to instantiate 'root' scoped service '2' because it depends on 'plugin' scoped service '1'.",
|
||||
);
|
||||
});
|
||||
|
||||
it('should be possible for plugin scoped services to depend on root scoped services', async () => {
|
||||
const factory = createServiceFactory({
|
||||
service: ref1,
|
||||
deps: { rootDep: ref2 },
|
||||
async factory({ rootDep }) {
|
||||
return async () => ({ x: rootDep.x });
|
||||
},
|
||||
});
|
||||
const registry = new ServiceRegistry([factory, sf2]);
|
||||
await expect(registry.get(ref1, 'catalog')).resolves.toEqual({
|
||||
x: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should be possible for root scoped services to depend on root scoped services', async () => {
|
||||
const ref = createServiceRef<{ x: number }>({ id: 'x', scope: 'root' });
|
||||
const factory = createServiceFactory({
|
||||
service: ref,
|
||||
deps: { rootDep: ref2 },
|
||||
async factory({ rootDep }) {
|
||||
return { x: rootDep.x };
|
||||
},
|
||||
});
|
||||
const registry = new ServiceRegistry([factory, sf2]);
|
||||
await expect(registry.get(ref, 'catalog')).resolves.toEqual({
|
||||
x: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the pluginId from the pluginMetadata service', async () => {
|
||||
const ref = createServiceRef<{ pluginId: string }>({ id: 'x' });
|
||||
const factory = createServiceFactory({
|
||||
service: ref,
|
||||
deps: { meta: pluginMetadataServiceRef },
|
||||
async factory() {
|
||||
return async ({ meta }) => ({ pluginId: meta.getId() });
|
||||
},
|
||||
});
|
||||
const registry = new ServiceRegistry([factory]);
|
||||
await expect(registry.get(ref, 'catalog')).resolves.toEqual({
|
||||
pluginId: 'catalog',
|
||||
});
|
||||
expect(await factory1('catalog')).not.toBe(await factory2('catalog'));
|
||||
});
|
||||
|
||||
it('should use the last factory for each ref', async () => {
|
||||
const registry = new ServiceRegistry([sf2, sf2b]);
|
||||
const factory2 = registry.get(ref2)!;
|
||||
await expect(factory2('catalog')).resolves.toEqual({
|
||||
await expect(registry.get(ref2, 'catalog')).resolves.toEqual({
|
||||
x: 22,
|
||||
pluginId: 'catalog',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the defaultFactory from the ref if not provided to the registry', async () => {
|
||||
it('should use the defaultFactory from the ref if not provided to the registry', async () => {
|
||||
const registry = new ServiceRegistry([]);
|
||||
const factory = registry.get(refDefault1)!;
|
||||
expect(factory).toEqual(expect.any(Function));
|
||||
await expect(factory('catalog')).resolves.toEqual({
|
||||
await expect(registry.get(refDefault1, 'catalog')).resolves.toEqual({
|
||||
x: 10,
|
||||
pluginId: 'catalog',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not return the defaultFactory from the ref if provided to the registry', async () => {
|
||||
it('should not use the defaultFactory from the ref if provided to the registry', async () => {
|
||||
const registry = new ServiceRegistry([sf1]);
|
||||
const factory = registry.get(refDefault1)!;
|
||||
expect(factory).toEqual(expect.any(Function));
|
||||
await expect(factory('catalog')).resolves.toEqual({
|
||||
await expect(registry.get(refDefault1, 'catalog')).resolves.toEqual({
|
||||
x: 1,
|
||||
pluginId: 'catalog',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle duplicate defaultFactories by duplicating the implementations', async () => {
|
||||
const registry = new ServiceRegistry([]);
|
||||
const factoryA = registry.get(refDefault2a)!;
|
||||
const factoryB = registry.get(refDefault2b)!;
|
||||
expect(factoryA).toEqual(expect.any(Function));
|
||||
expect(factoryB).toEqual(expect.any(Function));
|
||||
await expect(factoryA('catalog')).resolves.toEqual({
|
||||
await expect(registry.get(refDefault2a, 'catalog')).resolves.toEqual({
|
||||
x: 20,
|
||||
pluginId: 'catalog',
|
||||
});
|
||||
await expect(factoryB('catalog')).resolves.toEqual({
|
||||
await expect(registry.get(refDefault2b, 'catalog')).resolves.toEqual({
|
||||
x: 220,
|
||||
pluginId: 'catalog',
|
||||
});
|
||||
expect(await factoryA('catalog')).toBe(await factoryA('catalog'));
|
||||
expect(await factoryB('catalog')).toBe(await factoryB('catalog'));
|
||||
expect(await factoryA('catalog')).not.toBe(await factoryB('catalog'));
|
||||
expect(await registry.get(refDefault2a, 'catalog')).toBe(
|
||||
await registry.get(refDefault2a, 'catalog'),
|
||||
);
|
||||
expect(await registry.get(refDefault2b, 'catalog')).toBe(
|
||||
await registry.get(refDefault2b, 'catalog'),
|
||||
);
|
||||
expect(await registry.get(refDefault2a, 'catalog')).not.toBe(
|
||||
await registry.get(refDefault2b, 'catalog'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should only call each default factory loader once', async () => {
|
||||
@@ -177,7 +228,9 @@ describe('ServiceRegistry', () => {
|
||||
createServiceFactory({
|
||||
service,
|
||||
deps: {},
|
||||
factory: async () => async () => {},
|
||||
async factory() {
|
||||
return async () => {};
|
||||
},
|
||||
}),
|
||||
);
|
||||
const ref = createServiceRef<void>({
|
||||
@@ -186,17 +239,16 @@ describe('ServiceRegistry', () => {
|
||||
});
|
||||
|
||||
const registry = new ServiceRegistry([]);
|
||||
const factory = registry.get(ref)!;
|
||||
await Promise.all([
|
||||
expect(factory('catalog')).resolves.toBeUndefined(),
|
||||
expect(factory('catalog')).resolves.toBeUndefined(),
|
||||
expect(registry.get(ref, 'catalog')).resolves.toBeUndefined(),
|
||||
expect(registry.get(ref, 'catalog')).resolves.toBeUndefined(),
|
||||
]);
|
||||
expect(factoryLoader).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not call factory functions more than once', async () => {
|
||||
const innerFactory = jest.fn(async (pluginId: string) => {
|
||||
return { x: 1, pluginId };
|
||||
const innerFactory = jest.fn(async () => {
|
||||
return { x: 1 };
|
||||
});
|
||||
const factory = jest.fn(async () => innerFactory);
|
||||
const myFactory = createServiceFactory({
|
||||
@@ -208,17 +260,15 @@ describe('ServiceRegistry', () => {
|
||||
const registry = new ServiceRegistry([myFactory]);
|
||||
|
||||
await Promise.all([
|
||||
registry.get(ref1)!('catalog')!,
|
||||
registry.get(ref1)!('catalog')!,
|
||||
registry.get(ref1)!('catalog')!,
|
||||
registry.get(ref1)!('scaffolder')!,
|
||||
registry.get(ref1)!('scaffolder')!,
|
||||
registry.get(ref1, 'catalog')!,
|
||||
registry.get(ref1, 'catalog')!,
|
||||
registry.get(ref1, 'catalog')!,
|
||||
registry.get(ref1, 'scaffolder')!,
|
||||
registry.get(ref1, 'scaffolder')!,
|
||||
]);
|
||||
|
||||
expect(factory).toHaveBeenCalledTimes(1);
|
||||
expect(innerFactory).toHaveBeenCalledTimes(2);
|
||||
expect(innerFactory).toHaveBeenCalledWith('catalog');
|
||||
expect(innerFactory).toHaveBeenCalledWith('scaffolder');
|
||||
});
|
||||
|
||||
it('should throw if dependencies are not available', async () => {
|
||||
@@ -231,9 +281,8 @@ describe('ServiceRegistry', () => {
|
||||
});
|
||||
|
||||
const registry = new ServiceRegistry([myFactory]);
|
||||
const factory = registry.get(ref1)!;
|
||||
|
||||
await expect(factory('catalog')).rejects.toThrow(
|
||||
await expect(registry.get(ref1, 'catalog')).rejects.toThrow(
|
||||
"Failed to instantiate service '1' for 'catalog' because the following dependent services are missing: '2'",
|
||||
);
|
||||
});
|
||||
@@ -247,8 +296,8 @@ describe('ServiceRegistry', () => {
|
||||
const factoryA = createServiceFactory({
|
||||
service: refA,
|
||||
deps: { b: refB },
|
||||
async factory({ b }) {
|
||||
return async pluginId => b(pluginId);
|
||||
async factory() {
|
||||
return async ({ b }) => b;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -261,9 +310,8 @@ describe('ServiceRegistry', () => {
|
||||
});
|
||||
|
||||
const registry = new ServiceRegistry([factoryA, factoryB]);
|
||||
const factory = registry.get(refA)!;
|
||||
|
||||
await expect(factory('catalog')).rejects.toThrow(
|
||||
await expect(registry.get(refA, 'catalog')).rejects.toThrow(
|
||||
"Failed to instantiate service 'a' for 'catalog' because the factory function threw an error, Error: Failed to instantiate service 'b' for 'catalog' because the following dependent services are missing: 'c', 'd'",
|
||||
);
|
||||
});
|
||||
@@ -278,9 +326,8 @@ describe('ServiceRegistry', () => {
|
||||
});
|
||||
|
||||
const registry = new ServiceRegistry([myFactory]);
|
||||
const factory = registry.get(ref1)!;
|
||||
|
||||
await expect(factory('catalog')).rejects.toThrow(
|
||||
await expect(registry.get(ref1, 'catalog')).rejects.toThrow(
|
||||
"Failed to instantiate service '1' because the top-level factory function threw an error, Error: top-level error",
|
||||
);
|
||||
});
|
||||
@@ -290,17 +337,16 @@ describe('ServiceRegistry', () => {
|
||||
service: ref1,
|
||||
deps: {},
|
||||
async factory() {
|
||||
return pluginId => {
|
||||
throw new Error(`error in plugin ${pluginId}`);
|
||||
return () => {
|
||||
throw new Error(`error in plugin`);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const registry = new ServiceRegistry([myFactory]);
|
||||
const factory = registry.get(ref1)!;
|
||||
|
||||
await expect(factory('catalog')).rejects.toThrow(
|
||||
"Failed to instantiate service '1' for 'catalog' because the factory function threw an error, Error: error in plugin catalog",
|
||||
await expect(registry.get(ref1, 'catalog')).rejects.toThrow(
|
||||
"Failed to instantiate service '1' for 'catalog' because the factory function threw an error, Error: error in plugin",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -313,9 +359,8 @@ describe('ServiceRegistry', () => {
|
||||
});
|
||||
|
||||
const registry = new ServiceRegistry([]);
|
||||
const factory = registry.get(ref)!;
|
||||
|
||||
await expect(factory('catalog')).rejects.toThrow(
|
||||
await expect(registry.get(ref, 'catalog')).rejects.toThrow(
|
||||
"Failed to instantiate service '1' because the default factory loader threw an error, Error: default factory error",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
|
||||
import {
|
||||
ServiceFactory,
|
||||
FactoryFunc,
|
||||
ServiceRef,
|
||||
pluginMetadataServiceRef,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { stringifyError } from '@backstage/errors';
|
||||
|
||||
@@ -37,7 +37,9 @@ export class ServiceRegistry {
|
||||
readonly #implementations: Map<
|
||||
ServiceFactory,
|
||||
{
|
||||
factoryFunc: Promise<FactoryFunc<unknown>>;
|
||||
factoryFunc: Promise<
|
||||
(deps: { [name in string]: unknown }) => Promise<unknown>
|
||||
>;
|
||||
byPlugin: Map<string, Promise<unknown>>;
|
||||
}
|
||||
>;
|
||||
@@ -58,67 +60,123 @@ export class ServiceRegistry {
|
||||
this.#implementations = new Map();
|
||||
}
|
||||
|
||||
get<T>(ref: ServiceRef<T>): FactoryFunc<T> | undefined {
|
||||
let factory = this.#providedFactories.get(ref.id);
|
||||
const { __defaultFactory: defaultFactory } = ref as InternalServiceRef<T>;
|
||||
if (!factory && !defaultFactory) {
|
||||
#resolveFactory(
|
||||
ref: ServiceRef<unknown>,
|
||||
pluginId: string,
|
||||
): Promise<ServiceFactory> | undefined {
|
||||
// Special case handling of the plugin metadata service, generating a custom factory for it each time
|
||||
if (ref.id === pluginMetadataServiceRef.id) {
|
||||
return Promise.resolve({
|
||||
scope: 'plugin',
|
||||
service: pluginMetadataServiceRef,
|
||||
deps: {},
|
||||
factory: async () => async () => ({
|
||||
getId() {
|
||||
return pluginId;
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
let resolvedFactory: Promise<ServiceFactory> | ServiceFactory | undefined =
|
||||
this.#providedFactories.get(ref.id);
|
||||
const { __defaultFactory: defaultFactory } =
|
||||
ref as InternalServiceRef<unknown>;
|
||||
if (!resolvedFactory && !defaultFactory) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return async (pluginId: string): Promise<T> => {
|
||||
if (!factory) {
|
||||
let loadedFactory = this.#loadedDefaultFactories.get(defaultFactory!);
|
||||
if (!loadedFactory) {
|
||||
loadedFactory = Promise.resolve()
|
||||
.then(() => defaultFactory!(ref))
|
||||
.then(f =>
|
||||
typeof f === 'function' ? f() : f,
|
||||
) as Promise<ServiceFactory>;
|
||||
this.#loadedDefaultFactories.set(defaultFactory!, loadedFactory);
|
||||
}
|
||||
// NOTE: This await is safe as long as #providedFactories is not mutated.
|
||||
factory = await loadedFactory.catch(error => {
|
||||
throw new Error(
|
||||
`Failed to instantiate service '${
|
||||
ref.id
|
||||
}' because the default factory loader threw an error, ${stringifyError(
|
||||
error,
|
||||
)}`,
|
||||
if (!resolvedFactory) {
|
||||
let loadedFactory = this.#loadedDefaultFactories.get(defaultFactory!);
|
||||
if (!loadedFactory) {
|
||||
loadedFactory = Promise.resolve()
|
||||
.then(() => defaultFactory!(ref))
|
||||
.then(f =>
|
||||
typeof f === 'function' ? f() : f,
|
||||
) as Promise<ServiceFactory>;
|
||||
this.#loadedDefaultFactories.set(defaultFactory!, loadedFactory);
|
||||
}
|
||||
resolvedFactory = loadedFactory.catch(error => {
|
||||
throw new Error(
|
||||
`Failed to instantiate service '${
|
||||
ref.id
|
||||
}' because the default factory loader threw an error, ${stringifyError(
|
||||
error,
|
||||
)}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve(resolvedFactory);
|
||||
}
|
||||
|
||||
#separateMapForTheRootService = new Map<ServiceFactory, Promise<unknown>>();
|
||||
|
||||
#checkForMissingDeps(factory: ServiceFactory, pluginId: string) {
|
||||
const missingDeps = Object.values(factory.deps).filter(ref => {
|
||||
if (ref.id === pluginMetadataServiceRef.id) {
|
||||
return false;
|
||||
}
|
||||
if (this.#providedFactories.get(ref.id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !(ref as InternalServiceRef<unknown>).__defaultFactory;
|
||||
});
|
||||
|
||||
if (missingDeps.length) {
|
||||
const missing = missingDeps.map(r => `'${r.id}'`).join(', ');
|
||||
throw new Error(
|
||||
`Failed to instantiate service '${factory.service.id}' for '${pluginId}' because the following dependent services are missing: ${missing}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
get<T>(ref: ServiceRef<T>, pluginId: string): Promise<T> | undefined {
|
||||
return this.#resolveFactory(ref, pluginId)?.then(factory => {
|
||||
if (factory.scope === 'root') {
|
||||
let existing = this.#separateMapForTheRootService.get(factory);
|
||||
if (!existing) {
|
||||
this.#checkForMissingDeps(factory, pluginId);
|
||||
const rootDeps = new Array<Promise<[name: string, impl: unknown]>>();
|
||||
|
||||
for (const [name, serviceRef] of Object.entries(factory.deps)) {
|
||||
if (serviceRef.scope !== 'root') {
|
||||
throw new Error(
|
||||
`Failed to instantiate 'root' scoped service '${ref.id}' because it depends on '${serviceRef.scope}' scoped service '${serviceRef.id}'.`,
|
||||
);
|
||||
}
|
||||
const target = this.get(serviceRef, pluginId)!;
|
||||
rootDeps.push(target.then(impl => [name, impl]));
|
||||
}
|
||||
|
||||
existing = Promise.all(rootDeps).then(entries =>
|
||||
factory.factory(Object.fromEntries(entries)),
|
||||
);
|
||||
});
|
||||
this.#separateMapForTheRootService.set(factory, existing);
|
||||
}
|
||||
return existing as Promise<T>;
|
||||
}
|
||||
|
||||
let implementation = this.#implementations.get(factory);
|
||||
if (!implementation) {
|
||||
const missingRefs = new Array<ServiceRef<unknown>>();
|
||||
const factoryDeps: { [name in string]: FactoryFunc<unknown> } = {};
|
||||
this.#checkForMissingDeps(factory, pluginId);
|
||||
const rootDeps = new Array<Promise<[name: string, impl: unknown]>>();
|
||||
|
||||
for (const [name, serviceRef] of Object.entries(factory.deps)) {
|
||||
const target = this.get(serviceRef);
|
||||
if (!target) {
|
||||
missingRefs.push(serviceRef);
|
||||
} else {
|
||||
factoryDeps[name] = target;
|
||||
if (serviceRef.scope === 'root') {
|
||||
const target = this.get(serviceRef, pluginId)!;
|
||||
rootDeps.push(target.then(impl => [name, impl]));
|
||||
}
|
||||
}
|
||||
|
||||
if (missingRefs.length) {
|
||||
const missing = missingRefs.map(r => `'${r.id}'`).join(', ');
|
||||
throw new Error(
|
||||
`Failed to instantiate service '${ref.id}' for '${pluginId}' because the following dependent services are missing: ${missing}`,
|
||||
);
|
||||
}
|
||||
|
||||
implementation = {
|
||||
factoryFunc: Promise.resolve()
|
||||
.then(() => factory!.factory(factoryDeps))
|
||||
factoryFunc: Promise.all(rootDeps)
|
||||
.then(entries => factory.factory(Object.fromEntries(entries)))
|
||||
.catch(error => {
|
||||
const cause = stringifyError(error);
|
||||
throw new Error(
|
||||
`Failed to instantiate service '${
|
||||
ref.id
|
||||
}' because the top-level factory function threw an error, ${stringifyError(
|
||||
error,
|
||||
)}`,
|
||||
`Failed to instantiate service '${ref.id}' because the top-level factory function threw an error, ${cause}`,
|
||||
);
|
||||
}),
|
||||
byPlugin: new Map(),
|
||||
@@ -129,24 +187,29 @@ export class ServiceRegistry {
|
||||
|
||||
let result = implementation.byPlugin.get(pluginId) as Promise<any>;
|
||||
if (!result) {
|
||||
result = implementation.factoryFunc.then(func =>
|
||||
Promise.resolve()
|
||||
.then(() => func(pluginId))
|
||||
.catch(error => {
|
||||
throw new Error(
|
||||
`Failed to instantiate service '${
|
||||
ref.id
|
||||
}' for '${pluginId}' because the factory function threw an error, ${stringifyError(
|
||||
error,
|
||||
)}`,
|
||||
);
|
||||
}),
|
||||
);
|
||||
const allDeps = new Array<Promise<[name: string, impl: unknown]>>();
|
||||
|
||||
for (const [name, serviceRef] of Object.entries(factory.deps)) {
|
||||
const target = this.get(serviceRef, pluginId)!;
|
||||
allDeps.push(target.then(impl => [name, impl]));
|
||||
}
|
||||
|
||||
result = implementation.factoryFunc
|
||||
.then(func =>
|
||||
Promise.all(allDeps).then(entries =>
|
||||
func(Object.fromEntries(entries)),
|
||||
),
|
||||
)
|
||||
.catch(error => {
|
||||
const cause = stringifyError(error);
|
||||
throw new Error(
|
||||
`Failed to instantiate service '${ref.id}' for '${pluginId}' because the factory function threw an error, ${cause}`,
|
||||
);
|
||||
});
|
||||
implementation.byPlugin.set(pluginId, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
ServiceFactory,
|
||||
BackendFeature,
|
||||
ExtensionPoint,
|
||||
FactoryFunc,
|
||||
ServiceRef,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { BackstageBackend } from './BackstageBackend';
|
||||
@@ -47,7 +46,7 @@ export interface CreateSpecializedBackendOptions {
|
||||
}
|
||||
|
||||
export type ServiceHolder = {
|
||||
get<T>(api: ServiceRef<T>): FactoryFunc<T> | undefined;
|
||||
get<T>(api: ServiceRef<T>, pluginId: string): Promise<T> | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -66,10 +66,10 @@ export interface BackendRegistrationPoints {
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export const cacheServiceRef: ServiceRef<PluginCacheManager>;
|
||||
export const cacheServiceRef: ServiceRef<PluginCacheManager, 'plugin'>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const configServiceRef: ServiceRef<Config>;
|
||||
export const configServiceRef: ServiceRef<Config, 'root'>;
|
||||
|
||||
// @public (undocumented)
|
||||
export function createBackendModule<
|
||||
@@ -105,22 +105,25 @@ export function createExtensionPoint<T>(options: {
|
||||
// @public (undocumented)
|
||||
export function createServiceFactory<
|
||||
TService,
|
||||
TScope extends 'root' | 'plugin',
|
||||
TImpl extends TService,
|
||||
TDeps extends {
|
||||
[name in string]: unknown;
|
||||
[name in string]: ServiceRef<unknown>;
|
||||
},
|
||||
TOpts extends
|
||||
| {
|
||||
[name in string]: unknown;
|
||||
}
|
||||
| undefined = undefined,
|
||||
>(factory: {
|
||||
service: ServiceRef<TService>;
|
||||
deps: TypesToServiceRef<TDeps>;
|
||||
>(config: {
|
||||
service: ServiceRef<TService, TScope>;
|
||||
deps: TDeps;
|
||||
factory(
|
||||
deps: DepsToDepFactories<TDeps>,
|
||||
deps: ServiceRefsToInstances<TDeps, 'root'>,
|
||||
options: TOpts,
|
||||
): Promise<FactoryFunc<TImpl>>;
|
||||
): TScope extends 'root'
|
||||
? Promise<TImpl>
|
||||
: Promise<(deps: ServiceRefsToInstances<TDeps>) => Promise<TImpl>>;
|
||||
}): undefined extends TOpts
|
||||
? (options?: TOpts) => ServiceFactory<TService>
|
||||
: (options: TOpts) => ServiceFactory<TService>;
|
||||
@@ -128,21 +131,26 @@ export function createServiceFactory<
|
||||
// @public (undocumented)
|
||||
export function createServiceRef<T>(options: {
|
||||
id: string;
|
||||
scope?: 'plugin';
|
||||
defaultFactory?: (
|
||||
service: ServiceRef<T>,
|
||||
service: ServiceRef<T, 'plugin'>,
|
||||
) => Promise<ServiceFactory<T> | (() => ServiceFactory<T>)>;
|
||||
}): ServiceRef<T>;
|
||||
}): ServiceRef<T, 'plugin'>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const databaseServiceRef: ServiceRef<PluginDatabaseManager>;
|
||||
export function createServiceRef<T>(options: {
|
||||
id: string;
|
||||
scope: 'root';
|
||||
defaultFactory?: (
|
||||
service: ServiceRef<T, 'root'>,
|
||||
) => Promise<ServiceFactory<T> | (() => ServiceFactory<T>)>;
|
||||
}): ServiceRef<T, 'root'>;
|
||||
|
||||
// @public (undocumented)
|
||||
export type DepsToDepFactories<T> = {
|
||||
[key in keyof T]: (pluginId: string) => Promise<T[key]>;
|
||||
};
|
||||
export const databaseServiceRef: ServiceRef<PluginDatabaseManager, 'plugin'>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const discoveryServiceRef: ServiceRef<PluginEndpointDiscovery>;
|
||||
export const discoveryServiceRef: ServiceRef<PluginEndpointDiscovery, 'plugin'>;
|
||||
|
||||
// @public
|
||||
export type ExtensionPoint<T> = {
|
||||
@@ -152,9 +160,6 @@ export type ExtensionPoint<T> = {
|
||||
$$ref: 'extension-point';
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type FactoryFunc<Impl> = (pluginId: string) => Promise<Impl>;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface HttpRouterService {
|
||||
// (undocumented)
|
||||
@@ -162,7 +167,7 @@ export interface HttpRouterService {
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export const httpRouterServiceRef: ServiceRef<HttpRouterService>;
|
||||
export const httpRouterServiceRef: ServiceRef<HttpRouterService, 'plugin'>;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface Logger {
|
||||
@@ -173,7 +178,7 @@ export interface Logger {
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export const loggerServiceRef: ServiceRef<Logger>;
|
||||
export const loggerServiceRef: ServiceRef<Logger, 'plugin'>;
|
||||
|
||||
// @public (undocumented)
|
||||
export function loggerToWinstonLogger(
|
||||
@@ -183,33 +188,66 @@ export function loggerToWinstonLogger(
|
||||
|
||||
// @public (undocumented)
|
||||
export const permissionsServiceRef: ServiceRef<
|
||||
PermissionAuthorizer | PermissionEvaluator
|
||||
PermissionAuthorizer | PermissionEvaluator,
|
||||
'plugin'
|
||||
>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const schedulerServiceRef: ServiceRef<PluginTaskScheduler>;
|
||||
export interface PluginMetadata {
|
||||
// (undocumented)
|
||||
getId(): string;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export type ServiceFactory<TService = unknown> = {
|
||||
service: ServiceRef<TService>;
|
||||
deps: {
|
||||
[key in string]: ServiceRef<unknown>;
|
||||
};
|
||||
factory(deps: {
|
||||
[key in string]: unknown;
|
||||
}): Promise<FactoryFunc<TService>>;
|
||||
};
|
||||
export const pluginMetadataServiceRef: ServiceRef<PluginMetadata, 'plugin'>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const rootLoggerServiceRef: ServiceRef<Logger, 'root'>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const schedulerServiceRef: ServiceRef<PluginTaskScheduler, 'plugin'>;
|
||||
|
||||
// @public (undocumented)
|
||||
export type ServiceFactory<TService = unknown> =
|
||||
| {
|
||||
scope: 'root';
|
||||
service: ServiceRef<TService, 'root'>;
|
||||
deps: {
|
||||
[key in string]: ServiceRef<unknown>;
|
||||
};
|
||||
factory(deps: {
|
||||
[key in string]: unknown;
|
||||
}): Promise<TService>;
|
||||
}
|
||||
| {
|
||||
scope: 'plugin';
|
||||
service: ServiceRef<TService, 'plugin'>;
|
||||
deps: {
|
||||
[key in string]: ServiceRef<unknown>;
|
||||
};
|
||||
factory(deps: {
|
||||
[key in string]: unknown;
|
||||
}): Promise<
|
||||
(deps: {
|
||||
[key in string]: unknown;
|
||||
}) => Promise<TService>
|
||||
>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type ServiceRef<T> = {
|
||||
export type ServiceRef<
|
||||
TService,
|
||||
TScope extends 'root' | 'plugin' = 'root' | 'plugin',
|
||||
> = {
|
||||
id: string;
|
||||
T: T;
|
||||
scope: TScope;
|
||||
T: TService;
|
||||
toString(): string;
|
||||
$$ref: 'service';
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export const tokenManagerServiceRef: ServiceRef<TokenManager>;
|
||||
export const tokenManagerServiceRef: ServiceRef<TokenManager, 'plugin'>;
|
||||
|
||||
// @public (undocumented)
|
||||
export type TypesToServiceRef<T> = {
|
||||
@@ -217,5 +255,5 @@ export type TypesToServiceRef<T> = {
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export const urlReaderServiceRef: ServiceRef<UrlReader>;
|
||||
export const urlReaderServiceRef: ServiceRef<UrlReader, 'plugin'>;
|
||||
```
|
||||
|
||||
@@ -21,5 +21,6 @@ import { createServiceRef } from '../system/types';
|
||||
* @public
|
||||
*/
|
||||
export const configServiceRef = createServiceRef<Config>({
|
||||
id: 'core.config',
|
||||
id: 'core.root.config',
|
||||
scope: 'root',
|
||||
});
|
||||
|
||||
@@ -26,3 +26,6 @@ export { discoveryServiceRef } from './discoveryServiceRef';
|
||||
export { tokenManagerServiceRef } from './tokenManagerServiceRef';
|
||||
export { permissionsServiceRef } from './permissionsServiceRef';
|
||||
export { schedulerServiceRef } from './schedulerServiceRef';
|
||||
export { rootLoggerServiceRef } from './rootLoggerServiceRef';
|
||||
export { pluginMetadataServiceRef } from './pluginMetadataServiceRef';
|
||||
export type { PluginMetadata } from './pluginMetadataServiceRef';
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2022 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.
|
||||
*/
|
||||
|
||||
import { createServiceRef } from '../system/types';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface PluginMetadata {
|
||||
getId(): string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const pluginMetadataServiceRef = createServiceRef<PluginMetadata>({
|
||||
id: 'core.plugin-metadata',
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2022 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.
|
||||
*/
|
||||
|
||||
import { createServiceRef } from '../system/types';
|
||||
import { Logger } from './loggerServiceRef';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const rootLoggerServiceRef = createServiceRef<Logger>({
|
||||
id: 'core.root.logger',
|
||||
scope: 'root',
|
||||
});
|
||||
@@ -14,11 +14,5 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type {
|
||||
ServiceRef,
|
||||
TypesToServiceRef,
|
||||
DepsToDepFactories,
|
||||
FactoryFunc,
|
||||
ServiceFactory,
|
||||
} from './types';
|
||||
export type { ServiceRef, TypesToServiceRef, ServiceFactory } from './types';
|
||||
export { createServiceRef, createServiceFactory } from './types';
|
||||
|
||||
@@ -19,63 +19,87 @@
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type ServiceRef<T> = {
|
||||
export type ServiceRef<
|
||||
TService,
|
||||
TScope extends 'root' | 'plugin' = 'root' | 'plugin',
|
||||
> = {
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* This determines the scope at which this service is available.
|
||||
*
|
||||
* Root scoped services are available to all other services but
|
||||
* may only depend on other root scoped services.
|
||||
*
|
||||
* Plugin scoped services are only available to other plugin scoped
|
||||
* services but may depend on all other services.
|
||||
*/
|
||||
scope: TScope;
|
||||
|
||||
/**
|
||||
* Utility for getting the type of the service, using `typeof serviceRef.T`.
|
||||
* Attempting to actually read this value will result in an exception.
|
||||
*/
|
||||
T: T;
|
||||
T: TService;
|
||||
|
||||
toString(): string;
|
||||
|
||||
$$ref: 'service';
|
||||
};
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export type InternalServiceRef<T> = ServiceRef<T> & {
|
||||
/**
|
||||
* The default factory that will be used to create service
|
||||
* instances if no other factory is provided.
|
||||
*/
|
||||
__defaultFactory?: (
|
||||
service: ServiceRef<T>,
|
||||
) => Promise<ServiceFactory<T> | (() => ServiceFactory<T>)>;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type TypesToServiceRef<T> = { [key in keyof T]: ServiceRef<T[key]> };
|
||||
|
||||
/** @public */
|
||||
export type DepsToDepFactories<T> = {
|
||||
[key in keyof T]: (pluginId: string) => Promise<T[key]>;
|
||||
};
|
||||
export type ServiceFactory<TService = unknown> =
|
||||
| {
|
||||
// This scope prop is needed in addition to the service ref, as TypeScript
|
||||
// can't properly discriminate the two factory types otherwise.
|
||||
scope: 'root';
|
||||
service: ServiceRef<TService, 'root'>;
|
||||
deps: { [key in string]: ServiceRef<unknown> };
|
||||
factory(deps: { [key in string]: unknown }): Promise<TService>;
|
||||
}
|
||||
| {
|
||||
scope: 'plugin';
|
||||
service: ServiceRef<TService, 'plugin'>;
|
||||
deps: { [key in string]: ServiceRef<unknown> };
|
||||
factory(deps: { [key in string]: unknown }): Promise<
|
||||
(deps: { [key in string]: unknown }) => Promise<TService>
|
||||
>;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type FactoryFunc<Impl> = (pluginId: string) => Promise<Impl>;
|
||||
|
||||
/** @public */
|
||||
export type ServiceFactory<TService = unknown> = {
|
||||
service: ServiceRef<TService>;
|
||||
deps: { [key in string]: ServiceRef<unknown> };
|
||||
factory(deps: { [key in string]: unknown }): Promise<FactoryFunc<TService>>;
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function createServiceRef<T>(options: {
|
||||
id: string;
|
||||
scope?: 'plugin';
|
||||
defaultFactory?: (
|
||||
service: ServiceRef<T>,
|
||||
service: ServiceRef<T, 'plugin'>,
|
||||
) => Promise<ServiceFactory<T> | (() => ServiceFactory<T>)>;
|
||||
}): ServiceRef<T, 'plugin'>;
|
||||
/** @public */
|
||||
export function createServiceRef<T>(options: {
|
||||
id: string;
|
||||
scope: 'root';
|
||||
defaultFactory?: (
|
||||
service: ServiceRef<T, 'root'>,
|
||||
) => Promise<ServiceFactory<T> | (() => ServiceFactory<T>)>;
|
||||
}): ServiceRef<T, 'root'>;
|
||||
export function createServiceRef<T>(options: {
|
||||
id: string;
|
||||
scope?: 'plugin' | 'root';
|
||||
defaultFactory?:
|
||||
| ((
|
||||
service: ServiceRef<T, 'plugin'>,
|
||||
) => Promise<ServiceFactory<T> | (() => ServiceFactory<T>)>)
|
||||
| ((
|
||||
service: ServiceRef<T, 'root'>,
|
||||
) => Promise<ServiceFactory<T> | (() => ServiceFactory<T>)>);
|
||||
}): ServiceRef<T> {
|
||||
const { id, defaultFactory } = options;
|
||||
const { id, scope = 'plugin', defaultFactory } = options;
|
||||
return {
|
||||
id,
|
||||
scope,
|
||||
get T(): T {
|
||||
throw new Error(`tried to read ServiceRef.T of ${this}`);
|
||||
},
|
||||
@@ -84,32 +108,51 @@ export function createServiceRef<T>(options: {
|
||||
},
|
||||
$$ref: 'service', // TODO: declare
|
||||
__defaultFactory: defaultFactory,
|
||||
} as InternalServiceRef<T>;
|
||||
} as ServiceRef<T, typeof scope> & {
|
||||
__defaultFactory?: (
|
||||
service: ServiceRef<T>,
|
||||
) => Promise<ServiceFactory<T> | (() => ServiceFactory<T>)>;
|
||||
};
|
||||
}
|
||||
|
||||
/** @ignore */
|
||||
type ServiceRefsToInstances<
|
||||
T extends { [key in string]: ServiceRef<unknown> },
|
||||
TScope extends 'root' | 'plugin' = 'root' | 'plugin',
|
||||
> = {
|
||||
[name in {
|
||||
[key in keyof T]: T[key] extends ServiceRef<unknown, TScope> ? key : never;
|
||||
}[keyof T]]: T[name] extends ServiceRef<infer TImpl> ? TImpl : never;
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function createServiceFactory<
|
||||
TService,
|
||||
TScope extends 'root' | 'plugin',
|
||||
TImpl extends TService,
|
||||
TDeps extends { [name in string]: unknown },
|
||||
TDeps extends { [name in string]: ServiceRef<unknown> },
|
||||
TOpts extends { [name in string]: unknown } | undefined = undefined,
|
||||
>(factory: {
|
||||
service: ServiceRef<TService>;
|
||||
deps: TypesToServiceRef<TDeps>;
|
||||
>(config: {
|
||||
service: ServiceRef<TService, TScope>;
|
||||
deps: TDeps;
|
||||
factory(
|
||||
deps: DepsToDepFactories<TDeps>,
|
||||
deps: ServiceRefsToInstances<TDeps, 'root'>,
|
||||
options: TOpts,
|
||||
): Promise<FactoryFunc<TImpl>>;
|
||||
): TScope extends 'root'
|
||||
? Promise<TImpl>
|
||||
: Promise<(deps: ServiceRefsToInstances<TDeps>) => Promise<TImpl>>;
|
||||
}): undefined extends TOpts
|
||||
? (options?: TOpts) => ServiceFactory<TService>
|
||||
: (options: TOpts) => ServiceFactory<TService> {
|
||||
return (options?: TOpts) => ({
|
||||
service: factory.service,
|
||||
deps: factory.deps,
|
||||
factory(deps: DepsToDepFactories<TDeps>) {
|
||||
return factory.factory(deps, options!);
|
||||
},
|
||||
});
|
||||
return (options?: TOpts) =>
|
||||
({
|
||||
scope: config.service.scope,
|
||||
service: config.service,
|
||||
deps: config.deps,
|
||||
factory(deps: ServiceRefsToInstances<TDeps, 'root'>) {
|
||||
return config.factory(deps, options!);
|
||||
},
|
||||
} as ServiceFactory<TService>);
|
||||
}
|
||||
|
||||
@@ -287,13 +287,10 @@ export async function loadConfig(
|
||||
let handle: NodeJS.Timeout | undefined;
|
||||
try {
|
||||
handle = setInterval(async () => {
|
||||
console.info(`Checking for config update`);
|
||||
const newRemoteConfigs = await loadRemoteConfigFiles();
|
||||
if (await hasConfigChanged(remoteConfigs, newRemoteConfigs)) {
|
||||
remoteConfigs = newRemoteConfigs;
|
||||
console.info(`Remote config change, reloading config ...`);
|
||||
watchProp.onChange([...remoteConfigs, ...fileConfigs, ...envConfigs]);
|
||||
console.info(`Remote config reloaded`);
|
||||
}
|
||||
}, remoteProp.reloadIntervalSeconds * 1000);
|
||||
} catch (error) {
|
||||
@@ -303,7 +300,6 @@ export async function loadConfig(
|
||||
if (watchProp.stopSignal) {
|
||||
watchProp.stopSignal.then(() => {
|
||||
if (handle !== undefined) {
|
||||
console.info(`Stopping remote config watch`);
|
||||
clearInterval(handle);
|
||||
handle = undefined;
|
||||
}
|
||||
|
||||
@@ -251,7 +251,6 @@ describe.each(['beta', 'stable'])('react-router %s', rrVersion => {
|
||||
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
|
||||
rendered.debug();
|
||||
await expect(
|
||||
rendered.findByText('Path at inside: /foo/bar'),
|
||||
).resolves.toBeInTheDocument();
|
||||
@@ -344,7 +343,6 @@ describe.each(['beta', 'stable'])('react-router %s', rrVersion => {
|
||||
await expect(
|
||||
rendered.findByText('Path at inside: /foo/blob/baz'),
|
||||
).resolves.toBeInTheDocument();
|
||||
rendered.debug();
|
||||
});
|
||||
|
||||
it('should throw errors for routing to other routeRefs with unsupported parameters', () => {
|
||||
@@ -352,6 +350,7 @@ describe.each(['beta', 'stable'])('react-router %s', rrVersion => {
|
||||
const root = (
|
||||
<MemoryRouter initialEntries={['/']}>
|
||||
<Routes>
|
||||
<Route path="/" element={<div />} />
|
||||
<Route path="foo:id" element={<ExtensionPage3 />}>
|
||||
<Route
|
||||
path="bar"
|
||||
|
||||
@@ -294,13 +294,13 @@ describe('discovery', () => {
|
||||
await expect(
|
||||
rendered.findByText('Path at inside: /foo/blob/baz'),
|
||||
).resolves.toBeInTheDocument();
|
||||
rendered.debug();
|
||||
});
|
||||
|
||||
it('should throw errors for routing to other routeRefs with unsupported parameters', () => {
|
||||
const root = (
|
||||
<MemoryRouter initialEntries={['/']}>
|
||||
<Routes>
|
||||
<Route path="/" element={<div />} />
|
||||
<Route path="foo:id" element={<ExtensionPage3 />}>
|
||||
<Route
|
||||
path="bar"
|
||||
|
||||
@@ -38,7 +38,7 @@ const id = {
|
||||
|
||||
const setEdge = jest.fn();
|
||||
const renderElement = jest.fn((props: RenderLabelProps) => (
|
||||
<text>{props.edge.label}</text>
|
||||
<div>{props.edge.label}</div>
|
||||
));
|
||||
|
||||
const minProps = {
|
||||
@@ -53,8 +53,7 @@ const edgeWithLabel = { ...edge, label };
|
||||
|
||||
describe('<Edge />', () => {
|
||||
beforeEach(() => {
|
||||
// jsdom does not support SVG elements so we have to fall back to HTMLUnknownElement
|
||||
Object.defineProperty(window.HTMLUnknownElement.prototype, 'getBBox', {
|
||||
Object.defineProperty(window.SVGElement.prototype, 'getBBox', {
|
||||
value: () => ({ width: 100, height: 100 }),
|
||||
configurable: true,
|
||||
});
|
||||
@@ -63,26 +62,40 @@ describe('<Edge />', () => {
|
||||
afterEach(jest.clearAllMocks);
|
||||
|
||||
it('does not render the supplied label element if label is missing', () => {
|
||||
const { container } = render(<Edge {...minProps} />);
|
||||
const { container } = render(
|
||||
<svg>
|
||||
<Edge {...minProps} />
|
||||
</svg>,
|
||||
);
|
||||
expect(container.getElementsByTagName('g')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('renders the supplied label element if label is present', () => {
|
||||
const { getByText } = render(<Edge {...minProps} edge={edgeWithLabel} />);
|
||||
const { getByText } = render(
|
||||
<svg>
|
||||
<Edge {...minProps} edge={edgeWithLabel} />
|
||||
</svg>,
|
||||
);
|
||||
expect(getByText(label)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('passes down edge properties to the render method if label is present', () => {
|
||||
const edgeWithRandomProp = { ...edge, label, randomProp: true };
|
||||
render(
|
||||
<Edge {...minProps} render={renderElement} edge={edgeWithRandomProp} />,
|
||||
<svg>
|
||||
<Edge {...minProps} render={renderElement} edge={edgeWithRandomProp} />
|
||||
</svg>,
|
||||
);
|
||||
|
||||
expect(renderElement).toHaveBeenCalledWith({ edge: edgeWithRandomProp });
|
||||
});
|
||||
|
||||
it('calls setEdge with edge ID and actual label size after rendering', () => {
|
||||
const { getByText } = render(<Edge {...minProps} edge={edgeWithLabel} />);
|
||||
const { getByText } = render(
|
||||
<svg>
|
||||
<Edge {...minProps} edge={edgeWithLabel} />
|
||||
</svg>,
|
||||
);
|
||||
expect(getByText(label)).toBeInTheDocument();
|
||||
|
||||
// Updates the edge in the graph
|
||||
|
||||
@@ -23,7 +23,7 @@ import { RenderNodeProps } from './types';
|
||||
const node = { id: 'abc', x: 0, y: 0, width: 0, height: 0 };
|
||||
const setNode = jest.fn(() => new dagre.graphlib.Graph());
|
||||
const renderElement = jest.fn((props: RenderNodeProps) => (
|
||||
<text>{props.node.id}</text>
|
||||
<div>{props.node.id}</div>
|
||||
));
|
||||
|
||||
const minProps = {
|
||||
@@ -34,8 +34,7 @@ const minProps = {
|
||||
|
||||
describe('<Node />', () => {
|
||||
beforeEach(() => {
|
||||
// jsdom does not support SVG elements so we have to fall back to HTMLUnknownElement
|
||||
Object.defineProperty(window.HTMLUnknownElement.prototype, 'getBBox', {
|
||||
Object.defineProperty(window.SVGElement.prototype, 'getBBox', {
|
||||
value: () => ({ width: 100, height: 100 }),
|
||||
configurable: true,
|
||||
});
|
||||
@@ -44,19 +43,31 @@ describe('<Node />', () => {
|
||||
afterEach(jest.clearAllMocks);
|
||||
|
||||
it('renders the supplied element', () => {
|
||||
const { getByText } = render(<Node {...minProps} />);
|
||||
const { getByText } = render(
|
||||
<svg>
|
||||
<Node {...minProps} />
|
||||
</svg>,
|
||||
);
|
||||
expect(getByText(minProps.node.id)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('passes down node properties to the render method', () => {
|
||||
const nodeWithRandomProp = { ...node, randomProp: true };
|
||||
render(<Node {...minProps} node={nodeWithRandomProp} />);
|
||||
render(
|
||||
<svg>
|
||||
<Node {...minProps} node={nodeWithRandomProp} />
|
||||
</svg>,
|
||||
);
|
||||
|
||||
expect(renderElement).toHaveBeenCalledWith({ node: nodeWithRandomProp });
|
||||
});
|
||||
|
||||
it('calls setNode with node ID and actual size after rendering', () => {
|
||||
const { getByText } = render(<Node {...minProps} />);
|
||||
const { getByText } = render(
|
||||
<svg>
|
||||
<Node {...minProps} />
|
||||
</svg>,
|
||||
);
|
||||
expect(getByText(minProps.node.id)).toBeInTheDocument();
|
||||
|
||||
// Updates the node in the graph
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { renderInTestApp, withLogCollector } from '@backstage/test-utils';
|
||||
|
||||
import { GaugeCard } from './GaugeCard';
|
||||
|
||||
@@ -40,7 +40,12 @@ describe('<GaugeCard />', () => {
|
||||
|
||||
it('handles invalid numbers', async () => {
|
||||
const badProps = { title: 'Tingle upgrade', progress: 'hejjo' } as any;
|
||||
const { getByText } = await renderInTestApp(<GaugeCard {...badProps} />);
|
||||
expect(getByText(/N\/A.*/)).toBeInTheDocument();
|
||||
const { error } = await withLogCollector(async () => {
|
||||
const { getByText } = await renderInTestApp(<GaugeCard {...badProps} />);
|
||||
expect(getByText(/N\/A.*/)).toBeInTheDocument();
|
||||
});
|
||||
expect(error).toEqual([
|
||||
expect.stringMatching(/^Warning: `NaN` is an invalid value/),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -58,6 +58,7 @@ describe('<HeaderTabs />', () => {
|
||||
const TextualBadge = React.forwardRef<HTMLSpanElement>((props, ref) => (
|
||||
<Badge
|
||||
classes={useStyles()}
|
||||
overlap="rectangular"
|
||||
color="secondary"
|
||||
badgeContent="three new alarms"
|
||||
>
|
||||
|
||||
@@ -14,7 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { renderWithEffects, TestApiProvider } from '@backstage/test-utils';
|
||||
import {
|
||||
renderWithEffects,
|
||||
TestApiProvider,
|
||||
withLogCollector,
|
||||
} from '@backstage/test-utils';
|
||||
import { HomepageTimer } from './HomepageTimer';
|
||||
import React from 'react';
|
||||
import { lightTheme } from '@backstage/theme';
|
||||
@@ -35,13 +39,18 @@ it('changes default timezone to GMT', async () => {
|
||||
context: 'test',
|
||||
});
|
||||
|
||||
const rendered = await renderWithEffects(
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<TestApiProvider apis={[[configApiRef, configApi]]}>
|
||||
<HomepageTimer />
|
||||
</TestApiProvider>
|
||||
</ThemeProvider>,
|
||||
);
|
||||
const { warn } = await withLogCollector(async () => {
|
||||
const rendered = await renderWithEffects(
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<TestApiProvider apis={[[configApiRef, configApi]]}>
|
||||
<HomepageTimer />
|
||||
</TestApiProvider>
|
||||
</ThemeProvider>,
|
||||
);
|
||||
|
||||
expect(rendered.getByText('GMT')).toBeInTheDocument();
|
||||
expect(rendered.getByText('GMT')).toBeInTheDocument();
|
||||
});
|
||||
expect(warn).toEqual([
|
||||
'The timezone America/New_Pork is invalid. Defaulting to GMT',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -61,11 +61,19 @@ describe('extensions', () => {
|
||||
const Component = () => <div />;
|
||||
const routeRef = createRouteRef({ id: 'foo' });
|
||||
|
||||
const extension1 = createComponentExtension({
|
||||
component: {
|
||||
sync: Component,
|
||||
},
|
||||
let extension1: ReturnType<typeof createComponentExtension>;
|
||||
const { warn } = withLogCollector(['warn'], () => {
|
||||
extension1 = createComponentExtension({
|
||||
component: {
|
||||
sync: Component,
|
||||
},
|
||||
});
|
||||
});
|
||||
expect(warn).toEqual([
|
||||
expect.stringMatching(
|
||||
/^Declaring extensions without name is DEPRECATED. /,
|
||||
),
|
||||
]);
|
||||
|
||||
const extension2 = createRoutableExtension({
|
||||
name: 'Extension2',
|
||||
@@ -73,7 +81,7 @@ describe('extensions', () => {
|
||||
mountPoint: routeRef,
|
||||
});
|
||||
|
||||
const ExtensionComponent1 = plugin.provide(extension1);
|
||||
const ExtensionComponent1 = plugin.provide(extension1!);
|
||||
const ExtensionComponent2 = plugin.provide(extension2);
|
||||
|
||||
const element1 = <ExtensionComponent1 />;
|
||||
|
||||
@@ -19,6 +19,7 @@ import mockFs from 'mock-fs';
|
||||
import child_process from 'child_process';
|
||||
import path from 'path';
|
||||
import {
|
||||
Task,
|
||||
buildAppTask,
|
||||
checkAppExistsTask,
|
||||
checkPathExistsTask,
|
||||
@@ -27,6 +28,13 @@ import {
|
||||
templatingTask,
|
||||
} from './tasks';
|
||||
|
||||
jest.spyOn(Task, 'log').mockReturnValue(undefined);
|
||||
jest.spyOn(Task, 'error').mockReturnValue(undefined);
|
||||
jest.spyOn(Task, 'section').mockReturnValue(undefined);
|
||||
jest
|
||||
.spyOn(Task, 'forItem')
|
||||
.mockImplementation((_a, _b, taskFunc) => taskFunc());
|
||||
|
||||
jest.mock('child_process');
|
||||
|
||||
// By mocking this the filesystem mocks won't mess with reading all of the package.jsons
|
||||
|
||||
@@ -54,6 +54,8 @@ catalog:
|
||||
# and for the syntax https://docs.microsoft.com/en-us/graph/query-parameters#filter-parameter
|
||||
# This and userGroupMemberFilter are mutually exclusive, only one can be specified
|
||||
filter: accountEnabled eq true and userType eq 'member'
|
||||
# See https://docs.microsoft.com/en-us/graph/api/resources/schemaextension?view=graph-rest-1.0
|
||||
select: ['id', 'displayName', 'description']
|
||||
# Optional configuration block
|
||||
userGroupMember:
|
||||
# Optional filter for users, use group membership to get users.
|
||||
|
||||
@@ -193,6 +193,7 @@ export type MicrosoftGraphProviderConfig = {
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
userFilter?: string;
|
||||
userSelect?: string[];
|
||||
userExpand?: string;
|
||||
userGroupMemberFilter?: string;
|
||||
userGroupMemberSearch?: string;
|
||||
@@ -232,6 +233,7 @@ export function readMicrosoftGraphOrg(
|
||||
options: {
|
||||
userExpand?: string;
|
||||
userFilter?: string;
|
||||
userSelect?: string[];
|
||||
userGroupMemberSearch?: string;
|
||||
userGroupMemberFilter?: string;
|
||||
groupExpand?: string;
|
||||
|
||||
@@ -74,6 +74,12 @@ export interface Config {
|
||||
* E.g. "securityEnabled eq false and mailEnabled eq true"
|
||||
*/
|
||||
groupFilter?: string;
|
||||
/**
|
||||
* The fields to be fetched on query.
|
||||
*
|
||||
* E.g. ["id", "displayName", "description"]
|
||||
*/
|
||||
userSelect?: string[];
|
||||
/**
|
||||
* The search criteria to apply to extract users by groups memberships.
|
||||
*
|
||||
|
||||
@@ -62,6 +62,12 @@ export type MicrosoftGraphProviderConfig = {
|
||||
* E.g. "accountEnabled eq true and userType eq 'member'"
|
||||
*/
|
||||
userFilter?: string;
|
||||
/**
|
||||
* The fields to be fetched on query.
|
||||
*
|
||||
* E.g. ["id", "displayName", "description"]
|
||||
*/
|
||||
userSelect?: string[];
|
||||
/**
|
||||
* The "expand" argument to apply to users.
|
||||
*
|
||||
@@ -144,6 +150,7 @@ export function readMicrosoftGraphConfig(
|
||||
|
||||
const userExpand = providerConfig.getOptionalString('userExpand');
|
||||
const userFilter = providerConfig.getOptionalString('userFilter');
|
||||
const userSelect = providerConfig.getOptionalStringArray('userSelect');
|
||||
const userGroupMemberFilter = providerConfig.getOptionalString(
|
||||
'userGroupMemberFilter',
|
||||
);
|
||||
@@ -196,6 +203,7 @@ export function readMicrosoftGraphConfig(
|
||||
clientSecret,
|
||||
userExpand,
|
||||
userFilter,
|
||||
userSelect,
|
||||
userGroupMemberFilter,
|
||||
userGroupMemberSearch,
|
||||
groupExpand,
|
||||
|
||||
@@ -1002,6 +1002,12 @@ describe('read microsoft graph', () => {
|
||||
};
|
||||
}
|
||||
|
||||
async function* getExampleUsersEmail() {
|
||||
yield {
|
||||
mail: 'user.name@example.com',
|
||||
};
|
||||
}
|
||||
|
||||
async function getExampleUserProfile(userId: string) {
|
||||
return {
|
||||
id: userId,
|
||||
@@ -1109,6 +1115,34 @@ describe('read microsoft graph', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should read users with userSelect', async () => {
|
||||
client.getOrganization.mockResolvedValue({
|
||||
id: 'tenantid',
|
||||
displayName: 'Organization Name',
|
||||
});
|
||||
|
||||
client.getUsers.mockImplementation(getExampleUsersEmail);
|
||||
client.getUserPhotoWithSizeLimit.mockResolvedValue(
|
||||
'data:image/jpeg;base64,...',
|
||||
);
|
||||
|
||||
client.getGroups.mockImplementation(getExampleGroups);
|
||||
client.getGroupMembers.mockImplementation(getExampleGroupMembers);
|
||||
|
||||
await readMicrosoftGraphOrg(client, 'tenantid', {
|
||||
logger: getVoidLogger(),
|
||||
userSelect: ['mail'],
|
||||
});
|
||||
|
||||
expect(client.getUsers).toHaveBeenCalledTimes(1);
|
||||
expect(client.getUsers).toHaveBeenCalledWith(
|
||||
{
|
||||
select: ['mail'],
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('should read users using userExpand and userGroupMemberFilter', async () => {
|
||||
client.getOrganization.mockResolvedValue({
|
||||
id: 'tenantid',
|
||||
|
||||
@@ -89,6 +89,7 @@ export async function readMicrosoftGraphUsers(
|
||||
queryMode?: 'basic' | 'advanced';
|
||||
userFilter?: string;
|
||||
userExpand?: string;
|
||||
userSelect?: string[];
|
||||
transformer?: UserTransformer;
|
||||
logger: Logger;
|
||||
},
|
||||
@@ -105,6 +106,7 @@ export async function readMicrosoftGraphUsers(
|
||||
{
|
||||
filter: options.userFilter,
|
||||
expand: options.userExpand,
|
||||
select: options.userSelect,
|
||||
},
|
||||
options.queryMode,
|
||||
)) {
|
||||
@@ -145,6 +147,7 @@ export async function readMicrosoftGraphUsersInGroups(
|
||||
options: {
|
||||
queryMode?: 'basic' | 'advanced';
|
||||
userExpand?: string;
|
||||
userSelect?: string[];
|
||||
userGroupMemberSearch?: string;
|
||||
userGroupMemberFilter?: string;
|
||||
groupExpand?: string;
|
||||
@@ -534,6 +537,7 @@ export async function readMicrosoftGraphOrg(
|
||||
options: {
|
||||
userExpand?: string;
|
||||
userFilter?: string;
|
||||
userSelect?: string[];
|
||||
userGroupMemberSearch?: string;
|
||||
userGroupMemberFilter?: string;
|
||||
groupExpand?: string;
|
||||
@@ -565,6 +569,7 @@ export async function readMicrosoftGraphOrg(
|
||||
const { users: usersWithFilter } = await readMicrosoftGraphUsers(client, {
|
||||
queryMode: options.queryMode,
|
||||
userFilter: options.userFilter,
|
||||
userSelect: options.userSelect,
|
||||
userExpand: options.userExpand,
|
||||
transformer: options.userTransformer,
|
||||
logger: options.logger,
|
||||
|
||||
@@ -109,7 +109,7 @@ export type CatalogProcessorResult =
|
||||
| CatalogProcessorRefreshKeysResult;
|
||||
|
||||
// @alpha
|
||||
export const catalogServiceRef: ServiceRef<CatalogApi>;
|
||||
export const catalogServiceRef: ServiceRef<CatalogApi, 'plugin'>;
|
||||
|
||||
// @public
|
||||
export type DeferredEntity = {
|
||||
|
||||
@@ -31,13 +31,11 @@ export const catalogServiceRef = createServiceRef<CatalogApi>({
|
||||
createServiceFactory({
|
||||
service,
|
||||
deps: {
|
||||
discoveryFactory: discoveryServiceRef,
|
||||
discoveryApi: discoveryServiceRef,
|
||||
},
|
||||
factory: async ({ discoveryFactory }) => {
|
||||
const discoveryApi = await discoveryFactory('root');
|
||||
const catalogClient = new CatalogClient({ discoveryApi });
|
||||
return async _pluginId => {
|
||||
return catalogClient;
|
||||
async factory() {
|
||||
return async ({ discoveryApi }) => {
|
||||
return new CatalogClient({ discoveryApi });
|
||||
};
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -436,6 +436,7 @@ export function humanizeEntityRef(
|
||||
entityRef: Entity | CompoundEntityRef,
|
||||
opts?: {
|
||||
defaultKind?: string;
|
||||
defaultNamespace?: string | false;
|
||||
},
|
||||
): string;
|
||||
|
||||
|
||||
@@ -34,6 +34,23 @@ describe('humanizeEntityRef', () => {
|
||||
expect(title).toEqual('component:software');
|
||||
});
|
||||
|
||||
it('formats entity in default namespace without skipping default namespace', () => {
|
||||
const entity = {
|
||||
apiVersion: 'v1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'software',
|
||||
},
|
||||
spec: {
|
||||
owner: 'guest',
|
||||
type: 'service',
|
||||
lifecycle: 'production',
|
||||
},
|
||||
};
|
||||
const title = humanizeEntityRef(entity, { defaultNamespace: false });
|
||||
expect(title).toEqual('component:default/software');
|
||||
});
|
||||
|
||||
it('formats entity in other namespace', () => {
|
||||
const entity = {
|
||||
apiVersion: 'v1',
|
||||
@@ -52,6 +69,24 @@ describe('humanizeEntityRef', () => {
|
||||
expect(title).toEqual('component:test/software');
|
||||
});
|
||||
|
||||
it('formats entity in other namespace and hides this namespace', () => {
|
||||
const entity = {
|
||||
apiVersion: 'v1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'software',
|
||||
namespace: 'test',
|
||||
},
|
||||
spec: {
|
||||
owner: 'guest',
|
||||
type: 'service',
|
||||
lifecycle: 'production',
|
||||
},
|
||||
};
|
||||
const title = humanizeEntityRef(entity, { defaultNamespace: 'test' });
|
||||
expect(title).toEqual('component:software');
|
||||
});
|
||||
|
||||
it('formats entity and hides default kind', () => {
|
||||
const entity = {
|
||||
apiVersion: 'v1',
|
||||
@@ -70,6 +105,27 @@ describe('humanizeEntityRef', () => {
|
||||
expect(title).toEqual('test/software');
|
||||
});
|
||||
|
||||
it('formats entity and hides default kind and hiding namespace', () => {
|
||||
const entity = {
|
||||
apiVersion: 'v1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'software',
|
||||
namespace: 'test',
|
||||
},
|
||||
spec: {
|
||||
owner: 'guest',
|
||||
type: 'service',
|
||||
lifecycle: 'production',
|
||||
},
|
||||
};
|
||||
const title = humanizeEntityRef(entity, {
|
||||
defaultKind: 'Component',
|
||||
defaultNamespace: 'test',
|
||||
});
|
||||
expect(title).toEqual('software');
|
||||
});
|
||||
|
||||
it('formats entity name in default namespace', () => {
|
||||
const entityName = {
|
||||
kind: 'Component',
|
||||
@@ -80,6 +136,16 @@ describe('humanizeEntityRef', () => {
|
||||
expect(title).toEqual('component:software');
|
||||
});
|
||||
|
||||
it('formats entity name in default namespace and does not skip default namespace', () => {
|
||||
const entityName = {
|
||||
kind: 'Component',
|
||||
namespace: 'default',
|
||||
name: 'software',
|
||||
};
|
||||
const title = humanizeEntityRef(entityName, { defaultNamespace: false });
|
||||
expect(title).toEqual('component:default/software');
|
||||
});
|
||||
|
||||
it('formats entity name in other namespace', () => {
|
||||
const entityName = {
|
||||
kind: 'Component',
|
||||
@@ -91,6 +157,19 @@ describe('humanizeEntityRef', () => {
|
||||
expect(title).toEqual('component:test/software');
|
||||
});
|
||||
|
||||
it('formats entity name in other namespace with skipping this namespace', () => {
|
||||
const entityName = {
|
||||
kind: 'Component',
|
||||
namespace: 'test',
|
||||
name: 'software',
|
||||
};
|
||||
|
||||
const title = humanizeEntityRef(entityName, {
|
||||
defaultNamespace: 'test',
|
||||
});
|
||||
expect(title).toEqual('component:software');
|
||||
});
|
||||
|
||||
it('renders link for entity name and hides default kind', () => {
|
||||
const entityName = {
|
||||
kind: 'Component',
|
||||
@@ -103,4 +182,31 @@ describe('humanizeEntityRef', () => {
|
||||
});
|
||||
expect(title).toEqual('test/software');
|
||||
});
|
||||
|
||||
it('renders link for entity name and hides default kind with skipping namespace', () => {
|
||||
const entityName = {
|
||||
kind: 'Component',
|
||||
namespace: 'test',
|
||||
name: 'software',
|
||||
};
|
||||
|
||||
const title = humanizeEntityRef(entityName, {
|
||||
defaultKind: 'component',
|
||||
defaultNamespace: 'test',
|
||||
});
|
||||
expect(title).toEqual('software');
|
||||
});
|
||||
|
||||
it('formats entity name in default namespace without skip of default namespace', () => {
|
||||
const entityName = {
|
||||
kind: 'Component',
|
||||
namespace: 'default',
|
||||
name: 'software',
|
||||
};
|
||||
|
||||
const title = humanizeEntityRef(entityName, {
|
||||
defaultNamespace: false,
|
||||
});
|
||||
expect(title).toEqual('component:default/software');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,10 +20,17 @@ import {
|
||||
DEFAULT_NAMESPACE,
|
||||
} from '@backstage/catalog-model';
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* @param defaultNamespace - if set to false then namespace is never omitted,
|
||||
* if set to string which matches namespace of entity then omitted
|
||||
*
|
||||
* @public */
|
||||
export function humanizeEntityRef(
|
||||
entityRef: Entity | CompoundEntityRef,
|
||||
opts?: { defaultKind?: string },
|
||||
opts?: {
|
||||
defaultKind?: string;
|
||||
defaultNamespace?: string | false;
|
||||
},
|
||||
) {
|
||||
const defaultKind = opts?.defaultKind;
|
||||
let kind;
|
||||
@@ -40,7 +47,14 @@ export function humanizeEntityRef(
|
||||
name = entityRef.name;
|
||||
}
|
||||
|
||||
if (namespace === DEFAULT_NAMESPACE) {
|
||||
if (namespace === undefined || namespace === '') {
|
||||
namespace = DEFAULT_NAMESPACE;
|
||||
}
|
||||
if (opts?.defaultNamespace !== undefined) {
|
||||
if (opts?.defaultNamespace === namespace) {
|
||||
namespace = undefined;
|
||||
}
|
||||
} else if (namespace === DEFAULT_NAMESPACE) {
|
||||
namespace = undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -76,8 +76,12 @@ export const EntityTagsPickerFieldExtension: FieldExtensionComponent<
|
||||
|
||||
// @public
|
||||
export interface EntityTagsPickerUiOptions {
|
||||
// (undocumented)
|
||||
helperText?: string;
|
||||
// (undocumented)
|
||||
kinds?: string[];
|
||||
// (undocumented)
|
||||
showCounts?: boolean;
|
||||
}
|
||||
|
||||
// @public
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
import React, { useState } from 'react';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import useEffectOnce from 'react-use/lib/useEffectOnce';
|
||||
import { GetEntitiesRequest } from '@backstage/catalog-client';
|
||||
import { Entity, makeValidator } from '@backstage/catalog-model';
|
||||
import { GetEntityFacetsRequest } from '@backstage/catalog-client';
|
||||
import { makeValidator } from '@backstage/catalog-model';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
import { FormControl, TextField } from '@material-ui/core';
|
||||
@@ -32,6 +32,8 @@ import { FieldExtensionComponentProps } from '../../../extensions';
|
||||
*/
|
||||
export interface EntityTagsPickerUiOptions {
|
||||
kinds?: string[];
|
||||
showCounts?: boolean;
|
||||
helperText?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,26 +47,34 @@ export const EntityTagsPicker = (
|
||||
) => {
|
||||
const { formData, onChange, uiSchema } = props;
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const [tagOptions, setTagOptions] = useState<string[]>([]);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [inputError, setInputError] = useState(false);
|
||||
const tagValidator = makeValidator().isValidTag;
|
||||
const kinds = uiSchema['ui:options']?.kinds;
|
||||
const showCounts = uiSchema['ui:options']?.showCounts;
|
||||
const helperText = uiSchema['ui:options']?.helperText;
|
||||
|
||||
const { loading, value: existingTags } = useAsync(async () => {
|
||||
const tagsRequest: GetEntitiesRequest = { fields: ['metadata.tags'] };
|
||||
const facet = 'metadata.tags';
|
||||
const tagsRequest: GetEntityFacetsRequest = { facets: [facet] };
|
||||
if (kinds) {
|
||||
tagsRequest.filter = { kind: kinds };
|
||||
}
|
||||
|
||||
const entities = await catalogApi.getEntities(tagsRequest);
|
||||
const { facets } = await catalogApi.getEntityFacets(tagsRequest);
|
||||
|
||||
return [
|
||||
...new Set(
|
||||
entities.items
|
||||
.flatMap((e: Entity) => e.metadata?.tags)
|
||||
.filter(Boolean) as string[],
|
||||
const tagFacets = Object.fromEntries(
|
||||
facets[facet].map(({ value, count }) => [value, count]),
|
||||
);
|
||||
|
||||
setTagOptions(
|
||||
Object.keys(tagFacets).sort((a, b) =>
|
||||
showCounts ? tagFacets[b] - tagFacets[a] : a.localeCompare(b),
|
||||
),
|
||||
].sort();
|
||||
);
|
||||
|
||||
return tagFacets;
|
||||
});
|
||||
|
||||
const setTags = (_: React.ChangeEvent<{}>, values: string[] | null) => {
|
||||
@@ -102,15 +112,21 @@ export const EntityTagsPicker = (
|
||||
value={formData || []}
|
||||
inputValue={inputValue}
|
||||
loading={loading}
|
||||
options={existingTags || []}
|
||||
options={tagOptions}
|
||||
ChipProps={{ size: 'small' }}
|
||||
renderOption={option =>
|
||||
showCounts ? `${option} (${existingTags?.[option]})` : option
|
||||
}
|
||||
renderInput={params => (
|
||||
<TextField
|
||||
{...params}
|
||||
label="Tags"
|
||||
onChange={e => setInputValue(e.target.value)}
|
||||
error={inputError}
|
||||
helperText="Add any relevant tags, hit 'Enter' to add new tags. Valid format: [a-z0-9+#] separated by [-], at most 63 characters"
|
||||
helperText={
|
||||
helperText ??
|
||||
"Add any relevant tags, hit 'Enter' to add new tags. Valid format: [a-z0-9+#] separated by [-], at most 63 characters"
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
+55
-55
@@ -2866,126 +2866,126 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-android-arm-eabi@npm:1.2.247":
|
||||
version: 1.2.247
|
||||
resolution: "@swc/core-android-arm-eabi@npm:1.2.247"
|
||||
"@swc/core-android-arm-eabi@npm:1.2.249":
|
||||
version: 1.2.249
|
||||
resolution: "@swc/core-android-arm-eabi@npm:1.2.249"
|
||||
dependencies:
|
||||
"@swc/wasm": 1.2.122
|
||||
conditions: os=android & cpu=arm
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-android-arm64@npm:1.2.247":
|
||||
version: 1.2.247
|
||||
resolution: "@swc/core-android-arm64@npm:1.2.247"
|
||||
"@swc/core-android-arm64@npm:1.2.249":
|
||||
version: 1.2.249
|
||||
resolution: "@swc/core-android-arm64@npm:1.2.249"
|
||||
dependencies:
|
||||
"@swc/wasm": 1.2.130
|
||||
conditions: os=android & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-darwin-arm64@npm:1.2.247":
|
||||
version: 1.2.247
|
||||
resolution: "@swc/core-darwin-arm64@npm:1.2.247"
|
||||
"@swc/core-darwin-arm64@npm:1.2.249":
|
||||
version: 1.2.249
|
||||
resolution: "@swc/core-darwin-arm64@npm:1.2.249"
|
||||
conditions: os=darwin & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-darwin-x64@npm:1.2.247":
|
||||
version: 1.2.247
|
||||
resolution: "@swc/core-darwin-x64@npm:1.2.247"
|
||||
"@swc/core-darwin-x64@npm:1.2.249":
|
||||
version: 1.2.249
|
||||
resolution: "@swc/core-darwin-x64@npm:1.2.249"
|
||||
conditions: os=darwin & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-freebsd-x64@npm:1.2.247":
|
||||
version: 1.2.247
|
||||
resolution: "@swc/core-freebsd-x64@npm:1.2.247"
|
||||
"@swc/core-freebsd-x64@npm:1.2.249":
|
||||
version: 1.2.249
|
||||
resolution: "@swc/core-freebsd-x64@npm:1.2.249"
|
||||
dependencies:
|
||||
"@swc/wasm": 1.2.130
|
||||
conditions: os=freebsd & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-linux-arm-gnueabihf@npm:1.2.247":
|
||||
version: 1.2.247
|
||||
resolution: "@swc/core-linux-arm-gnueabihf@npm:1.2.247"
|
||||
"@swc/core-linux-arm-gnueabihf@npm:1.2.249":
|
||||
version: 1.2.249
|
||||
resolution: "@swc/core-linux-arm-gnueabihf@npm:1.2.249"
|
||||
dependencies:
|
||||
"@swc/wasm": 1.2.130
|
||||
conditions: os=linux & cpu=arm
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-linux-arm64-gnu@npm:1.2.247":
|
||||
version: 1.2.247
|
||||
resolution: "@swc/core-linux-arm64-gnu@npm:1.2.247"
|
||||
"@swc/core-linux-arm64-gnu@npm:1.2.249":
|
||||
version: 1.2.249
|
||||
resolution: "@swc/core-linux-arm64-gnu@npm:1.2.249"
|
||||
conditions: os=linux & cpu=arm64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-linux-arm64-musl@npm:1.2.247":
|
||||
version: 1.2.247
|
||||
resolution: "@swc/core-linux-arm64-musl@npm:1.2.247"
|
||||
"@swc/core-linux-arm64-musl@npm:1.2.249":
|
||||
version: 1.2.249
|
||||
resolution: "@swc/core-linux-arm64-musl@npm:1.2.249"
|
||||
conditions: os=linux & cpu=arm64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-linux-x64-gnu@npm:1.2.247":
|
||||
version: 1.2.247
|
||||
resolution: "@swc/core-linux-x64-gnu@npm:1.2.247"
|
||||
"@swc/core-linux-x64-gnu@npm:1.2.249":
|
||||
version: 1.2.249
|
||||
resolution: "@swc/core-linux-x64-gnu@npm:1.2.249"
|
||||
conditions: os=linux & cpu=x64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-linux-x64-musl@npm:1.2.247":
|
||||
version: 1.2.247
|
||||
resolution: "@swc/core-linux-x64-musl@npm:1.2.247"
|
||||
"@swc/core-linux-x64-musl@npm:1.2.249":
|
||||
version: 1.2.249
|
||||
resolution: "@swc/core-linux-x64-musl@npm:1.2.249"
|
||||
conditions: os=linux & cpu=x64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-win32-arm64-msvc@npm:1.2.247":
|
||||
version: 1.2.247
|
||||
resolution: "@swc/core-win32-arm64-msvc@npm:1.2.247"
|
||||
"@swc/core-win32-arm64-msvc@npm:1.2.249":
|
||||
version: 1.2.249
|
||||
resolution: "@swc/core-win32-arm64-msvc@npm:1.2.249"
|
||||
dependencies:
|
||||
"@swc/wasm": 1.2.130
|
||||
conditions: os=win32 & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-win32-ia32-msvc@npm:1.2.247":
|
||||
version: 1.2.247
|
||||
resolution: "@swc/core-win32-ia32-msvc@npm:1.2.247"
|
||||
"@swc/core-win32-ia32-msvc@npm:1.2.249":
|
||||
version: 1.2.249
|
||||
resolution: "@swc/core-win32-ia32-msvc@npm:1.2.249"
|
||||
dependencies:
|
||||
"@swc/wasm": 1.2.130
|
||||
conditions: os=win32 & cpu=ia32
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-win32-x64-msvc@npm:1.2.247":
|
||||
version: 1.2.247
|
||||
resolution: "@swc/core-win32-x64-msvc@npm:1.2.247"
|
||||
"@swc/core-win32-x64-msvc@npm:1.2.249":
|
||||
version: 1.2.249
|
||||
resolution: "@swc/core-win32-x64-msvc@npm:1.2.249"
|
||||
conditions: os=win32 & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core@npm:^1.2.239":
|
||||
version: 1.2.247
|
||||
resolution: "@swc/core@npm:1.2.247"
|
||||
version: 1.2.249
|
||||
resolution: "@swc/core@npm:1.2.249"
|
||||
dependencies:
|
||||
"@swc/core-android-arm-eabi": 1.2.247
|
||||
"@swc/core-android-arm64": 1.2.247
|
||||
"@swc/core-darwin-arm64": 1.2.247
|
||||
"@swc/core-darwin-x64": 1.2.247
|
||||
"@swc/core-freebsd-x64": 1.2.247
|
||||
"@swc/core-linux-arm-gnueabihf": 1.2.247
|
||||
"@swc/core-linux-arm64-gnu": 1.2.247
|
||||
"@swc/core-linux-arm64-musl": 1.2.247
|
||||
"@swc/core-linux-x64-gnu": 1.2.247
|
||||
"@swc/core-linux-x64-musl": 1.2.247
|
||||
"@swc/core-win32-arm64-msvc": 1.2.247
|
||||
"@swc/core-win32-ia32-msvc": 1.2.247
|
||||
"@swc/core-win32-x64-msvc": 1.2.247
|
||||
"@swc/core-android-arm-eabi": 1.2.249
|
||||
"@swc/core-android-arm64": 1.2.249
|
||||
"@swc/core-darwin-arm64": 1.2.249
|
||||
"@swc/core-darwin-x64": 1.2.249
|
||||
"@swc/core-freebsd-x64": 1.2.249
|
||||
"@swc/core-linux-arm-gnueabihf": 1.2.249
|
||||
"@swc/core-linux-arm64-gnu": 1.2.249
|
||||
"@swc/core-linux-arm64-musl": 1.2.249
|
||||
"@swc/core-linux-x64-gnu": 1.2.249
|
||||
"@swc/core-linux-x64-musl": 1.2.249
|
||||
"@swc/core-win32-arm64-msvc": 1.2.249
|
||||
"@swc/core-win32-ia32-msvc": 1.2.249
|
||||
"@swc/core-win32-x64-msvc": 1.2.249
|
||||
dependenciesMeta:
|
||||
"@swc/core-android-arm-eabi":
|
||||
optional: true
|
||||
@@ -3015,7 +3015,7 @@ __metadata:
|
||||
optional: true
|
||||
bin:
|
||||
swcx: run_swcx.js
|
||||
checksum: 8ad850c5637405473cb7680865c0b1bdd9e955c0f32e86e6564d43c6a3fb0941b69d7ca152b661694e614530c69fe9824f32d10b606564187730d1d146f62805
|
||||
checksum: c47f17fefccd94fb7be3787e832f3e3fd62c07e22f8bf566435bd1de032efd9fdefb4a64e0eefd684a9a24902509a235afeacb5ed93e03512fa09064bf1e1268
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
@@ -13047,126 +13047,126 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-android-arm-eabi@npm:1.2.247":
|
||||
version: 1.2.247
|
||||
resolution: "@swc/core-android-arm-eabi@npm:1.2.247"
|
||||
"@swc/core-android-arm-eabi@npm:1.2.249":
|
||||
version: 1.2.249
|
||||
resolution: "@swc/core-android-arm-eabi@npm:1.2.249"
|
||||
dependencies:
|
||||
"@swc/wasm": 1.2.122
|
||||
conditions: os=android & cpu=arm
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-android-arm64@npm:1.2.247":
|
||||
version: 1.2.247
|
||||
resolution: "@swc/core-android-arm64@npm:1.2.247"
|
||||
"@swc/core-android-arm64@npm:1.2.249":
|
||||
version: 1.2.249
|
||||
resolution: "@swc/core-android-arm64@npm:1.2.249"
|
||||
dependencies:
|
||||
"@swc/wasm": 1.2.130
|
||||
conditions: os=android & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-darwin-arm64@npm:1.2.247":
|
||||
version: 1.2.247
|
||||
resolution: "@swc/core-darwin-arm64@npm:1.2.247"
|
||||
"@swc/core-darwin-arm64@npm:1.2.249":
|
||||
version: 1.2.249
|
||||
resolution: "@swc/core-darwin-arm64@npm:1.2.249"
|
||||
conditions: os=darwin & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-darwin-x64@npm:1.2.247":
|
||||
version: 1.2.247
|
||||
resolution: "@swc/core-darwin-x64@npm:1.2.247"
|
||||
"@swc/core-darwin-x64@npm:1.2.249":
|
||||
version: 1.2.249
|
||||
resolution: "@swc/core-darwin-x64@npm:1.2.249"
|
||||
conditions: os=darwin & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-freebsd-x64@npm:1.2.247":
|
||||
version: 1.2.247
|
||||
resolution: "@swc/core-freebsd-x64@npm:1.2.247"
|
||||
"@swc/core-freebsd-x64@npm:1.2.249":
|
||||
version: 1.2.249
|
||||
resolution: "@swc/core-freebsd-x64@npm:1.2.249"
|
||||
dependencies:
|
||||
"@swc/wasm": 1.2.130
|
||||
conditions: os=freebsd & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-linux-arm-gnueabihf@npm:1.2.247":
|
||||
version: 1.2.247
|
||||
resolution: "@swc/core-linux-arm-gnueabihf@npm:1.2.247"
|
||||
"@swc/core-linux-arm-gnueabihf@npm:1.2.249":
|
||||
version: 1.2.249
|
||||
resolution: "@swc/core-linux-arm-gnueabihf@npm:1.2.249"
|
||||
dependencies:
|
||||
"@swc/wasm": 1.2.130
|
||||
conditions: os=linux & cpu=arm
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-linux-arm64-gnu@npm:1.2.247":
|
||||
version: 1.2.247
|
||||
resolution: "@swc/core-linux-arm64-gnu@npm:1.2.247"
|
||||
"@swc/core-linux-arm64-gnu@npm:1.2.249":
|
||||
version: 1.2.249
|
||||
resolution: "@swc/core-linux-arm64-gnu@npm:1.2.249"
|
||||
conditions: os=linux & cpu=arm64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-linux-arm64-musl@npm:1.2.247":
|
||||
version: 1.2.247
|
||||
resolution: "@swc/core-linux-arm64-musl@npm:1.2.247"
|
||||
"@swc/core-linux-arm64-musl@npm:1.2.249":
|
||||
version: 1.2.249
|
||||
resolution: "@swc/core-linux-arm64-musl@npm:1.2.249"
|
||||
conditions: os=linux & cpu=arm64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-linux-x64-gnu@npm:1.2.247":
|
||||
version: 1.2.247
|
||||
resolution: "@swc/core-linux-x64-gnu@npm:1.2.247"
|
||||
"@swc/core-linux-x64-gnu@npm:1.2.249":
|
||||
version: 1.2.249
|
||||
resolution: "@swc/core-linux-x64-gnu@npm:1.2.249"
|
||||
conditions: os=linux & cpu=x64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-linux-x64-musl@npm:1.2.247":
|
||||
version: 1.2.247
|
||||
resolution: "@swc/core-linux-x64-musl@npm:1.2.247"
|
||||
"@swc/core-linux-x64-musl@npm:1.2.249":
|
||||
version: 1.2.249
|
||||
resolution: "@swc/core-linux-x64-musl@npm:1.2.249"
|
||||
conditions: os=linux & cpu=x64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-win32-arm64-msvc@npm:1.2.247":
|
||||
version: 1.2.247
|
||||
resolution: "@swc/core-win32-arm64-msvc@npm:1.2.247"
|
||||
"@swc/core-win32-arm64-msvc@npm:1.2.249":
|
||||
version: 1.2.249
|
||||
resolution: "@swc/core-win32-arm64-msvc@npm:1.2.249"
|
||||
dependencies:
|
||||
"@swc/wasm": 1.2.130
|
||||
conditions: os=win32 & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-win32-ia32-msvc@npm:1.2.247":
|
||||
version: 1.2.247
|
||||
resolution: "@swc/core-win32-ia32-msvc@npm:1.2.247"
|
||||
"@swc/core-win32-ia32-msvc@npm:1.2.249":
|
||||
version: 1.2.249
|
||||
resolution: "@swc/core-win32-ia32-msvc@npm:1.2.249"
|
||||
dependencies:
|
||||
"@swc/wasm": 1.2.130
|
||||
conditions: os=win32 & cpu=ia32
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core-win32-x64-msvc@npm:1.2.247":
|
||||
version: 1.2.247
|
||||
resolution: "@swc/core-win32-x64-msvc@npm:1.2.247"
|
||||
"@swc/core-win32-x64-msvc@npm:1.2.249":
|
||||
version: 1.2.249
|
||||
resolution: "@swc/core-win32-x64-msvc@npm:1.2.249"
|
||||
conditions: os=win32 & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/core@npm:^1.2.239":
|
||||
version: 1.2.247
|
||||
resolution: "@swc/core@npm:1.2.247"
|
||||
version: 1.2.249
|
||||
resolution: "@swc/core@npm:1.2.249"
|
||||
dependencies:
|
||||
"@swc/core-android-arm-eabi": 1.2.247
|
||||
"@swc/core-android-arm64": 1.2.247
|
||||
"@swc/core-darwin-arm64": 1.2.247
|
||||
"@swc/core-darwin-x64": 1.2.247
|
||||
"@swc/core-freebsd-x64": 1.2.247
|
||||
"@swc/core-linux-arm-gnueabihf": 1.2.247
|
||||
"@swc/core-linux-arm64-gnu": 1.2.247
|
||||
"@swc/core-linux-arm64-musl": 1.2.247
|
||||
"@swc/core-linux-x64-gnu": 1.2.247
|
||||
"@swc/core-linux-x64-musl": 1.2.247
|
||||
"@swc/core-win32-arm64-msvc": 1.2.247
|
||||
"@swc/core-win32-ia32-msvc": 1.2.247
|
||||
"@swc/core-win32-x64-msvc": 1.2.247
|
||||
"@swc/core-android-arm-eabi": 1.2.249
|
||||
"@swc/core-android-arm64": 1.2.249
|
||||
"@swc/core-darwin-arm64": 1.2.249
|
||||
"@swc/core-darwin-x64": 1.2.249
|
||||
"@swc/core-freebsd-x64": 1.2.249
|
||||
"@swc/core-linux-arm-gnueabihf": 1.2.249
|
||||
"@swc/core-linux-arm64-gnu": 1.2.249
|
||||
"@swc/core-linux-arm64-musl": 1.2.249
|
||||
"@swc/core-linux-x64-gnu": 1.2.249
|
||||
"@swc/core-linux-x64-musl": 1.2.249
|
||||
"@swc/core-win32-arm64-msvc": 1.2.249
|
||||
"@swc/core-win32-ia32-msvc": 1.2.249
|
||||
"@swc/core-win32-x64-msvc": 1.2.249
|
||||
dependenciesMeta:
|
||||
"@swc/core-android-arm-eabi":
|
||||
optional: true
|
||||
@@ -13196,7 +13196,7 @@ __metadata:
|
||||
optional: true
|
||||
bin:
|
||||
swcx: run_swcx.js
|
||||
checksum: 8ad850c5637405473cb7680865c0b1bdd9e955c0f32e86e6564d43c6a3fb0941b69d7ca152b661694e614530c69fe9824f32d10b606564187730d1d146f62805
|
||||
checksum: c47f17fefccd94fb7be3787e832f3e3fd62c07e22f8bf566435bd1de032efd9fdefb4a64e0eefd684a9a24902509a235afeacb5ed93e03512fa09064bf1e1268
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user